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