Annotation of embedaddon/php/ext/intl/collator/collator_sort.c, revision 1.1.1.2
1.1 misho 1: /*
2: +----------------------------------------------------------------------+
3: | PHP Version 5 |
4: +----------------------------------------------------------------------+
5: | This source file is subject to version 3.01 of the PHP license, |
6: | that is bundled with this package in the file LICENSE, and is |
7: | available through the world-wide-web at the following url: |
8: | http://www.php.net/license/3_01.txt |
9: | If you did not receive a copy of the PHP license and are unable to |
10: | obtain it through the world-wide-web, please send a note to |
11: | license@php.net so we can mail you a copy immediately. |
12: +----------------------------------------------------------------------+
13: | Authors: Vadim Savchuk <vsavchuk@productengine.com> |
14: | Dmitry Lakhtyuk <dlakhtyuk@productengine.com> |
15: +----------------------------------------------------------------------+
16: */
17:
18: #ifdef HAVE_CONFIG_H
19: #include "config.h"
20: #endif
21:
22: #include "php_intl.h"
23: #include "collator.h"
24: #include "collator_class.h"
25: #include "collator_sort.h"
26: #include "collator_convert.h"
27: #include "intl_convert.h"
28:
29: #if !defined(HAVE_PTRDIFF_T) && !defined(_PTRDIFF_T_DEFINED)
30: typedef long ptrdiff_t;
31: #endif
32:
33: /**
34: * Declare 'index' which will point to sort key in sort key
35: * buffer.
36: */
37: typedef struct _collator_sort_key_index {
38: char* key; /* pointer to sort key */
39: zval** zstr; /* pointer to original string(hash-item) */
40: } collator_sort_key_index_t;
41:
42: ZEND_EXTERN_MODULE_GLOBALS( intl )
43:
44: static const size_t DEF_SORT_KEYS_BUF_SIZE = 1048576;
45: static const size_t DEF_SORT_KEYS_BUF_INCREMENT = 1048576;
46:
47: static const size_t DEF_SORT_KEYS_INDX_BUF_SIZE = 1048576;
48: static const size_t DEF_SORT_KEYS_INDX_BUF_INCREMENT = 1048576;
49:
50: static const size_t DEF_UTF16_BUF_SIZE = 1024;
51:
52: /* {{{ collator_regular_compare_function */
53: static int collator_regular_compare_function(zval *result, zval *op1, zval *op2 TSRMLS_DC)
54: {
55: Collator_object* co = NULL;
56:
57: int rc = SUCCESS;
58:
59: zval* str1 = collator_convert_object_to_string( op1 TSRMLS_CC );
60: zval* str2 = collator_convert_object_to_string( op2 TSRMLS_CC );
61:
62: zval* num1 = NULL;
63: zval* num2 = NULL;
64: zval* norm1 = NULL;
65: zval* norm2 = NULL;
66:
67: /* If both args are strings AND either of args is not numeric string
68: * then use ICU-compare. Otherwise PHP-compare. */
69: if( Z_TYPE_P(str1) == IS_STRING && Z_TYPE_P(str2) == IS_STRING &&
70: ( str1 == ( num1 = collator_convert_string_to_number_if_possible( str1 ) ) ||
71: str2 == ( num2 = collator_convert_string_to_number_if_possible( str2 ) ) ) )
72: {
73: /* Fetch collator object. */
74: co = (Collator_object *) zend_object_store_get_object( INTL_G(current_collator) TSRMLS_CC );
75:
76: if (!co || !co->ucoll) {
77: intl_error_set_code( NULL, COLLATOR_ERROR_CODE( co ) TSRMLS_CC );
78: intl_errors_set_custom_msg( COLLATOR_ERROR_P( co ),
79: "Object not initialized", 0 TSRMLS_CC );
80: php_error_docref(NULL TSRMLS_CC, E_RECOVERABLE_ERROR, "Object not initialized");
81: }
82:
83: /* Compare the strings using ICU. */
84: result->value.lval = ucol_strcoll(
85: co->ucoll,
86: INTL_Z_STRVAL_P(str1), INTL_Z_STRLEN_P(str1),
87: INTL_Z_STRVAL_P(str2), INTL_Z_STRLEN_P(str2) );
88: result->type = IS_LONG;
89: }
90: else
91: {
92: /* num1 is set if str1 and str2 are strings. */
93: if( num1 )
94: {
95: if( num1 == str1 )
96: {
97: /* str1 is string but not numeric string
98: * just convert it to utf8.
99: */
100: norm1 = collator_convert_zstr_utf16_to_utf8( str1 );
101:
102: /* num2 is not set but str2 is string => do normalization. */
103: norm2 = collator_normalize_sort_argument( str2 );
104: }
105: else
106: {
107: /* str1 is numeric strings => passthru to PHP-compare. */
108: zval_add_ref( &num1 );
109: norm1 = num1;
110:
111: /* str2 is numeric strings => passthru to PHP-compare. */
112: zval_add_ref( &num2 );
113: norm2 = num2;
114: }
115: }
116: else
117: {
118: /* num1 is not set if str1 or str2 is not a string => do normalization. */
119: norm1 = collator_normalize_sort_argument( str1 );
120:
121: /* if num1 is not set then num2 is not set as well => do normalization. */
122: norm2 = collator_normalize_sort_argument( str2 );
123: }
124:
125: rc = compare_function( result, norm1, norm2 TSRMLS_CC );
126:
127: zval_ptr_dtor( &norm1 );
128: zval_ptr_dtor( &norm2 );
129: }
130:
131: if( num1 )
132: zval_ptr_dtor( &num1 );
133:
134: if( num2 )
135: zval_ptr_dtor( &num2 );
136:
137: zval_ptr_dtor( &str1 );
138: zval_ptr_dtor( &str2 );
139:
140: return rc;
141: }
142: /* }}} */
143:
144: /* {{{ collator_numeric_compare_function
145: * Convert input args to double and compare it.
146: */
147: static int collator_numeric_compare_function(zval *result, zval *op1, zval *op2 TSRMLS_DC)
148: {
149: int rc = SUCCESS;
150: zval* num1 = NULL;
151: zval* num2 = NULL;
152:
153: if( Z_TYPE_P(op1) == IS_STRING )
154: {
155: num1 = collator_convert_string_to_double( op1 );
156: op1 = num1;
157: }
158:
159: if( Z_TYPE_P(op2) == IS_STRING )
160: {
161: num2 = collator_convert_string_to_double( op2 );
162: op2 = num2;
163: }
164:
165: rc = numeric_compare_function( result, op1, op2 TSRMLS_CC);
166:
167: if( num1 )
168: zval_ptr_dtor( &num1 );
169: if( num2 )
170: zval_ptr_dtor( &num2 );
171:
172: return rc;
173: }
174: /* }}} */
175:
176: /* {{{ collator_icu_compare_function
177: * Direct use of ucol_strcoll.
178: */
179: static int collator_icu_compare_function(zval *result, zval *op1, zval *op2 TSRMLS_DC)
180: {
181: int rc = SUCCESS;
182: Collator_object* co = NULL;
183: zval* str1 = NULL;
184: zval* str2 = NULL;
185:
186: str1 = collator_make_printable_zval( op1 );
187: str2 = collator_make_printable_zval( op2 );
188:
189: /* Fetch collator object. */
190: co = (Collator_object *) zend_object_store_get_object( INTL_G(current_collator) TSRMLS_CC );
191:
192: /* Compare the strings using ICU. */
193: result->value.lval = ucol_strcoll(
194: co->ucoll,
195: INTL_Z_STRVAL_P(str1), INTL_Z_STRLEN_P(str1),
196: INTL_Z_STRVAL_P(str2), INTL_Z_STRLEN_P(str2) );
197: result->type = IS_LONG;
198:
199: zval_ptr_dtor( &str1 );
200: zval_ptr_dtor( &str2 );
201:
202: return rc;
203: }
204: /* }}} */
205:
206: /* {{{ collator_compare_func
207: * Taken from PHP5 source (array_data_compare).
208: */
209: static int collator_compare_func( const void* a, const void* b TSRMLS_DC )
210: {
211: Bucket *f;
212: Bucket *s;
213: zval result;
214: zval *first;
215: zval *second;
216:
217: f = *((Bucket **) a);
218: s = *((Bucket **) b);
219:
220: first = *((zval **) f->pData);
221: second = *((zval **) s->pData);
222:
223: if( INTL_G(compare_func)( &result, first, second TSRMLS_CC) == FAILURE )
224: return 0;
225:
226: if( Z_TYPE(result) == IS_DOUBLE )
227: {
228: if( Z_DVAL(result) < 0 )
229: return -1;
230: else if( Z_DVAL(result) > 0 )
231: return 1;
232: else
233: return 0;
234: }
235:
236: convert_to_long(&result);
237:
238: if( Z_LVAL(result) < 0 )
239: return -1;
240: else if( Z_LVAL(result) > 0 )
241: return 1;
242:
243: return 0;
244: }
245: /* }}} */
246:
247: /* {{{ collator_cmp_sort_keys
248: * Compare sort keys
249: */
250: static int collator_cmp_sort_keys( const void *p1, const void *p2 TSRMLS_DC )
251: {
252: char* key1 = ((collator_sort_key_index_t*)p1)->key;
253: char* key2 = ((collator_sort_key_index_t*)p2)->key;
254:
255: return strcmp( key1, key2 );
256: }
257: /* }}} */
258:
259: /* {{{ collator_get_compare_function
260: * Choose compare function according to sort flags.
261: */
262: static collator_compare_func_t collator_get_compare_function( const long sort_flags )
263: {
264: collator_compare_func_t func;
265:
266: switch( sort_flags )
267: {
268: case COLLATOR_SORT_NUMERIC:
269: func = collator_numeric_compare_function;
270: break;
271:
272: case COLLATOR_SORT_STRING:
273: func = collator_icu_compare_function;
274: break;
275:
276: case COLLATOR_SORT_REGULAR:
277: default:
278: func = collator_regular_compare_function;
279: break;
280: }
281:
282: return func;
283: }
284: /* }}} */
285:
286: /* {{{ collator_sort_internal
287: * Common code shared by collator_sort() and collator_asort() API functions.
288: */
289: static void collator_sort_internal( int renumber, INTERNAL_FUNCTION_PARAMETERS )
290: {
291: zval* array = NULL;
292: HashTable* hash = NULL;
293: zval* saved_collator = NULL;
294: long sort_flags = COLLATOR_SORT_REGULAR;
295:
296: COLLATOR_METHOD_INIT_VARS
297:
298: /* Parse parameters. */
299: if( zend_parse_method_parameters( ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "Oa|l",
300: &object, Collator_ce_ptr, &array, &sort_flags ) == FAILURE )
301: {
302: intl_error_set( NULL, U_ILLEGAL_ARGUMENT_ERROR,
303: "collator_sort_internal: unable to parse input params", 0 TSRMLS_CC );
304:
305: RETURN_FALSE;
306: }
307:
308: /* Fetch the object. */
309: COLLATOR_METHOD_FETCH_OBJECT;
310:
311: /* Set 'compare function' according to sort flags. */
312: INTL_G(compare_func) = collator_get_compare_function( sort_flags );
313:
314: hash = HASH_OF( array );
315:
316: /* Convert strings in the specified array from UTF-8 to UTF-16. */
317: collator_convert_hash_from_utf8_to_utf16( hash, COLLATOR_ERROR_CODE_P( co ) );
318: COLLATOR_CHECK_STATUS( co, "Error converting hash from UTF-8 to UTF-16" );
319:
320: /* Save specified collator in the request-global (?) variable. */
321: saved_collator = INTL_G( current_collator );
322: INTL_G( current_collator ) = object;
323:
324: /* Sort specified array. */
325: zend_hash_sort( hash, zend_qsort, collator_compare_func, renumber TSRMLS_CC );
326:
327: /* Restore saved collator. */
328: INTL_G( current_collator ) = saved_collator;
329:
330: /* Convert strings in the specified array back to UTF-8. */
331: collator_convert_hash_from_utf16_to_utf8( hash, COLLATOR_ERROR_CODE_P( co ) );
332: COLLATOR_CHECK_STATUS( co, "Error converting hash from UTF-16 to UTF-8" );
333:
334: RETURN_TRUE;
335: }
336: /* }}} */
337:
338: /* {{{ proto bool Collator::sort( Collator $coll, array(string) $arr [, int $sort_flags] )
339: * Sort array using specified collator. }}} */
340: /* {{{ proto bool collator_sort( Collator $coll, array(string) $arr [, int $sort_flags] )
341: * Sort array using specified collator.
342: */
343: PHP_FUNCTION( collator_sort )
344: {
345: collator_sort_internal( TRUE, INTERNAL_FUNCTION_PARAM_PASSTHRU );
346: }
347: /* }}} */
348:
349: /* {{{ proto bool Collator::sortWithSortKeys( Collator $coll, array(string) $arr )
350: * Equivalent to standard PHP sort using Collator.
351: * Uses ICU ucol_getSortKey for performance. }}} */
352: /* {{{ proto bool collator_sort_with_sort_keys( Collator $coll, array(string) $arr )
353: * Equivalent to standard PHP sort using Collator.
354: * Uses ICU ucol_getSortKey for performance.
355: */
356: PHP_FUNCTION( collator_sort_with_sort_keys )
357: {
358: zval* array = NULL;
359: HashTable* hash = NULL;
360: zval** hashData = NULL; /* currently processed item of input hash */
361:
362: char* sortKeyBuf = NULL; /* buffer to store sort keys */
363: uint32_t sortKeyBufSize = DEF_SORT_KEYS_BUF_SIZE; /* buffer size */
364: ptrdiff_t sortKeyBufOffset = 0; /* pos in buffer to store sort key */
365: int32_t sortKeyLen = 0; /* the length of currently processing key */
366: uint32_t bufLeft = 0;
367: uint32_t bufIncrement = 0;
368:
369: collator_sort_key_index_t* sortKeyIndxBuf = NULL; /* buffer to store 'indexes' which will be passed to 'qsort' */
370: uint32_t sortKeyIndxBufSize = DEF_SORT_KEYS_INDX_BUF_SIZE;
371: uint32_t sortKeyIndxSize = sizeof( collator_sort_key_index_t );
372:
373: uint32_t sortKeyCount = 0;
374: uint32_t j = 0;
375:
376: UChar* utf16_buf = NULL; /* tmp buffer to hold current processing string in utf-16 */
377: int utf16_buf_size = DEF_UTF16_BUF_SIZE; /* the length of utf16_buf */
378: int utf16_len = 0; /* length of converted string */
379:
380: HashTable* sortedHash = NULL;
381:
382: COLLATOR_METHOD_INIT_VARS
383:
384: /* Parse parameters. */
385: if( zend_parse_method_parameters( ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "Oa",
386: &object, Collator_ce_ptr, &array ) == FAILURE )
387: {
388: intl_error_set( NULL, U_ILLEGAL_ARGUMENT_ERROR,
389: "collator_sort_with_sort_keys: unable to parse input params", 0 TSRMLS_CC );
390:
391: RETURN_FALSE;
392: }
393:
394: /* Fetch the object. */
395: COLLATOR_METHOD_FETCH_OBJECT;
396:
397: if (!co || !co->ucoll) {
398: intl_error_set_code( NULL, COLLATOR_ERROR_CODE( co ) TSRMLS_CC );
399: intl_errors_set_custom_msg( COLLATOR_ERROR_P( co ),
400: "Object not initialized", 0 TSRMLS_CC );
401: php_error_docref(NULL TSRMLS_CC, E_RECOVERABLE_ERROR, "Object not initialized");
402:
403: RETURN_FALSE;
404: }
405:
406: /*
407: * Sort specified array.
408: */
409: hash = HASH_OF( array );
410:
411: if( !hash || zend_hash_num_elements( hash ) == 0 )
412: RETURN_TRUE;
413:
414: /* Create bufers */
415: sortKeyBuf = ecalloc( sortKeyBufSize, sizeof( char ) );
416: sortKeyIndxBuf = ecalloc( sortKeyIndxBufSize, sizeof( uint8_t ) );
417: utf16_buf = eumalloc( utf16_buf_size );
418:
419: /* Iterate through input hash and create a sort key for each value. */
420: zend_hash_internal_pointer_reset( hash );
421: while( zend_hash_get_current_data( hash, (void**) &hashData ) == SUCCESS )
422: {
423: /* Convert current hash item from UTF-8 to UTF-16LE and save the result to utf16_buf. */
424:
425: utf16_len = utf16_buf_size;
426:
427: /* Process string values only. */
428: if( Z_TYPE_PP( hashData ) == IS_STRING )
429: {
430: intl_convert_utf8_to_utf16( &utf16_buf, &utf16_len, Z_STRVAL_PP( hashData ), Z_STRLEN_PP( hashData ), COLLATOR_ERROR_CODE_P( co ) );
431:
432: if( U_FAILURE( COLLATOR_ERROR_CODE( co ) ) )
433: {
434: intl_error_set_code( NULL, COLLATOR_ERROR_CODE( co ) TSRMLS_CC );
435: intl_errors_set_custom_msg( COLLATOR_ERROR_P( co ), "Sort with sort keys failed", 0 TSRMLS_CC );
436:
437: if( utf16_buf )
438: efree( utf16_buf );
439:
440: efree( sortKeyIndxBuf );
441: efree( sortKeyBuf );
442:
443: RETURN_FALSE;
444: }
445: }
446: else
447: {
448: /* Set empty string */
449: utf16_len = 0;
450: utf16_buf[utf16_len] = 0;
451: }
452:
453: if( (utf16_len + 1) > utf16_buf_size )
454: utf16_buf_size = utf16_len + 1;
455:
456: /* Get sort key, reallocating the buffer if needed. */
457: bufLeft = sortKeyBufSize - sortKeyBufOffset;
458:
459: sortKeyLen = ucol_getSortKey( co->ucoll,
460: utf16_buf,
461: utf16_len,
462: (uint8_t*)sortKeyBuf + sortKeyBufOffset,
463: bufLeft );
464:
465: /* check for sortKeyBuf overflow, increasing its size of the buffer if needed */
466: if( sortKeyLen > bufLeft )
467: {
468: bufIncrement = ( sortKeyLen > DEF_SORT_KEYS_BUF_INCREMENT ) ? sortKeyLen : DEF_SORT_KEYS_BUF_INCREMENT;
469:
470: sortKeyBufSize += bufIncrement;
471: bufLeft += bufIncrement;
472:
473: sortKeyBuf = erealloc( sortKeyBuf, sortKeyBufSize );
474:
475: sortKeyLen = ucol_getSortKey( co->ucoll, utf16_buf, utf16_len, (uint8_t*)sortKeyBuf + sortKeyBufOffset, bufLeft );
476: }
477:
478: /* check sortKeyIndxBuf overflow, increasing its size of the buffer if needed */
479: if( ( sortKeyCount + 1 ) * sortKeyIndxSize > sortKeyIndxBufSize )
480: {
481: bufIncrement = ( sortKeyIndxSize > DEF_SORT_KEYS_INDX_BUF_INCREMENT ) ? sortKeyIndxSize : DEF_SORT_KEYS_INDX_BUF_INCREMENT;
482:
483: sortKeyIndxBufSize += bufIncrement;
484:
485: sortKeyIndxBuf = erealloc( sortKeyIndxBuf, sortKeyIndxBufSize );
486: }
487:
488: sortKeyIndxBuf[sortKeyCount].key = (char*)sortKeyBufOffset; /* remeber just offset, cause address */
489: /* of 'sortKeyBuf' may be changed due to realloc. */
490: sortKeyIndxBuf[sortKeyCount].zstr = hashData;
491:
492: sortKeyBufOffset += sortKeyLen;
493: ++sortKeyCount;
494:
495: zend_hash_move_forward( hash );
496: }
497:
498: /* update ptrs to point to valid keys. */
499: for( j = 0; j < sortKeyCount; j++ )
500: sortKeyIndxBuf[j].key = sortKeyBuf + (ptrdiff_t)sortKeyIndxBuf[j].key;
501:
502: /* sort it */
503: zend_qsort( sortKeyIndxBuf, sortKeyCount, sortKeyIndxSize, collator_cmp_sort_keys TSRMLS_CC );
504:
505: /* for resulting hash we'll assign new hash keys rather then reordering */
506: ALLOC_HASHTABLE( sortedHash );
507: zend_hash_init( sortedHash, 0, NULL, ZVAL_PTR_DTOR, 0 );
508:
509: for( j = 0; j < sortKeyCount; j++ )
510: {
511: zval_add_ref( sortKeyIndxBuf[j].zstr );
512: zend_hash_next_index_insert( sortedHash, sortKeyIndxBuf[j].zstr, sizeof(zval **), NULL );
513: }
514:
515: /* Save sorted hash into return variable. */
516: zval_dtor( array );
517: (array)->value.ht = sortedHash;
518: (array)->type = IS_ARRAY;
519:
520: if( utf16_buf )
521: efree( utf16_buf );
522:
523: efree( sortKeyIndxBuf );
524: efree( sortKeyBuf );
525:
526: RETURN_TRUE;
527: }
528: /* }}} */
529:
530: /* {{{ proto bool Collator::asort( Collator $coll, array(string) $arr )
531: * Sort array using specified collator, maintaining index association. }}} */
532: /* {{{ proto bool collator_asort( Collator $coll, array(string) $arr )
533: * Sort array using specified collator, maintaining index association.
534: */
535: PHP_FUNCTION( collator_asort )
536: {
537: collator_sort_internal( FALSE, INTERNAL_FUNCTION_PARAM_PASSTHRU );
538: }
539: /* }}} */
540:
541: /* {{{ proto bool Collator::getSortKey( Collator $coll, string $str )
542: * Get a sort key for a string from a Collator. }}} */
543: /* {{{ proto bool collator_get_sort_key( Collator $coll, string $str )
544: * Get a sort key for a string from a Collator. }}} */
545: PHP_FUNCTION( collator_get_sort_key )
546: {
547: char* str = NULL;
548: int str_len = 0;
549: UChar* ustr = NULL;
550: int ustr_len = 0;
551: uint8_t* key = NULL;
552: int key_len = 0;
553:
554: COLLATOR_METHOD_INIT_VARS
555:
556: /* Parse parameters. */
557: if( zend_parse_method_parameters( ZEND_NUM_ARGS() TSRMLS_CC, getThis(), "Os",
558: &object, Collator_ce_ptr, &str, &str_len ) == FAILURE )
559: {
560: intl_error_set( NULL, U_ILLEGAL_ARGUMENT_ERROR,
561: "collator_get_sort_key: unable to parse input params", 0 TSRMLS_CC );
562:
563: RETURN_FALSE;
564: }
565:
566: /* Fetch the object. */
567: COLLATOR_METHOD_FETCH_OBJECT;
568:
569: if (!co || !co->ucoll) {
570: intl_error_set_code( NULL, COLLATOR_ERROR_CODE( co ) TSRMLS_CC );
571: intl_errors_set_custom_msg( COLLATOR_ERROR_P( co ),
572: "Object not initialized", 0 TSRMLS_CC );
573: php_error_docref(NULL TSRMLS_CC, E_RECOVERABLE_ERROR, "Object not initialized");
574:
575: RETURN_FALSE;
576: }
577:
578: /*
579: * Compare given strings (converting them to UTF-16 first).
580: */
581:
582: /* First convert the strings to UTF-16. */
583: intl_convert_utf8_to_utf16(
584: &ustr, &ustr_len, str, str_len, COLLATOR_ERROR_CODE_P( co ) );
585: if( U_FAILURE( COLLATOR_ERROR_CODE( co ) ) )
586: {
587: /* Set global error code. */
588: intl_error_set_code( NULL, COLLATOR_ERROR_CODE( co ) TSRMLS_CC );
589:
590: /* Set error messages. */
591: intl_errors_set_custom_msg( COLLATOR_ERROR_P( co ),
592: "Error converting first argument to UTF-16", 0 TSRMLS_CC );
593: efree( ustr );
594: RETURN_FALSE;
595: }
596:
1.1.1.2 ! misho 597: /* ucol_getSortKey is exception in that the key length includes the
! 598: * NUL terminator*/
1.1 misho 599: key_len = ucol_getSortKey(co->ucoll, ustr, ustr_len, key, 0);
600: if(!key_len) {
601: efree( ustr );
602: RETURN_FALSE;
603: }
604: key = emalloc(key_len);
605: key_len = ucol_getSortKey(co->ucoll, ustr, ustr_len, key, key_len);
606: efree( ustr );
607: if(!key_len) {
608: RETURN_FALSE;
609: }
1.1.1.2 ! misho 610: RETURN_STRINGL((char *)key, key_len - 1, 0);
1.1 misho 611: }
612: /* }}} */
613:
614: /*
615: * Local variables:
616: * tab-width: 4
617: * c-basic-offset: 4
618: * End:
619: * vim600: noet sw=4 ts=4 fdm=marker
620: * vim<600: noet sw=4 ts=4
621: */
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>