1 /*
2 +----------------------------------------------------------------------+
3 | PHP Version 7 |
4 +----------------------------------------------------------------------+
5 | Copyright (c) 2006-2017 The PHP Group |
6 +----------------------------------------------------------------------+
7 | This source file is subject to version 3.01 of the PHP license, |
8 | that is bundled with this package in the file LICENSE, and is |
9 | available through the world-wide-web at the following url: |
10 | http://www.php.net/license/3_01.txt |
11 | If you did not receive a copy of the PHP license and are unable to |
12 | obtain it through the world-wide-web, please send a note to |
13 | license@php.net so we can mail you a copy immediately. |
14 +----------------------------------------------------------------------+
15 | Authors: Andrey Hristov <andrey@php.net> |
16 | Ulf Wendel <uw@php.net> |
17 | Georg Richter <georg@php.net> |
18 +----------------------------------------------------------------------+
19 */
20
21 #include "php.h"
22 #include "mysqlnd.h"
23 #include "mysqlnd_wireprotocol.h"
24 #include "mysqlnd_priv.h"
25 #include "mysqlnd_debug.h"
26 #include "ext/mysqlnd/mysql_float_to_double.h"
27
28 #define MYSQLND_SILENT
29
30
31 enum mysqlnd_timestamp_type
32 {
33 MYSQLND_TIMESTAMP_NONE= -2,
34 MYSQLND_TIMESTAMP_ERROR= -1,
35 MYSQLND_TIMESTAMP_DATE= 0,
36 MYSQLND_TIMESTAMP_DATETIME= 1,
37 MYSQLND_TIMESTAMP_TIME= 2
38 };
39
40
41 struct st_mysqlnd_time
42 {
43 unsigned int year, month, day, hour, minute, second;
44 zend_ulong second_part;
45 zend_bool neg;
46 enum mysqlnd_timestamp_type time_type;
47 };
48
49
50 struct st_mysqlnd_perm_bind mysqlnd_ps_fetch_functions[MYSQL_TYPE_LAST + 1];
51
52 #define MYSQLND_PS_SKIP_RESULT_W_LEN -1
53 #define MYSQLND_PS_SKIP_RESULT_STR -2
54
55 /* {{{ ps_fetch_from_1_to_8_bytes */
56 void
ps_fetch_from_1_to_8_bytes(zval * zv,const MYSQLND_FIELD * const field,unsigned int pack_len,zend_uchar ** row,unsigned int byte_count)57 ps_fetch_from_1_to_8_bytes(zval * zv, const MYSQLND_FIELD * const field, unsigned int pack_len,
58 zend_uchar ** row, unsigned int byte_count)
59 {
60 char tmp[22];
61 size_t tmp_len = 0;
62 zend_bool is_bit = field->type == MYSQL_TYPE_BIT;
63 DBG_ENTER("ps_fetch_from_1_to_8_bytes");
64 DBG_INF_FMT("zv=%p byte_count=%u", zv, byte_count);
65 if (field->flags & UNSIGNED_FLAG) {
66 uint64_t uval = 0;
67
68 switch (byte_count) {
69 case 8:uval = is_bit? (uint64_t) bit_uint8korr(*row):(uint64_t) uint8korr(*row);break;
70 case 7:uval = bit_uint7korr(*row);break;
71 case 6:uval = bit_uint6korr(*row);break;
72 case 5:uval = bit_uint5korr(*row);break;
73 case 4:uval = is_bit? (uint64_t) bit_uint4korr(*row):(uint64_t) uint4korr(*row);break;
74 case 3:uval = is_bit? (uint64_t) bit_uint3korr(*row):(uint64_t) uint3korr(*row);break;
75 case 2:uval = is_bit? (uint64_t) bit_uint2korr(*row):(uint64_t) uint2korr(*row);break;
76 case 1:uval = (uint64_t) uint1korr(*row);break;
77 }
78
79 #if SIZEOF_ZEND_LONG==4
80 if (uval > INT_MAX) {
81 DBG_INF("stringify");
82 tmp_len = sprintf((char *)&tmp, MYSQLND_LLU_SPEC, uval);
83 } else
84 #endif /* #if SIZEOF_LONG==4 */
85 {
86 if (byte_count < 8 || uval <= L64(9223372036854775807)) {
87 ZVAL_LONG(zv, (zend_long) uval); /* the cast is safe, we are in the range */
88 } else {
89 DBG_INF("stringify");
90 tmp_len = sprintf((char *)&tmp, MYSQLND_LLU_SPEC, uval);
91 DBG_INF_FMT("value=%s", tmp);
92 }
93 }
94 } else {
95 /* SIGNED */
96 int64_t lval = 0;
97 switch (byte_count) {
98 case 8:lval = (int64_t) sint8korr(*row);break;
99 /*
100 7, 6 and 5 are not possible.
101 BIT is only unsigned, thus only uint5|6|7 macroses exist
102 */
103 case 4:lval = (int64_t) sint4korr(*row);break;
104 case 3:lval = (int64_t) sint3korr(*row);break;
105 case 2:lval = (int64_t) sint2korr(*row);break;
106 case 1:lval = (int64_t) *(int8_t*)*row;break;
107 }
108
109 #if SIZEOF_ZEND_LONG==4
110 if ((L64(2147483647) < (int64_t) lval) || (L64(-2147483648) > (int64_t) lval)) {
111 DBG_INF("stringify");
112 tmp_len = sprintf((char *)&tmp, MYSQLND_LL_SPEC, lval);
113 } else
114 #endif /* SIZEOF */
115 {
116 ZVAL_LONG(zv, (zend_long) lval); /* the cast is safe, we are in the range */
117 }
118 }
119
120 if (tmp_len) {
121 ZVAL_STRINGL(zv, tmp, tmp_len);
122 }
123 (*row)+= byte_count;
124 DBG_VOID_RETURN;
125 }
126 /* }}} */
127
128
129 /* {{{ ps_fetch_null */
130 static void
ps_fetch_null(zval * zv,const MYSQLND_FIELD * const field,unsigned int pack_len,zend_uchar ** row)131 ps_fetch_null(zval *zv, const MYSQLND_FIELD * const field, unsigned int pack_len, zend_uchar ** row)
132 {
133 ZVAL_NULL(zv);
134 }
135 /* }}} */
136
137
138 /* {{{ ps_fetch_int8 */
139 static void
ps_fetch_int8(zval * zv,const MYSQLND_FIELD * const field,unsigned int pack_len,zend_uchar ** row)140 ps_fetch_int8(zval * zv, const MYSQLND_FIELD * const field, unsigned int pack_len, zend_uchar ** row)
141 {
142 ps_fetch_from_1_to_8_bytes(zv, field, pack_len, row, 1);
143 }
144 /* }}} */
145
146
147 /* {{{ ps_fetch_int16 */
148 static void
ps_fetch_int16(zval * zv,const MYSQLND_FIELD * const field,unsigned int pack_len,zend_uchar ** row)149 ps_fetch_int16(zval * zv, const MYSQLND_FIELD * const field, unsigned int pack_len, zend_uchar ** row)
150 {
151 ps_fetch_from_1_to_8_bytes(zv, field, pack_len, row, 2);
152 }
153 /* }}} */
154
155
156 /* {{{ ps_fetch_int32 */
157 static void
ps_fetch_int32(zval * zv,const MYSQLND_FIELD * const field,unsigned int pack_len,zend_uchar ** row)158 ps_fetch_int32(zval * zv, const MYSQLND_FIELD * const field, unsigned int pack_len, zend_uchar ** row)
159 {
160 ps_fetch_from_1_to_8_bytes(zv, field, pack_len, row, 4);
161 }
162 /* }}} */
163
164
165 /* {{{ ps_fetch_int64 */
166 static void
ps_fetch_int64(zval * zv,const MYSQLND_FIELD * const field,unsigned int pack_len,zend_uchar ** row)167 ps_fetch_int64(zval * zv, const MYSQLND_FIELD * const field, unsigned int pack_len, zend_uchar ** row)
168 {
169 ps_fetch_from_1_to_8_bytes(zv, field, pack_len, row, 8);
170 }
171 /* }}} */
172
173
174 /* {{{ ps_fetch_float */
175 static void
ps_fetch_float(zval * zv,const MYSQLND_FIELD * const field,unsigned int pack_len,zend_uchar ** row)176 ps_fetch_float(zval * zv, const MYSQLND_FIELD * const field, unsigned int pack_len, zend_uchar ** row)
177 {
178 float fval;
179 double dval;
180 DBG_ENTER("ps_fetch_float");
181 float4get(fval, *row);
182 (*row)+= 4;
183 DBG_INF_FMT("value=%f", fval);
184
185 #ifndef NOT_FIXED_DEC
186 # define NOT_FIXED_DEC 31
187 #endif
188
189 dval = mysql_float_to_double(fval, (field->decimals >= NOT_FIXED_DEC) ? -1 : field->decimals);
190
191 ZVAL_DOUBLE(zv, dval);
192 DBG_VOID_RETURN;
193 }
194 /* }}} */
195
196
197 /* {{{ ps_fetch_double */
198 static void
ps_fetch_double(zval * zv,const MYSQLND_FIELD * const field,unsigned int pack_len,zend_uchar ** row)199 ps_fetch_double(zval * zv, const MYSQLND_FIELD * const field, unsigned int pack_len, zend_uchar ** row)
200 {
201 double value;
202 DBG_ENTER("ps_fetch_double");
203 float8get(value, *row);
204 ZVAL_DOUBLE(zv, value);
205 (*row)+= 8;
206 DBG_INF_FMT("value=%f", value);
207 DBG_VOID_RETURN;
208 }
209 /* }}} */
210
211
212 /* {{{ ps_fetch_time */
213 static void
ps_fetch_time(zval * zv,const MYSQLND_FIELD * const field,unsigned int pack_len,zend_uchar ** row)214 ps_fetch_time(zval * zv, const MYSQLND_FIELD * const field, unsigned int pack_len, zend_uchar ** row)
215 {
216 struct st_mysqlnd_time t;
217 zend_ulong length; /* First byte encodes the length*/
218 char * value;
219 DBG_ENTER("ps_fetch_time");
220
221 if ((length = php_mysqlnd_net_field_length(row))) {
222 zend_uchar * to= *row;
223
224 t.time_type = MYSQLND_TIMESTAMP_TIME;
225 t.neg = (zend_bool) to[0];
226
227 t.day = (zend_ulong) sint4korr(to+1);
228 t.hour = (unsigned int) to[5];
229 t.minute = (unsigned int) to[6];
230 t.second = (unsigned int) to[7];
231 t.second_part = (length > 8) ? (zend_ulong) sint4korr(to+8) : 0;
232 t.year = t.month= 0;
233 if (t.day) {
234 /* Convert days to hours at once */
235 t.hour += t.day*24;
236 t.day = 0;
237 }
238
239 (*row) += length;
240 } else {
241 memset(&t, 0, sizeof(t));
242 t.time_type = MYSQLND_TIMESTAMP_TIME;
243 }
244
245 length = mnd_sprintf(&value, 0, "%s%02u:%02u:%02u", (t.neg ? "-" : ""), t.hour, t.minute, t.second);
246
247 DBG_INF_FMT("%s", value);
248 ZVAL_STRINGL(zv, value, length);
249 mnd_sprintf_free(value);
250 DBG_VOID_RETURN;
251 }
252 /* }}} */
253
254
255 /* {{{ ps_fetch_date */
256 static void
ps_fetch_date(zval * zv,const MYSQLND_FIELD * const field,unsigned int pack_len,zend_uchar ** row)257 ps_fetch_date(zval * zv, const MYSQLND_FIELD * const field, unsigned int pack_len, zend_uchar ** row)
258 {
259 struct st_mysqlnd_time t = {0};
260 zend_ulong length; /* First byte encodes the length*/
261 char * value;
262 DBG_ENTER("ps_fetch_date");
263
264 if ((length = php_mysqlnd_net_field_length(row))) {
265 zend_uchar *to= *row;
266
267 t.time_type= MYSQLND_TIMESTAMP_DATE;
268 t.neg= 0;
269
270 t.second_part = t.hour = t.minute = t.second = 0;
271
272 t.year = (unsigned int) sint2korr(to);
273 t.month = (unsigned int) to[2];
274 t.day = (unsigned int) to[3];
275
276 (*row)+= length;
277 } else {
278 memset(&t, 0, sizeof(t));
279 t.time_type = MYSQLND_TIMESTAMP_DATE;
280 }
281
282 length = mnd_sprintf(&value, 0, "%04u-%02u-%02u", t.year, t.month, t.day);
283
284 DBG_INF_FMT("%s", value);
285 ZVAL_STRINGL(zv, value, length);
286 mnd_sprintf_free(value);
287 DBG_VOID_RETURN;
288 }
289 /* }}} */
290
291
292 /* {{{ ps_fetch_datetime */
293 static void
ps_fetch_datetime(zval * zv,const MYSQLND_FIELD * const field,unsigned int pack_len,zend_uchar ** row)294 ps_fetch_datetime(zval * zv, const MYSQLND_FIELD * const field, unsigned int pack_len, zend_uchar ** row)
295 {
296 struct st_mysqlnd_time t;
297 zend_ulong length; /* First byte encodes the length*/
298 char * value;
299 DBG_ENTER("ps_fetch_datetime");
300
301 if ((length = php_mysqlnd_net_field_length(row))) {
302 zend_uchar * to = *row;
303
304 t.time_type = MYSQLND_TIMESTAMP_DATETIME;
305 t.neg = 0;
306
307 t.year = (unsigned int) sint2korr(to);
308 t.month = (unsigned int) to[2];
309 t.day = (unsigned int) to[3];
310
311 if (length > 4) {
312 t.hour = (unsigned int) to[4];
313 t.minute = (unsigned int) to[5];
314 t.second = (unsigned int) to[6];
315 } else {
316 t.hour = t.minute = t.second= 0;
317 }
318 t.second_part = (length > 7) ? (zend_ulong) sint4korr(to+7) : 0;
319
320 (*row)+= length;
321 } else {
322 memset(&t, 0, sizeof(t));
323 t.time_type = MYSQLND_TIMESTAMP_DATETIME;
324 }
325
326 length = mnd_sprintf(&value, 0, "%04u-%02u-%02u %02u:%02u:%02u", t.year, t.month, t.day, t.hour, t.minute, t.second);
327
328 DBG_INF_FMT("%s", value);
329 ZVAL_STRINGL(zv, value, length);
330 mnd_sprintf_free(value);
331 DBG_VOID_RETURN;
332 }
333 /* }}} */
334
335
336 /* {{{ ps_fetch_string */
337 static void
ps_fetch_string(zval * zv,const MYSQLND_FIELD * const field,unsigned int pack_len,zend_uchar ** row)338 ps_fetch_string(zval * zv, const MYSQLND_FIELD * const field, unsigned int pack_len, zend_uchar ** row)
339 {
340 /*
341 For now just copy, before we make it possible
342 to write \0 to the row buffer
343 */
344 const zend_ulong length = php_mysqlnd_net_field_length(row);
345 DBG_ENTER("ps_fetch_string");
346 DBG_INF_FMT("len = %lu", length);
347 DBG_INF("copying from the row buffer");
348 ZVAL_STRINGL(zv, (char *)*row, length);
349
350 (*row) += length;
351 DBG_VOID_RETURN;
352 }
353 /* }}} */
354
355
356 /* {{{ ps_fetch_bit */
357 static void
ps_fetch_bit(zval * zv,const MYSQLND_FIELD * const field,unsigned int pack_len,zend_uchar ** row)358 ps_fetch_bit(zval * zv, const MYSQLND_FIELD * const field, unsigned int pack_len, zend_uchar ** row)
359 {
360 zend_ulong length = php_mysqlnd_net_field_length(row);
361 ps_fetch_from_1_to_8_bytes(zv, field, pack_len, row, length);
362 }
363 /* }}} */
364
365
366 /* {{{ _mysqlnd_init_ps_fetch_subsystem */
_mysqlnd_init_ps_fetch_subsystem()367 void _mysqlnd_init_ps_fetch_subsystem()
368 {
369 memset(mysqlnd_ps_fetch_functions, 0, sizeof(mysqlnd_ps_fetch_functions));
370 mysqlnd_ps_fetch_functions[MYSQL_TYPE_NULL].func = ps_fetch_null;
371 mysqlnd_ps_fetch_functions[MYSQL_TYPE_NULL].pack_len = 0;
372 mysqlnd_ps_fetch_functions[MYSQL_TYPE_NULL].php_type = IS_NULL;
373 mysqlnd_ps_fetch_functions[MYSQL_TYPE_NULL].can_ret_as_str_in_uni = TRUE;
374
375 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TINY].func = ps_fetch_int8;
376 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TINY].pack_len = 1;
377 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TINY].php_type = IS_LONG;
378 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TINY].can_ret_as_str_in_uni = TRUE;
379
380 mysqlnd_ps_fetch_functions[MYSQL_TYPE_SHORT].func = ps_fetch_int16;
381 mysqlnd_ps_fetch_functions[MYSQL_TYPE_SHORT].pack_len = 2;
382 mysqlnd_ps_fetch_functions[MYSQL_TYPE_SHORT].php_type = IS_LONG;
383 mysqlnd_ps_fetch_functions[MYSQL_TYPE_SHORT].can_ret_as_str_in_uni = TRUE;
384
385 mysqlnd_ps_fetch_functions[MYSQL_TYPE_YEAR].func = ps_fetch_int16;
386 mysqlnd_ps_fetch_functions[MYSQL_TYPE_YEAR].pack_len = 2;
387 mysqlnd_ps_fetch_functions[MYSQL_TYPE_YEAR].php_type = IS_LONG;
388 mysqlnd_ps_fetch_functions[MYSQL_TYPE_YEAR].can_ret_as_str_in_uni = TRUE;
389
390 mysqlnd_ps_fetch_functions[MYSQL_TYPE_INT24].func = ps_fetch_int32;
391 mysqlnd_ps_fetch_functions[MYSQL_TYPE_INT24].pack_len = 4;
392 mysqlnd_ps_fetch_functions[MYSQL_TYPE_INT24].php_type = IS_LONG;
393 mysqlnd_ps_fetch_functions[MYSQL_TYPE_INT24].can_ret_as_str_in_uni = TRUE;
394
395 mysqlnd_ps_fetch_functions[MYSQL_TYPE_LONG].func = ps_fetch_int32;
396 mysqlnd_ps_fetch_functions[MYSQL_TYPE_LONG].pack_len = 4;
397 mysqlnd_ps_fetch_functions[MYSQL_TYPE_LONG].php_type = IS_LONG;
398 mysqlnd_ps_fetch_functions[MYSQL_TYPE_LONG].can_ret_as_str_in_uni = TRUE;
399
400 mysqlnd_ps_fetch_functions[MYSQL_TYPE_LONGLONG].func = ps_fetch_int64;
401 mysqlnd_ps_fetch_functions[MYSQL_TYPE_LONGLONG].pack_len= 8;
402 mysqlnd_ps_fetch_functions[MYSQL_TYPE_LONGLONG].php_type= IS_LONG;
403 mysqlnd_ps_fetch_functions[MYSQL_TYPE_LONGLONG].can_ret_as_str_in_uni = TRUE;
404
405 mysqlnd_ps_fetch_functions[MYSQL_TYPE_FLOAT].func = ps_fetch_float;
406 mysqlnd_ps_fetch_functions[MYSQL_TYPE_FLOAT].pack_len = 4;
407 mysqlnd_ps_fetch_functions[MYSQL_TYPE_FLOAT].php_type = IS_DOUBLE;
408 mysqlnd_ps_fetch_functions[MYSQL_TYPE_FLOAT].can_ret_as_str_in_uni = TRUE;
409
410 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DOUBLE].func = ps_fetch_double;
411 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DOUBLE].pack_len = 8;
412 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DOUBLE].php_type = IS_DOUBLE;
413 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DOUBLE].can_ret_as_str_in_uni = TRUE;
414
415 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TIME].func = ps_fetch_time;
416 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TIME].pack_len = MYSQLND_PS_SKIP_RESULT_W_LEN;
417 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TIME].php_type = IS_STRING;
418 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TIME].can_ret_as_str_in_uni = TRUE;
419
420 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DATE].func = ps_fetch_date;
421 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DATE].pack_len = MYSQLND_PS_SKIP_RESULT_W_LEN;
422 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DATE].php_type = IS_STRING;
423 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DATE].can_ret_as_str_in_uni = TRUE;
424
425 mysqlnd_ps_fetch_functions[MYSQL_TYPE_NEWDATE].func = ps_fetch_string;
426 mysqlnd_ps_fetch_functions[MYSQL_TYPE_NEWDATE].pack_len = MYSQLND_PS_SKIP_RESULT_W_LEN;
427 mysqlnd_ps_fetch_functions[MYSQL_TYPE_NEWDATE].php_type = IS_STRING;
428 mysqlnd_ps_fetch_functions[MYSQL_TYPE_NEWDATE].can_ret_as_str_in_uni = TRUE;
429
430 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DATETIME].func = ps_fetch_datetime;
431 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DATETIME].pack_len= MYSQLND_PS_SKIP_RESULT_W_LEN;
432 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DATETIME].php_type= IS_STRING;
433 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DATETIME].can_ret_as_str_in_uni = TRUE;
434
435 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TIMESTAMP].func = ps_fetch_datetime;
436 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TIMESTAMP].pack_len= MYSQLND_PS_SKIP_RESULT_W_LEN;
437 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TIMESTAMP].php_type= IS_STRING;
438 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TIMESTAMP].can_ret_as_str_in_uni = TRUE;
439
440 mysqlnd_ps_fetch_functions[MYSQL_TYPE_JSON].func = ps_fetch_string;
441 mysqlnd_ps_fetch_functions[MYSQL_TYPE_JSON].pack_len= MYSQLND_PS_SKIP_RESULT_STR;
442 mysqlnd_ps_fetch_functions[MYSQL_TYPE_JSON].php_type = IS_STRING;
443 mysqlnd_ps_fetch_functions[MYSQL_TYPE_JSON].is_possibly_blob = TRUE;
444 mysqlnd_ps_fetch_functions[MYSQL_TYPE_JSON].can_ret_as_str_in_uni = TRUE;
445
446 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TINY_BLOB].func = ps_fetch_string;
447 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TINY_BLOB].pack_len= MYSQLND_PS_SKIP_RESULT_STR;
448 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TINY_BLOB].php_type = IS_STRING;
449 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TINY_BLOB].is_possibly_blob = TRUE;
450 mysqlnd_ps_fetch_functions[MYSQL_TYPE_TINY_BLOB].can_ret_as_str_in_uni = TRUE;
451
452 mysqlnd_ps_fetch_functions[MYSQL_TYPE_BLOB].func = ps_fetch_string;
453 mysqlnd_ps_fetch_functions[MYSQL_TYPE_BLOB].pack_len = MYSQLND_PS_SKIP_RESULT_STR;
454 mysqlnd_ps_fetch_functions[MYSQL_TYPE_BLOB].php_type = IS_STRING;
455 mysqlnd_ps_fetch_functions[MYSQL_TYPE_BLOB].is_possibly_blob = TRUE;
456 mysqlnd_ps_fetch_functions[MYSQL_TYPE_BLOB].can_ret_as_str_in_uni = TRUE;
457
458 mysqlnd_ps_fetch_functions[MYSQL_TYPE_MEDIUM_BLOB].func = ps_fetch_string;
459 mysqlnd_ps_fetch_functions[MYSQL_TYPE_MEDIUM_BLOB].pack_len = MYSQLND_PS_SKIP_RESULT_STR;
460 mysqlnd_ps_fetch_functions[MYSQL_TYPE_MEDIUM_BLOB].php_type = IS_STRING;
461 mysqlnd_ps_fetch_functions[MYSQL_TYPE_MEDIUM_BLOB].is_possibly_blob = TRUE;
462 mysqlnd_ps_fetch_functions[MYSQL_TYPE_MEDIUM_BLOB].can_ret_as_str_in_uni = TRUE;
463
464 mysqlnd_ps_fetch_functions[MYSQL_TYPE_LONG_BLOB].func = ps_fetch_string;
465 mysqlnd_ps_fetch_functions[MYSQL_TYPE_LONG_BLOB].pack_len = MYSQLND_PS_SKIP_RESULT_STR;
466 mysqlnd_ps_fetch_functions[MYSQL_TYPE_LONG_BLOB].php_type = IS_STRING;
467 mysqlnd_ps_fetch_functions[MYSQL_TYPE_LONG_BLOB].is_possibly_blob = TRUE;
468 mysqlnd_ps_fetch_functions[MYSQL_TYPE_LONG_BLOB].can_ret_as_str_in_uni = TRUE;
469
470 mysqlnd_ps_fetch_functions[MYSQL_TYPE_BIT].func = ps_fetch_bit;
471 mysqlnd_ps_fetch_functions[MYSQL_TYPE_BIT].pack_len = 8;
472 mysqlnd_ps_fetch_functions[MYSQL_TYPE_BIT].php_type = IS_LONG;
473 mysqlnd_ps_fetch_functions[MYSQL_TYPE_BIT].can_ret_as_str_in_uni = TRUE;
474
475 mysqlnd_ps_fetch_functions[MYSQL_TYPE_VAR_STRING].func = ps_fetch_string;
476 mysqlnd_ps_fetch_functions[MYSQL_TYPE_VAR_STRING].pack_len = MYSQLND_PS_SKIP_RESULT_STR;
477 mysqlnd_ps_fetch_functions[MYSQL_TYPE_VAR_STRING].php_type = IS_STRING;
478 mysqlnd_ps_fetch_functions[MYSQL_TYPE_VAR_STRING].is_possibly_blob = TRUE;
479
480 mysqlnd_ps_fetch_functions[MYSQL_TYPE_VARCHAR].func = ps_fetch_string;
481 mysqlnd_ps_fetch_functions[MYSQL_TYPE_VARCHAR].pack_len = MYSQLND_PS_SKIP_RESULT_STR;
482 mysqlnd_ps_fetch_functions[MYSQL_TYPE_VARCHAR].php_type = IS_STRING;
483 mysqlnd_ps_fetch_functions[MYSQL_TYPE_VARCHAR].is_possibly_blob = TRUE;
484
485 mysqlnd_ps_fetch_functions[MYSQL_TYPE_STRING].func = ps_fetch_string;
486 mysqlnd_ps_fetch_functions[MYSQL_TYPE_STRING].pack_len = MYSQLND_PS_SKIP_RESULT_STR;
487 mysqlnd_ps_fetch_functions[MYSQL_TYPE_STRING].php_type = IS_STRING;
488 mysqlnd_ps_fetch_functions[MYSQL_TYPE_STRING].is_possibly_blob = TRUE;
489
490 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DECIMAL].func = ps_fetch_string;
491 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DECIMAL].pack_len = MYSQLND_PS_SKIP_RESULT_STR;
492 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DECIMAL].php_type = IS_STRING;
493 mysqlnd_ps_fetch_functions[MYSQL_TYPE_DECIMAL].can_ret_as_str_in_uni = TRUE;
494
495 mysqlnd_ps_fetch_functions[MYSQL_TYPE_NEWDECIMAL].func = ps_fetch_string;
496 mysqlnd_ps_fetch_functions[MYSQL_TYPE_NEWDECIMAL].pack_len = MYSQLND_PS_SKIP_RESULT_STR;
497 mysqlnd_ps_fetch_functions[MYSQL_TYPE_NEWDECIMAL].php_type = IS_STRING;
498 mysqlnd_ps_fetch_functions[MYSQL_TYPE_NEWDECIMAL].can_ret_as_str_in_uni = TRUE;
499
500 mysqlnd_ps_fetch_functions[MYSQL_TYPE_ENUM].func = ps_fetch_string;
501 mysqlnd_ps_fetch_functions[MYSQL_TYPE_ENUM].pack_len = MYSQLND_PS_SKIP_RESULT_STR;
502 mysqlnd_ps_fetch_functions[MYSQL_TYPE_ENUM].php_type = IS_STRING;
503
504 mysqlnd_ps_fetch_functions[MYSQL_TYPE_SET].func = ps_fetch_string;
505 mysqlnd_ps_fetch_functions[MYSQL_TYPE_SET].pack_len = MYSQLND_PS_SKIP_RESULT_STR;
506 mysqlnd_ps_fetch_functions[MYSQL_TYPE_SET].php_type = IS_STRING;
507
508 mysqlnd_ps_fetch_functions[MYSQL_TYPE_GEOMETRY].func = ps_fetch_string;
509 mysqlnd_ps_fetch_functions[MYSQL_TYPE_GEOMETRY].pack_len= MYSQLND_PS_SKIP_RESULT_STR;
510 mysqlnd_ps_fetch_functions[MYSQL_TYPE_GEOMETRY].php_type= IS_STRING;
511 }
512 /* }}} */
513
514
515 /* {{{ mysqlnd_stmt_copy_it */
516 static enum_func_status
mysqlnd_stmt_copy_it(zval ** copies,zval * original,unsigned int param_count,unsigned int current)517 mysqlnd_stmt_copy_it(zval ** copies, zval * original, unsigned int param_count, unsigned int current)
518 {
519 if (!*copies) {
520 *copies = mnd_ecalloc(param_count, sizeof(zval));
521 }
522 if (*copies) {
523 ZVAL_COPY(&(*copies)[current], original);
524 return PASS;
525 }
526 return FAIL;
527 }
528 /* }}} */
529
530
531 /* {{{ mysqlnd_stmt_free_copies */
532 static void
mysqlnd_stmt_free_copies(MYSQLND_STMT_DATA * stmt,zval * copies)533 mysqlnd_stmt_free_copies(MYSQLND_STMT_DATA * stmt, zval *copies)
534 {
535 if (copies) {
536 unsigned int i;
537 for (i = 0; i < stmt->param_count; i++) {
538 zval_ptr_dtor(&copies[i]);
539 }
540 mnd_efree(copies);
541 }
542 }
543 /* }}} */
544
545
546 /* {{{ mysqlnd_stmt_execute_check_n_enlarge_buffer */
547 static enum_func_status
mysqlnd_stmt_execute_check_n_enlarge_buffer(zend_uchar ** buf,zend_uchar ** p,size_t * buf_len,zend_uchar * const provided_buffer,size_t needed_bytes)548 mysqlnd_stmt_execute_check_n_enlarge_buffer(zend_uchar **buf, zend_uchar **p, size_t * buf_len, zend_uchar * const provided_buffer, size_t needed_bytes)
549 {
550 const size_t overalloc = 5;
551 size_t left = (*buf_len - (*p - *buf));
552
553 if (left < (needed_bytes + overalloc)) {
554 size_t offset = *p - *buf;
555 zend_uchar *tmp_buf;
556 *buf_len = offset + needed_bytes + overalloc;
557 tmp_buf = mnd_emalloc(*buf_len);
558 if (!tmp_buf) {
559 return FAIL;
560 }
561 memcpy(tmp_buf, *buf, offset);
562 if (*buf != provided_buffer) {
563 mnd_efree(*buf);
564 }
565 *buf = tmp_buf;
566 /* Update our pos pointer */
567 *p = *buf + offset;
568 }
569 return PASS;
570 }
571 /* }}} */
572
573
574 /* {{{ mysqlnd_stmt_execute_prepare_param_types */
575 static enum_func_status
mysqlnd_stmt_execute_prepare_param_types(MYSQLND_STMT_DATA * stmt,zval ** copies_param,int * resend_types_next_time)576 mysqlnd_stmt_execute_prepare_param_types(MYSQLND_STMT_DATA * stmt, zval ** copies_param, int * resend_types_next_time)
577 {
578 unsigned int i;
579 DBG_ENTER("mysqlnd_stmt_execute_prepare_param_types");
580 for (i = 0; i < stmt->param_count; i++) {
581 short current_type = stmt->param_bind[i].type;
582 zval *parameter = &stmt->param_bind[i].zv;
583
584 ZVAL_DEREF(parameter);
585 if (!Z_ISNULL_P(parameter) && (current_type == MYSQL_TYPE_LONG || current_type == MYSQL_TYPE_LONGLONG)) {
586 /* always copy the var, because we do many conversions */
587 if (Z_TYPE_P(parameter) != IS_LONG &&
588 PASS != mysqlnd_stmt_copy_it(copies_param, parameter, stmt->param_count, i))
589 {
590 SET_OOM_ERROR(*stmt->error_info);
591 goto end;
592 }
593 /*
594 if it doesn't fit in a long send it as a string.
595 Check bug #52891 : Wrong data inserted with mysqli/mysqlnd when using bind_param, value > LONG_MAX
596 */
597 if (Z_TYPE_P(parameter) != IS_LONG) {
598 zval *tmp_data = (*copies_param && !Z_ISUNDEF((*copies_param)[i]))? &(*copies_param)[i]: parameter;
599 /*
600 Because converting to double and back to long can lead
601 to losing precision we need second variable. Conversion to double is to see if
602 value is too big for a long. As said, precision could be lost.
603 */
604 zval tmp_data_copy;
605 ZVAL_COPY(&tmp_data_copy, tmp_data);
606 convert_to_double_ex(&tmp_data_copy);
607
608 /*
609 if it doesn't fit in a long send it as a string.
610 Check bug #52891 : Wrong data inserted with mysqli/mysqlnd when using bind_param, value > LONG_MAX
611 We do transformation here, which will be used later when sending types. The code later relies on this.
612 */
613 if (Z_DVAL(tmp_data_copy) > ZEND_LONG_MAX || Z_DVAL(tmp_data_copy) < ZEND_LONG_MIN) {
614 stmt->send_types_to_server = *resend_types_next_time = 1;
615 convert_to_string_ex(tmp_data);
616 } else {
617 convert_to_long_ex(tmp_data);
618 }
619
620 zval_ptr_dtor(&tmp_data_copy);
621 }
622 }
623 }
624 DBG_RETURN(PASS);
625 end:
626 DBG_RETURN(FAIL);
627 }
628 /* }}} */
629
630
631 /* {{{ mysqlnd_stmt_execute_store_types */
632 static void
mysqlnd_stmt_execute_store_types(MYSQLND_STMT_DATA * stmt,zval * copies,zend_uchar ** p)633 mysqlnd_stmt_execute_store_types(MYSQLND_STMT_DATA * stmt, zval * copies, zend_uchar ** p)
634 {
635 unsigned int i;
636 for (i = 0; i < stmt->param_count; i++) {
637 short current_type = stmt->param_bind[i].type;
638 zval *parameter = &stmt->param_bind[i].zv;
639 /* our types are not unsigned */
640 #if SIZEOF_ZEND_LONG==8
641 if (current_type == MYSQL_TYPE_LONG) {
642 current_type = MYSQL_TYPE_LONGLONG;
643 }
644 #endif
645 ZVAL_DEREF(parameter);
646 if (!Z_ISNULL_P(parameter) && (current_type == MYSQL_TYPE_LONG || current_type == MYSQL_TYPE_LONGLONG)) {
647 /*
648 if it doesn't fit in a long send it as a string.
649 Check bug #52891 : Wrong data inserted with mysqli/mysqlnd when using bind_param, value > LONG_MAX
650 */
651 if (Z_TYPE_P(parameter) != IS_LONG) {
652 const zval *tmp_data = (copies && !Z_ISUNDEF(copies[i]))? &copies[i] : parameter;
653 /*
654 In case of IS_LONG we do nothing, it is ok, in case of string, we just need to set current_type.
655 The actual transformation has been performed several dozens line above.
656 */
657 if (Z_TYPE_P(tmp_data) == IS_STRING) {
658 current_type = MYSQL_TYPE_VAR_STRING;
659 /*
660 don't change stmt->param_bind[i].type to MYSQL_TYPE_VAR_STRING
661 we force convert_to_long_ex in all cases, thus the type will be right in the next switch.
662 if the type is however not long, then we will do a goto in the next switch.
663 We want to preserve the original bind type given by the user. Thus, we do these hacks.
664 */
665 }
666 }
667 }
668 int2store(*p, current_type);
669 *p+= 2;
670 }
671 }
672 /* }}} */
673
674
675 /* {{{ mysqlnd_stmt_execute_calculate_param_values_size */
676 static enum_func_status
mysqlnd_stmt_execute_calculate_param_values_size(MYSQLND_STMT_DATA * stmt,zval ** copies_param,size_t * data_size)677 mysqlnd_stmt_execute_calculate_param_values_size(MYSQLND_STMT_DATA * stmt, zval ** copies_param, size_t * data_size)
678 {
679 unsigned int i;
680 DBG_ENTER("mysqlnd_stmt_execute_calculate_param_values_size");
681 for (i = 0; i < stmt->param_count; i++) {
682 unsigned short is_longlong = 0;
683 unsigned int j;
684 zval *bind_var, *the_var = &stmt->param_bind[i].zv;
685
686 bind_var = the_var;
687 ZVAL_DEREF(the_var);
688 if ((stmt->param_bind[i].type != MYSQL_TYPE_LONG_BLOB && Z_TYPE_P(the_var) == IS_NULL)) {
689 continue;
690 }
691
692 if (Z_ISREF_P(bind_var)) {
693 for (j = i + 1; j < stmt->param_count; j++) {
694 if (Z_ISREF(stmt->param_bind[j].zv) && Z_REFVAL(stmt->param_bind[j].zv) == the_var) {
695 /* Double binding of the same zval, make a copy */
696 if (!*copies_param || Z_ISUNDEF((*copies_param)[i])) {
697 if (PASS != mysqlnd_stmt_copy_it(copies_param, the_var, stmt->param_count, i)) {
698 SET_OOM_ERROR(*stmt->error_info);
699 goto end;
700 }
701 }
702 break;
703 }
704 }
705 }
706
707 switch (stmt->param_bind[i].type) {
708 case MYSQL_TYPE_DOUBLE:
709 *data_size += 8;
710 if (Z_TYPE_P(the_var) != IS_DOUBLE) {
711 if (!*copies_param || Z_ISUNDEF((*copies_param)[i])) {
712 if (PASS != mysqlnd_stmt_copy_it(copies_param, the_var, stmt->param_count, i)) {
713 SET_OOM_ERROR(*stmt->error_info);
714 goto end;
715 }
716 }
717 }
718 break;
719 case MYSQL_TYPE_LONGLONG:
720 is_longlong = 4;
721 /* fall-through */
722 case MYSQL_TYPE_LONG:
723 {
724 zval *tmp_data = (*copies_param && !Z_ISUNDEF((*copies_param)[i]))? &(*copies_param)[i]: the_var;
725 if (Z_TYPE_P(tmp_data) == IS_STRING) {
726 goto use_string;
727 }
728 convert_to_long_ex(tmp_data);
729 }
730 *data_size += 4 + is_longlong;
731 break;
732 case MYSQL_TYPE_LONG_BLOB:
733 if (!(stmt->param_bind[i].flags & MYSQLND_PARAM_BIND_BLOB_USED)) {
734 /*
735 User hasn't sent anything, we will send empty string.
736 Empty string has length of 0, encoded in 1 byte. No real
737 data will follows after it.
738 */
739 (*data_size)++;
740 }
741 break;
742 case MYSQL_TYPE_VAR_STRING:
743 use_string:
744 *data_size += 8; /* max 8 bytes for size */
745 if (Z_TYPE_P(the_var) != IS_STRING) {
746 if (!*copies_param || Z_ISUNDEF((*copies_param)[i])) {
747 if (PASS != mysqlnd_stmt_copy_it(copies_param, the_var, stmt->param_count, i)) {
748 SET_OOM_ERROR(*stmt->error_info);
749 goto end;
750 }
751 }
752 the_var = &((*copies_param)[i]);
753 }
754 convert_to_string_ex(the_var);
755 *data_size += Z_STRLEN_P(the_var);
756 break;
757 }
758 }
759 DBG_RETURN(PASS);
760 end:
761 DBG_RETURN(FAIL);
762 }
763 /* }}} */
764
765
766 /* {{{ mysqlnd_stmt_execute_store_param_values */
767 static void
mysqlnd_stmt_execute_store_param_values(MYSQLND_STMT_DATA * stmt,zval * copies,zend_uchar * buf,zend_uchar ** p,size_t null_byte_offset)768 mysqlnd_stmt_execute_store_param_values(MYSQLND_STMT_DATA * stmt, zval * copies, zend_uchar * buf, zend_uchar ** p, size_t null_byte_offset)
769 {
770 unsigned int i;
771 for (i = 0; i < stmt->param_count; i++) {
772 zval *data, *parameter = &stmt->param_bind[i].zv;
773
774 ZVAL_DEREF(parameter);
775 data = (copies && !Z_ISUNDEF(copies[i]))? &copies[i]: parameter;
776 /* Handle long data */
777 if (!Z_ISUNDEF_P(parameter) && Z_TYPE_P(data) == IS_NULL) {
778 (buf + null_byte_offset)[i/8] |= (zend_uchar) (1 << (i & 7));
779 } else {
780 switch (stmt->param_bind[i].type) {
781 case MYSQL_TYPE_DOUBLE:
782 convert_to_double_ex(data);
783 float8store(*p, Z_DVAL_P(data));
784 (*p) += 8;
785 break;
786 case MYSQL_TYPE_LONGLONG:
787 if (Z_TYPE_P(data) == IS_STRING) {
788 goto send_string;
789 }
790 /* data has alreade been converted to long */
791 int8store(*p, Z_LVAL_P(data));
792 (*p) += 8;
793 break;
794 case MYSQL_TYPE_LONG:
795 if (Z_TYPE_P(data) == IS_STRING) {
796 goto send_string;
797 }
798 /* data has alreade been converted to long */
799 int4store(*p, Z_LVAL_P(data));
800 (*p) += 4;
801 break;
802 case MYSQL_TYPE_LONG_BLOB:
803 if (stmt->param_bind[i].flags & MYSQLND_PARAM_BIND_BLOB_USED) {
804 stmt->param_bind[i].flags &= ~MYSQLND_PARAM_BIND_BLOB_USED;
805 } else {
806 /* send_long_data() not called, send empty string */
807 *p = php_mysqlnd_net_store_length(*p, 0);
808 }
809 break;
810 case MYSQL_TYPE_VAR_STRING:
811 send_string:
812 {
813 size_t len = Z_STRLEN_P(data);
814 /* to is after p. The latter hasn't been moved */
815 *p = php_mysqlnd_net_store_length(*p, len);
816 memcpy(*p, Z_STRVAL_P(data), len);
817 (*p) += len;
818 }
819 break;
820 default:
821 /* Won't happen, but set to NULL */
822 (buf + null_byte_offset)[i/8] |= (zend_uchar) (1 << (i & 7));
823 break;
824 }
825 }
826 }
827 }
828 /* }}} */
829
830
831 /* {{{ mysqlnd_stmt_execute_store_params */
832 static enum_func_status
mysqlnd_stmt_execute_store_params(MYSQLND_STMT * s,zend_uchar ** buf,zend_uchar ** p,size_t * buf_len)833 mysqlnd_stmt_execute_store_params(MYSQLND_STMT * s, zend_uchar **buf, zend_uchar **p, size_t *buf_len )
834 {
835 MYSQLND_STMT_DATA * stmt = s->data;
836 zend_uchar * provided_buffer = *buf;
837 size_t data_size = 0;
838 zval *copies = NULL;/* if there are different types */
839 enum_func_status ret = FAIL;
840 int resend_types_next_time = 0;
841 size_t null_byte_offset;
842
843 DBG_ENTER("mysqlnd_stmt_execute_store_params");
844
845 {
846 unsigned int null_count = (stmt->param_count + 7) / 8;
847 if (FAIL == mysqlnd_stmt_execute_check_n_enlarge_buffer(buf, p, buf_len, provided_buffer, null_count)) {
848 SET_OOM_ERROR(*stmt->error_info);
849 goto end;
850 }
851 /* put `null` bytes */
852 null_byte_offset = *p - *buf;
853 memset(*p, 0, null_count);
854 *p += null_count;
855 }
856
857 /* 1. Store type information */
858 /*
859 check if need to send the types even if stmt->send_types_to_server is 0. This is because
860 if we send "i" (42) then the type will be int and the server will expect int. However, if next
861 time we try to send > LONG_MAX, the conversion to string will send a string and the server
862 won't expect it and interpret the value as 0. Thus we need to resend the types, if any such values
863 occur, and force resend for the next execution.
864 */
865 if (FAIL == mysqlnd_stmt_execute_prepare_param_types(stmt, &copies, &resend_types_next_time)) {
866 goto end;
867 }
868
869 int1store(*p, stmt->send_types_to_server);
870 (*p)++;
871
872 if (stmt->send_types_to_server) {
873 if (FAIL == mysqlnd_stmt_execute_check_n_enlarge_buffer(buf, p, buf_len, provided_buffer, stmt->param_count * 2)) {
874 SET_OOM_ERROR(*stmt->error_info);
875 goto end;
876 }
877 mysqlnd_stmt_execute_store_types(stmt, copies, p);
878 }
879
880 stmt->send_types_to_server = resend_types_next_time;
881
882 /* 2. Store data */
883 /* 2.1 Calculate how much space we need */
884 if (FAIL == mysqlnd_stmt_execute_calculate_param_values_size(stmt, &copies, &data_size)) {
885 goto end;
886 }
887
888 /* 2.2 Enlarge the buffer, if needed */
889 if (FAIL == mysqlnd_stmt_execute_check_n_enlarge_buffer(buf, p, buf_len, provided_buffer, data_size)) {
890 SET_OOM_ERROR(*stmt->error_info);
891 goto end;
892 }
893
894 /* 2.3 Store the actual data */
895 mysqlnd_stmt_execute_store_param_values(stmt, copies, *buf, p, null_byte_offset);
896
897 ret = PASS;
898 end:
899 mysqlnd_stmt_free_copies(stmt, copies);
900
901 DBG_INF_FMT("ret=%s", ret == PASS? "PASS":"FAIL");
902 DBG_RETURN(ret);
903 }
904 /* }}} */
905
906
907 /* {{{ mysqlnd_stmt_execute_generate_request */
908 enum_func_status
mysqlnd_stmt_execute_generate_request(MYSQLND_STMT * const s,zend_uchar ** request,size_t * request_len,zend_bool * free_buffer)909 mysqlnd_stmt_execute_generate_request(MYSQLND_STMT * const s, zend_uchar ** request, size_t *request_len, zend_bool * free_buffer)
910 {
911 MYSQLND_STMT_DATA * stmt = s->data;
912 zend_uchar *p = stmt->execute_cmd_buffer.buffer,
913 *cmd_buffer = stmt->execute_cmd_buffer.buffer;
914 size_t cmd_buffer_length = stmt->execute_cmd_buffer.length;
915 enum_func_status ret;
916
917 DBG_ENTER("mysqlnd_stmt_execute_generate_request");
918
919 int4store(p, stmt->stmt_id);
920 p += 4;
921
922 /* flags is 4 bytes, we store just 1 */
923 int1store(p, (zend_uchar) stmt->flags);
924 p++;
925
926 /* Make it all zero */
927 int4store(p, 0);
928
929 int1store(p, 1); /* and send 1 for iteration count */
930 p+= 4;
931
932 ret = mysqlnd_stmt_execute_store_params(s, &cmd_buffer, &p, &cmd_buffer_length);
933
934 *free_buffer = (cmd_buffer != stmt->execute_cmd_buffer.buffer);
935 *request_len = (p - cmd_buffer);
936 *request = cmd_buffer;
937 DBG_INF_FMT("ret=%s", ret == PASS? "PASS":"FAIL");
938 DBG_RETURN(ret);
939 }
940 /* }}} */
941
942 /*
943 * Local variables:
944 * tab-width: 4
945 * c-basic-offset: 4
946 * End:
947 * vim600: noet sw=4 ts=4 fdm=marker
948 * vim<600: noet sw=4 ts=4
949 */
950