1 /*
2 +----------------------------------------------------------------------+
3 | PHP Version 5 |
4 +----------------------------------------------------------------------+
5 | Copyright (c) 2006-2015 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@mysql.com> |
16 | Ulf Wendel <uwendel@mysql.com> |
17 | Georg Richter <georg@mysql.com> |
18 +----------------------------------------------------------------------+
19 */
20
21 /* $Id$ */
22 #include "php.h"
23 #include "mysqlnd.h"
24 #include "mysqlnd_wireprotocol.h"
25 #include "mysqlnd_block_alloc.h"
26 #include "mysqlnd_priv.h"
27 #include "mysqlnd_result.h"
28 #include "mysqlnd_result_meta.h"
29 #include "mysqlnd_statistics.h"
30 #include "mysqlnd_debug.h"
31 #include "mysqlnd_ext_plugin.h"
32
33 #define MYSQLND_SILENT
34
35
36 /* {{{ mysqlnd_res::initialize_result_set_rest */
37 static enum_func_status
MYSQLND_METHOD(mysqlnd_res,initialize_result_set_rest)38 MYSQLND_METHOD(mysqlnd_res, initialize_result_set_rest)(MYSQLND_RES * const result TSRMLS_DC)
39 {
40 unsigned int i;
41 zval **data_cursor = result->stored_data? result->stored_data->data:NULL;
42 zval **data_begin = result->stored_data? result->stored_data->data:NULL;
43 unsigned int field_count = result->meta? result->meta->field_count : 0;
44 uint64_t row_count = result->stored_data? result->stored_data->row_count:0;
45 enum_func_status ret = PASS;
46 DBG_ENTER("mysqlnd_res::initialize_result_set_rest");
47
48 if (!data_cursor || row_count == result->stored_data->initialized_rows) {
49 DBG_RETURN(ret);
50 }
51 while ((data_cursor - data_begin) < (int)(row_count * field_count)) {
52 if (NULL == data_cursor[0]) {
53 enum_func_status rc = result->m.row_decoder(
54 result->stored_data->row_buffers[(data_cursor - data_begin) / field_count],
55 data_cursor,
56 result->meta->field_count,
57 result->meta->fields,
58 result->conn->options->int_and_float_native,
59 result->conn->stats TSRMLS_CC);
60 if (rc != PASS) {
61 ret = FAIL;
62 break;
63 }
64 result->stored_data->initialized_rows++;
65 for (i = 0; i < result->field_count; i++) {
66 /*
67 NULL fields are 0 length, 0 is not more than 0
68 String of zero size, definitely can't be the next max_length.
69 Thus for NULL and zero-length we are quite efficient.
70 */
71 if (Z_TYPE_P(data_cursor[i]) >= IS_STRING) {
72 unsigned long len = Z_STRLEN_P(data_cursor[i]);
73 if (result->meta->fields[i].max_length < len) {
74 result->meta->fields[i].max_length = len;
75 }
76 }
77 }
78 }
79 data_cursor += field_count;
80 }
81 DBG_RETURN(ret);
82 }
83 /* }}} */
84
85
86 /* {{{ mysqlnd_rset_zval_ptr_dtor */
87 static void
mysqlnd_rset_zval_ptr_dtor(zval ** zv,enum_mysqlnd_res_type type,zend_bool * copy_ctor_called TSRMLS_DC)88 mysqlnd_rset_zval_ptr_dtor(zval **zv, enum_mysqlnd_res_type type, zend_bool * copy_ctor_called TSRMLS_DC)
89 {
90 DBG_ENTER("mysqlnd_rset_zval_ptr_dtor");
91 if (!zv || !*zv) {
92 *copy_ctor_called = FALSE;
93 DBG_ERR_FMT("zv was NULL");
94 DBG_VOID_RETURN;
95 }
96 /*
97 This zval is not from the cache block.
98 Thus the refcount is -1 than of a zval from the cache,
99 because the zvals from the cache are owned by it.
100 */
101 if (type == MYSQLND_RES_PS_BUF || type == MYSQLND_RES_PS_UNBUF) {
102 *copy_ctor_called = FALSE;
103 ; /* do nothing, zval_ptr_dtor will do the job*/
104 } else if (Z_REFCOUNT_PP(zv) > 1) {
105 /*
106 Not a prepared statement, then we have to
107 call copy_ctor and then zval_ptr_dtor()
108 */
109 if (Z_TYPE_PP(zv) == IS_STRING) {
110 zval_copy_ctor(*zv);
111 }
112 *copy_ctor_called = TRUE;
113 } else {
114 /*
115 noone but us point to this, so we can safely ZVAL_NULL the zval,
116 so Zend does not try to free what the zval points to - which is
117 in result set buffers
118 */
119 *copy_ctor_called = FALSE;
120 if (Z_TYPE_PP(zv) == IS_STRING) {
121 ZVAL_NULL(*zv);
122 }
123 }
124 zval_ptr_dtor(zv);
125 DBG_VOID_RETURN;
126 }
127 /* }}} */
128
129
130 /* {{{ mysqlnd_res::unbuffered_free_last_data */
131 static void
MYSQLND_METHOD(mysqlnd_res,unbuffered_free_last_data)132 MYSQLND_METHOD(mysqlnd_res, unbuffered_free_last_data)(MYSQLND_RES * result TSRMLS_DC)
133 {
134 MYSQLND_RES_UNBUFFERED *unbuf = result->unbuf;
135
136 DBG_ENTER("mysqlnd_res::unbuffered_free_last_data");
137
138 if (!unbuf) {
139 DBG_VOID_RETURN;
140 }
141
142 if (unbuf->last_row_data) {
143 unsigned int i, ctor_called_count = 0;
144 zend_bool copy_ctor_called;
145 MYSQLND_STATS *global_stats = result->conn? result->conn->stats:NULL;
146
147 for (i = 0; i < result->field_count; i++) {
148 mysqlnd_rset_zval_ptr_dtor(&(unbuf->last_row_data[i]), result->type, ©_ctor_called TSRMLS_CC);
149 if (copy_ctor_called) {
150 ++ctor_called_count;
151 }
152 }
153 DBG_INF_FMT("copy_ctor_called_count=%u", ctor_called_count);
154 /* By using value3 macros we hold a mutex only once, there is no value2 */
155 MYSQLND_INC_CONN_STATISTIC_W_VALUE2(global_stats,
156 STAT_COPY_ON_WRITE_PERFORMED,
157 ctor_called_count,
158 STAT_COPY_ON_WRITE_SAVED,
159 result->field_count - ctor_called_count);
160 /* Free last row's zvals */
161 mnd_efree(unbuf->last_row_data);
162 unbuf->last_row_data = NULL;
163 }
164 if (unbuf->last_row_buffer) {
165 DBG_INF("Freeing last row buffer");
166 /* Nothing points to this buffer now, free it */
167 unbuf->last_row_buffer->free_chunk(unbuf->last_row_buffer TSRMLS_CC);
168 unbuf->last_row_buffer = NULL;
169 }
170
171 DBG_VOID_RETURN;
172 }
173 /* }}} */
174
175
176 /* {{{ mysqlnd_res::free_buffered_data */
177 static void
MYSQLND_METHOD(mysqlnd_res,free_buffered_data)178 MYSQLND_METHOD(mysqlnd_res, free_buffered_data)(MYSQLND_RES * result TSRMLS_DC)
179 {
180 MYSQLND_RES_BUFFERED *set = result->stored_data;
181 unsigned int field_count = result->field_count;
182 int64_t row;
183
184 DBG_ENTER("mysqlnd_res::free_buffered_data");
185 DBG_INF_FMT("Freeing "MYSQLND_LLU_SPEC" row(s)", set->row_count);
186
187 if (set->data) {
188 unsigned int copy_on_write_performed = 0;
189 unsigned int copy_on_write_saved = 0;
190 zval **data = set->data;
191 set->data = NULL; /* prevent double free if following loop is interrupted */
192
193 for (row = set->row_count - 1; row >= 0; row--) {
194 zval **current_row = data + row * field_count;
195 MYSQLND_MEMORY_POOL_CHUNK *current_buffer = set->row_buffers[row];
196 int64_t col;
197
198 if (current_row != NULL) {
199 for (col = field_count - 1; col >= 0; --col) {
200 if (current_row[col]) {
201 zend_bool copy_ctor_called;
202 mysqlnd_rset_zval_ptr_dtor(&(current_row[col]), result->type, ©_ctor_called TSRMLS_CC);
203 if (copy_ctor_called) {
204 ++copy_on_write_performed;
205 } else {
206 ++copy_on_write_saved;
207 }
208 }
209 }
210 }
211 current_buffer->free_chunk(current_buffer TSRMLS_CC);
212 }
213
214 MYSQLND_INC_GLOBAL_STATISTIC_W_VALUE2(STAT_COPY_ON_WRITE_PERFORMED, copy_on_write_performed,
215 STAT_COPY_ON_WRITE_SAVED, copy_on_write_saved);
216 mnd_efree(data);
217 }
218
219 if (set->row_buffers) {
220 mnd_efree(set->row_buffers);
221 set->row_buffers = NULL;
222 }
223 set->data_cursor = NULL;
224 set->row_count = 0;
225
226 mnd_efree(set);
227
228 DBG_VOID_RETURN;
229 }
230 /* }}} */
231
232
233 /* {{{ mysqlnd_res::free_result_buffers */
234 static void
MYSQLND_METHOD(mysqlnd_res,free_result_buffers)235 MYSQLND_METHOD(mysqlnd_res, free_result_buffers)(MYSQLND_RES * result TSRMLS_DC)
236 {
237 DBG_ENTER("mysqlnd_res::free_result_buffers");
238 DBG_INF_FMT("%s", result->unbuf? "unbuffered":(result->stored_data? "buffered":"unknown"));
239
240 if (result->unbuf) {
241 result->m.unbuffered_free_last_data(result TSRMLS_CC);
242 mnd_efree(result->unbuf);
243 result->unbuf = NULL;
244 } else if (result->stored_data) {
245 result->m.free_buffered_data(result TSRMLS_CC);
246 result->stored_data = NULL;
247 }
248
249 if (result->lengths) {
250 mnd_efree(result->lengths);
251 result->lengths = NULL;
252 }
253
254 if (result->row_packet) {
255 PACKET_FREE(result->row_packet);
256 result->row_packet = NULL;
257 }
258
259 if (result->result_set_memory_pool) {
260 mysqlnd_mempool_destroy(result->result_set_memory_pool TSRMLS_CC);
261 result->result_set_memory_pool = NULL;
262 }
263
264 DBG_VOID_RETURN;
265 }
266 /* }}} */
267
268
269 /* {{{ mysqlnd_internal_free_result_contents */
270 static
mysqlnd_internal_free_result_contents(MYSQLND_RES * result TSRMLS_DC)271 void mysqlnd_internal_free_result_contents(MYSQLND_RES * result TSRMLS_DC)
272 {
273 DBG_ENTER("mysqlnd_internal_free_result_contents");
274
275 result->m.free_result_buffers(result TSRMLS_CC);
276
277 if (result->meta) {
278 result->meta->m->free_metadata(result->meta TSRMLS_CC);
279 result->meta = NULL;
280 }
281
282 DBG_VOID_RETURN;
283 }
284 /* }}} */
285
286
287 /* {{{ mysqlnd_internal_free_result */
288 static
mysqlnd_internal_free_result(MYSQLND_RES * result TSRMLS_DC)289 void mysqlnd_internal_free_result(MYSQLND_RES * result TSRMLS_DC)
290 {
291 DBG_ENTER("mysqlnd_internal_free_result");
292 result->m.free_result_contents(result TSRMLS_CC);
293
294 if (result->conn) {
295 result->conn->m->free_reference(result->conn TSRMLS_CC);
296 result->conn = NULL;
297 }
298
299 mnd_pefree(result, result->persistent);
300
301 DBG_VOID_RETURN;
302 }
303 /* }}} */
304
305
306 /* {{{ mysqlnd_res::read_result_metadata */
307 static enum_func_status
MYSQLND_METHOD(mysqlnd_res,read_result_metadata)308 MYSQLND_METHOD(mysqlnd_res, read_result_metadata)(MYSQLND_RES * result, MYSQLND_CONN_DATA * conn TSRMLS_DC)
309 {
310 DBG_ENTER("mysqlnd_res::read_result_metadata");
311
312 /*
313 Make it safe to call it repeatedly for PS -
314 better free and allocate a new because the number of field might change
315 (select *) with altered table. Also for statements which skip the PS
316 infrastructure!
317 */
318 if (result->meta) {
319 result->meta->m->free_metadata(result->meta TSRMLS_CC);
320 result->meta = NULL;
321 }
322
323 result->meta = result->m.result_meta_init(result->field_count, result->persistent TSRMLS_CC);
324 if (!result->meta) {
325 SET_OOM_ERROR(*conn->error_info);
326 DBG_RETURN(FAIL);
327 }
328
329 /* 1. Read all fields metadata */
330
331 /* It's safe to reread without freeing */
332 if (FAIL == result->meta->m->read_metadata(result->meta, conn TSRMLS_CC)) {
333 result->m.free_result_contents(result TSRMLS_CC);
334 DBG_RETURN(FAIL);
335 }
336 /* COM_FIELD_LIST is broken and has premature EOF, thus we need to hack here and in mysqlnd_res_meta.c */
337 result->field_count = result->meta->field_count;
338
339 /*
340 2. Follows an EOF packet, which the client of mysqlnd_read_result_metadata()
341 should consume.
342 3. If there is a result set, it follows. The last packet will have 'eof' set
343 If PS, then no result set follows.
344 */
345
346 DBG_RETURN(PASS);
347 }
348 /* }}} */
349
350
351 /* {{{ mysqlnd_query_read_result_set_header */
352 enum_func_status
mysqlnd_query_read_result_set_header(MYSQLND_CONN_DATA * conn,MYSQLND_STMT * s TSRMLS_DC)353 mysqlnd_query_read_result_set_header(MYSQLND_CONN_DATA * conn, MYSQLND_STMT * s TSRMLS_DC)
354 {
355 MYSQLND_STMT_DATA * stmt = s ? s->data:NULL;
356 enum_func_status ret;
357 MYSQLND_PACKET_RSET_HEADER * rset_header = NULL;
358 MYSQLND_PACKET_EOF * fields_eof = NULL;
359
360 DBG_ENTER("mysqlnd_query_read_result_set_header");
361 DBG_INF_FMT("stmt=%lu", stmt? stmt->stmt_id:0);
362
363 ret = FAIL;
364 do {
365 rset_header = conn->protocol->m.get_rset_header_packet(conn->protocol, FALSE TSRMLS_CC);
366 if (!rset_header) {
367 SET_OOM_ERROR(*conn->error_info);
368 ret = FAIL;
369 break;
370 }
371
372 SET_ERROR_AFF_ROWS(conn);
373
374 if (FAIL == (ret = PACKET_READ(rset_header, conn))) {
375 php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error reading result set's header");
376 break;
377 }
378
379 if (rset_header->error_info.error_no) {
380 /*
381 Cover a protocol design error: error packet does not
382 contain the server status. Therefore, the client has no way
383 to find out whether there are more result sets of
384 a multiple-result-set statement pending. Luckily, in 5.0 an
385 error always aborts execution of a statement, wherever it is
386 a multi-statement or a stored procedure, so it should be
387 safe to unconditionally turn off the flag here.
388 */
389 conn->upsert_status->server_status &= ~SERVER_MORE_RESULTS_EXISTS;
390 /*
391 This will copy the error code and the messages, as they
392 are buffers in the struct
393 */
394 COPY_CLIENT_ERROR(*conn->error_info, rset_header->error_info);
395 ret = FAIL;
396 DBG_ERR_FMT("error=%s", rset_header->error_info.error);
397 /* Return back from CONN_QUERY_SENT */
398 CONN_SET_STATE(conn, CONN_READY);
399 break;
400 }
401 conn->error_info->error_no = 0;
402
403 switch (rset_header->field_count) {
404 case MYSQLND_NULL_LENGTH: { /* LOAD DATA LOCAL INFILE */
405 zend_bool is_warning;
406 DBG_INF("LOAD DATA");
407 conn->last_query_type = QUERY_LOAD_LOCAL;
408 conn->field_count = 0; /* overwrite previous value, or the last value could be used and lead to bug#53503 */
409 CONN_SET_STATE(conn, CONN_SENDING_LOAD_DATA);
410 ret = mysqlnd_handle_local_infile(conn, rset_header->info_or_local_file, &is_warning TSRMLS_CC);
411 CONN_SET_STATE(conn, (ret == PASS || is_warning == TRUE)? CONN_READY:CONN_QUIT_SENT);
412 MYSQLND_INC_CONN_STATISTIC(conn->stats, STAT_NON_RSET_QUERY);
413 break;
414 }
415 case 0: /* UPSERT */
416 DBG_INF("UPSERT");
417 conn->last_query_type = QUERY_UPSERT;
418 conn->field_count = rset_header->field_count;
419 memset(conn->upsert_status, 0, sizeof(*conn->upsert_status));
420 conn->upsert_status->warning_count = rset_header->warning_count;
421 conn->upsert_status->server_status = rset_header->server_status;
422 conn->upsert_status->affected_rows = rset_header->affected_rows;
423 conn->upsert_status->last_insert_id = rset_header->last_insert_id;
424 SET_NEW_MESSAGE(conn->last_message, conn->last_message_len,
425 rset_header->info_or_local_file, rset_header->info_or_local_file_len,
426 conn->persistent);
427 /* Result set can follow UPSERT statement, check server_status */
428 if (conn->upsert_status->server_status & SERVER_MORE_RESULTS_EXISTS) {
429 CONN_SET_STATE(conn, CONN_NEXT_RESULT_PENDING);
430 } else {
431 CONN_SET_STATE(conn, CONN_READY);
432 }
433 ret = PASS;
434 MYSQLND_INC_CONN_STATISTIC(conn->stats, STAT_NON_RSET_QUERY);
435 break;
436 default: do { /* Result set */
437 MYSQLND_RES * result;
438 enum_mysqlnd_collected_stats statistic = STAT_LAST;
439
440 DBG_INF("Result set pending");
441 SET_EMPTY_MESSAGE(conn->last_message, conn->last_message_len, conn->persistent);
442
443 MYSQLND_INC_CONN_STATISTIC(conn->stats, STAT_RSET_QUERY);
444 memset(conn->upsert_status, 0, sizeof(*conn->upsert_status));
445 /* restore after zeroing */
446 SET_ERROR_AFF_ROWS(conn);
447
448 conn->last_query_type = QUERY_SELECT;
449 CONN_SET_STATE(conn, CONN_FETCHING_DATA);
450 /* PS has already allocated it */
451 conn->field_count = rset_header->field_count;
452 if (!stmt) {
453 result = conn->current_result = conn->m->result_init(rset_header->field_count, conn->persistent TSRMLS_CC);
454 } else {
455 if (!stmt->result) {
456 DBG_INF("This is 'SHOW'/'EXPLAIN'-like query.");
457 /*
458 This is 'SHOW'/'EXPLAIN'-like query. Current implementation of
459 prepared statements can't send result set metadata for these queries
460 on prepare stage. Read it now.
461 */
462 result = stmt->result = conn->m->result_init(rset_header->field_count, stmt->persistent TSRMLS_CC);
463 } else {
464 /*
465 Update result set metadata if it for some reason changed between
466 prepare and execute, i.e.:
467 - in case of 'SELECT ?' we don't know column type unless data was
468 supplied to mysql_stmt_execute, so updated column type is sent
469 now.
470 - if data dictionary changed between prepare and execute, for
471 example a table used in the query was altered.
472 Note, that now (4.1.3) we always send metadata in reply to
473 COM_STMT_EXECUTE (even if it is not necessary), so either this or
474 previous branch always works.
475 */
476 }
477 result = stmt->result;
478 }
479 if (!result) {
480 SET_OOM_ERROR(*conn->error_info);
481 ret = FAIL;
482 break;
483 }
484
485 if (FAIL == (ret = result->m.read_result_metadata(result, conn TSRMLS_CC))) {
486 /* For PS, we leave them in Prepared state */
487 if (!stmt && conn->current_result) {
488 mnd_efree(conn->current_result);
489 conn->current_result = NULL;
490 }
491 DBG_ERR("Error occurred while reading metadata");
492 break;
493 }
494
495 /* Check for SERVER_STATUS_MORE_RESULTS if needed */
496 fields_eof = conn->protocol->m.get_eof_packet(conn->protocol, FALSE TSRMLS_CC);
497 if (!fields_eof) {
498 SET_OOM_ERROR(*conn->error_info);
499 ret = FAIL;
500 break;
501 }
502 if (FAIL == (ret = PACKET_READ(fields_eof, conn))) {
503 DBG_ERR("Error occurred while reading the EOF packet");
504 result->m.free_result_contents(result TSRMLS_CC);
505 mnd_efree(result);
506 if (!stmt) {
507 conn->current_result = NULL;
508 } else {
509 stmt->result = NULL;
510 memset(stmt, 0, sizeof(*stmt));
511 stmt->state = MYSQLND_STMT_INITTED;
512 }
513 } else {
514 unsigned int to_log = MYSQLND_G(log_mask);
515 to_log &= fields_eof->server_status;
516 DBG_INF_FMT("warnings=%u server_status=%u", fields_eof->warning_count, fields_eof->server_status);
517 conn->upsert_status->warning_count = fields_eof->warning_count;
518 /*
519 If SERVER_MORE_RESULTS_EXISTS is set then this is either MULTI_QUERY or a CALL()
520 The first packet after sending the query/com_execute has the bit set only
521 in this cases. Not sure why it's a needed but it marks that the whole stream
522 will include many result sets. What actually matters are the bits set at the end
523 of every result set (the EOF packet).
524 */
525 conn->upsert_status->server_status = fields_eof->server_status;
526 if (fields_eof->server_status & SERVER_QUERY_NO_GOOD_INDEX_USED) {
527 statistic = STAT_BAD_INDEX_USED;
528 } else if (fields_eof->server_status & SERVER_QUERY_NO_INDEX_USED) {
529 statistic = STAT_NO_INDEX_USED;
530 } else if (fields_eof->server_status & SERVER_QUERY_WAS_SLOW) {
531 statistic = STAT_QUERY_WAS_SLOW;
532 }
533 if (to_log) {
534 #if A0
535 char *backtrace = mysqlnd_get_backtrace(TSRMLS_C);
536 php_log_err(backtrace TSRMLS_CC);
537 efree(backtrace);
538 #endif
539 }
540 MYSQLND_INC_CONN_STATISTIC(conn->stats, statistic);
541 }
542 } while (0);
543 PACKET_FREE(fields_eof);
544 break; /* switch break */
545 }
546 } while (0);
547 PACKET_FREE(rset_header);
548
549 DBG_INF(ret == PASS? "PASS":"FAIL");
550 DBG_RETURN(ret);
551 }
552 /* }}} */
553
554
555 /* {{{ mysqlnd_fetch_lengths_buffered */
556 /*
557 Do lazy initialization for buffered results. As PHP strings have
558 length inside, this function makes not much sense in the context
559 of PHP, to be called as separate function. But let's have it for
560 completeness.
561 */
562 static unsigned long *
mysqlnd_fetch_lengths_buffered(MYSQLND_RES * const result TSRMLS_DC)563 mysqlnd_fetch_lengths_buffered(MYSQLND_RES * const result TSRMLS_DC)
564 {
565 unsigned int i;
566 zval **previous_row;
567 MYSQLND_RES_BUFFERED *set = result->stored_data;
568
569 /*
570 If:
571 - unbuffered result
572 - first row has not been read
573 - last_row has been read
574 */
575 if (set->data_cursor == NULL ||
576 set->data_cursor == set->data ||
577 ((set->data_cursor - set->data) > (set->row_count * result->meta->field_count) ))
578 {
579 return NULL;/* No rows or no more rows */
580 }
581
582 previous_row = set->data_cursor - result->meta->field_count;
583 for (i = 0; i < result->meta->field_count; i++) {
584 result->lengths[i] = (Z_TYPE_P(previous_row[i]) == IS_NULL)? 0:Z_STRLEN_P(previous_row[i]);
585 }
586
587 return result->lengths;
588 }
589 /* }}} */
590
591
592 /* {{{ mysqlnd_fetch_lengths_unbuffered */
593 static unsigned long *
mysqlnd_fetch_lengths_unbuffered(MYSQLND_RES * const result TSRMLS_DC)594 mysqlnd_fetch_lengths_unbuffered(MYSQLND_RES * const result TSRMLS_DC)
595 {
596 /* simulate output of libmysql */
597 return (!result->unbuf || result->unbuf->last_row_data || result->unbuf->eof_reached)? result->lengths:NULL;
598 }
599 /* }}} */
600
601
602 /* {{{ mysqlnd_res::fetch_lengths */
_mysqlnd_fetch_lengths(MYSQLND_RES * const result TSRMLS_DC)603 PHPAPI unsigned long * _mysqlnd_fetch_lengths(MYSQLND_RES * const result TSRMLS_DC)
604 {
605 return result->m.fetch_lengths? result->m.fetch_lengths(result TSRMLS_CC) : NULL;
606 }
607 /* }}} */
608
609
610 /* {{{ mysqlnd_fetch_row_unbuffered_c */
611 static MYSQLND_ROW_C
mysqlnd_fetch_row_unbuffered_c(MYSQLND_RES * result TSRMLS_DC)612 mysqlnd_fetch_row_unbuffered_c(MYSQLND_RES * result TSRMLS_DC)
613 {
614 enum_func_status ret;
615 MYSQLND_ROW_C retrow = NULL;
616 unsigned int i,
617 field_count = result->field_count;
618 MYSQLND_PACKET_ROW *row_packet = result->row_packet;
619 unsigned long *lengths = result->lengths;
620
621 DBG_ENTER("mysqlnd_fetch_row_unbuffered_c");
622
623 if (result->unbuf->eof_reached) {
624 /* No more rows obviously */
625 DBG_RETURN(retrow);
626 }
627 if (CONN_GET_STATE(result->conn) != CONN_FETCHING_DATA) {
628 SET_CLIENT_ERROR(*result->conn->error_info, CR_COMMANDS_OUT_OF_SYNC,
629 UNKNOWN_SQLSTATE, mysqlnd_out_of_sync);
630 DBG_RETURN(retrow);
631 }
632 if (!row_packet) {
633 /* Not fully initialized object that is being cleaned up */
634 DBG_RETURN(retrow);
635 }
636 /* Let the row packet fill our buffer and skip additional mnd_malloc + memcpy */
637 row_packet->skip_extraction = FALSE;
638
639 /*
640 If we skip rows (row == NULL) we have to
641 result->m.unbuffered_free_last_data() before it. The function returns always true.
642 */
643 if (PASS == (ret = PACKET_READ(row_packet, result->conn)) && !row_packet->eof) {
644 result->unbuf->row_count++;
645
646 result->m.unbuffered_free_last_data(result TSRMLS_CC);
647
648 result->unbuf->last_row_data = row_packet->fields;
649 result->unbuf->last_row_buffer = row_packet->row_buffer;
650 row_packet->fields = NULL;
651 row_packet->row_buffer = NULL;
652
653 MYSQLND_INC_CONN_STATISTIC(result->conn->stats, STAT_ROWS_FETCHED_FROM_CLIENT_NORMAL_UNBUF);
654
655 if (!row_packet->skip_extraction) {
656 MYSQLND_FIELD *field = result->meta->fields;
657 struct mysqlnd_field_hash_key * hash_key = result->meta->zend_hash_keys;
658
659 enum_func_status rc = result->m.row_decoder(result->unbuf->last_row_buffer,
660 result->unbuf->last_row_data,
661 row_packet->field_count,
662 row_packet->fields_metadata,
663 result->conn->options->int_and_float_native,
664 result->conn->stats TSRMLS_CC);
665 if (PASS != rc) {
666 DBG_RETURN(retrow);
667 }
668
669 retrow = mnd_malloc(result->field_count * sizeof(char *));
670 if (retrow) {
671 for (i = 0; i < field_count; i++, field++, hash_key++) {
672 zval *data = result->unbuf->last_row_data[i];
673 unsigned int len;
674
675 if (Z_TYPE_P(data) != IS_NULL) {
676 convert_to_string(data);
677 retrow[i] = Z_STRVAL_P(data);
678 len = Z_STRLEN_P(data);
679 } else {
680 retrow[i] = NULL;
681 len = 0;
682 }
683
684 if (lengths) {
685 lengths[i] = len;
686 }
687
688 if (field->max_length < len) {
689 field->max_length = len;
690 }
691 }
692 } else {
693 SET_OOM_ERROR(*result->conn->error_info);
694 }
695 }
696 } else if (ret == FAIL) {
697 if (row_packet->error_info.error_no) {
698 COPY_CLIENT_ERROR(*result->conn->error_info, row_packet->error_info);
699 DBG_ERR_FMT("errorno=%u error=%s", row_packet->error_info.error_no, row_packet->error_info.error);
700 }
701 CONN_SET_STATE(result->conn, CONN_READY);
702 result->unbuf->eof_reached = TRUE; /* so next time we won't get an error */
703 } else if (row_packet->eof) {
704 /* Mark the connection as usable again */
705 DBG_INF_FMT("warnings=%u server_status=%u", row_packet->warning_count, row_packet->server_status);
706 result->unbuf->eof_reached = TRUE;
707 memset(result->conn->upsert_status, 0, sizeof(*result->conn->upsert_status));
708 result->conn->upsert_status->warning_count = row_packet->warning_count;
709 result->conn->upsert_status->server_status = row_packet->server_status;
710 /*
711 result->row_packet will be cleaned when
712 destroying the result object
713 */
714 if (result->conn->upsert_status->server_status & SERVER_MORE_RESULTS_EXISTS) {
715 CONN_SET_STATE(result->conn, CONN_NEXT_RESULT_PENDING);
716 } else {
717 CONN_SET_STATE(result->conn, CONN_READY);
718 }
719 result->m.unbuffered_free_last_data(result TSRMLS_CC);
720 }
721
722 DBG_RETURN(retrow);
723 }
724 /* }}} */
725
726
727 /* {{{ mysqlnd_fetch_row_unbuffered */
728 static enum_func_status
mysqlnd_fetch_row_unbuffered(MYSQLND_RES * result,void * param,unsigned int flags,zend_bool * fetched_anything TSRMLS_DC)729 mysqlnd_fetch_row_unbuffered(MYSQLND_RES * result, void *param, unsigned int flags, zend_bool *fetched_anything TSRMLS_DC)
730 {
731 enum_func_status ret;
732 zval *row = (zval *) param;
733 MYSQLND_PACKET_ROW *row_packet = result->row_packet;
734
735 DBG_ENTER("mysqlnd_fetch_row_unbuffered");
736
737 *fetched_anything = FALSE;
738 if (result->unbuf->eof_reached) {
739 /* No more rows obviously */
740 DBG_RETURN(PASS);
741 }
742 if (CONN_GET_STATE(result->conn) != CONN_FETCHING_DATA) {
743 SET_CLIENT_ERROR(*result->conn->error_info, CR_COMMANDS_OUT_OF_SYNC, UNKNOWN_SQLSTATE, mysqlnd_out_of_sync);
744 DBG_RETURN(FAIL);
745 }
746 if (!row_packet) {
747 /* Not fully initialized object that is being cleaned up */
748 DBG_RETURN(FAIL);
749 }
750 /* Let the row packet fill our buffer and skip additional mnd_malloc + memcpy */
751 row_packet->skip_extraction = row? FALSE:TRUE;
752
753 /*
754 If we skip rows (row == NULL) we have to
755 result->m.unbuffered_free_last_data() before it. The function returns always true.
756 */
757 if (PASS == (ret = PACKET_READ(row_packet, result->conn)) && !row_packet->eof) {
758 result->m.unbuffered_free_last_data(result TSRMLS_CC);
759
760 result->unbuf->last_row_data = row_packet->fields;
761 result->unbuf->last_row_buffer = row_packet->row_buffer;
762 row_packet->fields = NULL;
763 row_packet->row_buffer = NULL;
764
765 MYSQLND_INC_CONN_STATISTIC(result->conn->stats, STAT_ROWS_FETCHED_FROM_CLIENT_NORMAL_UNBUF);
766
767 if (!row_packet->skip_extraction) {
768 HashTable *row_ht = Z_ARRVAL_P(row);
769 MYSQLND_FIELD *field = result->meta->fields;
770 struct mysqlnd_field_hash_key * hash_key = result->meta->zend_hash_keys;
771 unsigned int i, field_count = result->field_count;
772 unsigned long *lengths = result->lengths;
773
774 enum_func_status rc = result->m.row_decoder(result->unbuf->last_row_buffer,
775 result->unbuf->last_row_data,
776 field_count,
777 row_packet->fields_metadata,
778 result->conn->options->int_and_float_native,
779 result->conn->stats TSRMLS_CC);
780 if (PASS != rc) {
781 DBG_RETURN(FAIL);
782 }
783 for (i = 0; i < field_count; i++, field++, hash_key++) {
784 zval *data = result->unbuf->last_row_data[i];
785 unsigned int len = (Z_TYPE_P(data) == IS_NULL)? 0:Z_STRLEN_P(data);
786
787 if (lengths) {
788 lengths[i] = len;
789 }
790
791 if (flags & MYSQLND_FETCH_NUM) {
792 Z_ADDREF_P(data);
793 zend_hash_next_index_insert(row_ht, &data, sizeof(zval *), NULL);
794 }
795 if (flags & MYSQLND_FETCH_ASSOC) {
796 /* zend_hash_quick_update needs length + trailing zero */
797 /* QQ: Error handling ? */
798 /*
799 zend_hash_quick_update does not check, as add_assoc_zval_ex do, whether
800 the index is a numeric and convert it to it. This however means constant
801 hashing of the column name, which is not needed as it can be precomputed.
802 */
803 Z_ADDREF_P(data);
804 if (hash_key->is_numeric == FALSE) {
805 zend_hash_quick_update(Z_ARRVAL_P(row),
806 field->name,
807 field->name_length + 1,
808 hash_key->key,
809 (void *) &data, sizeof(zval *), NULL);
810 } else {
811 zend_hash_index_update(Z_ARRVAL_P(row),
812 hash_key->key,
813 (void *) &data, sizeof(zval *), NULL);
814 }
815 }
816 if (field->max_length < len) {
817 field->max_length = len;
818 }
819 }
820 }
821 *fetched_anything = TRUE;
822 result->unbuf->row_count++;
823 } else if (ret == FAIL) {
824 if (row_packet->error_info.error_no) {
825 COPY_CLIENT_ERROR(*result->conn->error_info, row_packet->error_info);
826 DBG_ERR_FMT("errorno=%u error=%s", row_packet->error_info.error_no, row_packet->error_info.error);
827 }
828 CONN_SET_STATE(result->conn, CONN_READY);
829 result->unbuf->eof_reached = TRUE; /* so next time we won't get an error */
830 } else if (row_packet->eof) {
831 /* Mark the connection as usable again */
832 DBG_INF_FMT("warnings=%u server_status=%u", row_packet->warning_count, row_packet->server_status);
833 result->unbuf->eof_reached = TRUE;
834 memset(result->conn->upsert_status, 0, sizeof(*result->conn->upsert_status));
835 result->conn->upsert_status->warning_count = row_packet->warning_count;
836 result->conn->upsert_status->server_status = row_packet->server_status;
837 /*
838 result->row_packet will be cleaned when
839 destroying the result object
840 */
841 if (result->conn->upsert_status->server_status & SERVER_MORE_RESULTS_EXISTS) {
842 CONN_SET_STATE(result->conn, CONN_NEXT_RESULT_PENDING);
843 } else {
844 CONN_SET_STATE(result->conn, CONN_READY);
845 }
846 result->m.unbuffered_free_last_data(result TSRMLS_CC);
847 }
848
849 DBG_INF_FMT("ret=%s fetched=%u", ret == PASS? "PASS":"FAIL", *fetched_anything);
850 DBG_RETURN(PASS);
851 }
852 /* }}} */
853
854
855 /* {{{ mysqlnd_res::use_result */
856 static MYSQLND_RES *
MYSQLND_METHOD(mysqlnd_res,use_result)857 MYSQLND_METHOD(mysqlnd_res, use_result)(MYSQLND_RES * const result, zend_bool ps TSRMLS_DC)
858 {
859 DBG_ENTER("mysqlnd_res::use_result");
860
861 SET_EMPTY_ERROR(*result->conn->error_info);
862
863 if (ps == FALSE) {
864 result->type = MYSQLND_RES_NORMAL;
865 result->m.fetch_row = result->m.fetch_row_normal_unbuffered;
866 result->m.fetch_lengths = mysqlnd_fetch_lengths_unbuffered;
867 result->m.row_decoder = php_mysqlnd_rowp_read_text_protocol;
868 result->lengths = mnd_ecalloc(result->field_count, sizeof(unsigned long));
869 if (!result->lengths) {
870 goto oom;
871 }
872 } else {
873 result->type = MYSQLND_RES_PS_UNBUF;
874 result->m.fetch_row = NULL;
875 /* result->m.fetch_row() will be set in mysqlnd_ps.c */
876 result->m.fetch_lengths = NULL; /* makes no sense */
877 result->m.row_decoder = php_mysqlnd_rowp_read_binary_protocol;
878 result->lengths = NULL;
879 }
880
881 result->result_set_memory_pool = mysqlnd_mempool_create(MYSQLND_G(mempool_default_size) TSRMLS_CC);
882 result->unbuf = mnd_ecalloc(1, sizeof(MYSQLND_RES_UNBUFFERED));
883 if (!result->result_set_memory_pool || !result->unbuf) {
884 goto oom;
885 }
886
887 /*
888 Will be freed in the mysqlnd_internal_free_result_contents() called
889 by the resource destructor. mysqlnd_fetch_row_unbuffered() expects
890 this to be not NULL.
891 */
892 /* FALSE = non-persistent */
893 result->row_packet = result->conn->protocol->m.get_row_packet(result->conn->protocol, FALSE TSRMLS_CC);
894 if (!result->row_packet) {
895 goto oom;
896 }
897 result->row_packet->result_set_memory_pool = result->result_set_memory_pool;
898 result->row_packet->field_count = result->field_count;
899 result->row_packet->binary_protocol = ps;
900 result->row_packet->fields_metadata = result->meta->fields;
901 result->row_packet->bit_fields_count = result->meta->bit_fields_count;
902 result->row_packet->bit_fields_total_len = result->meta->bit_fields_total_len;
903
904 DBG_RETURN(result);
905 oom:
906 SET_OOM_ERROR(*result->conn->error_info);
907 DBG_RETURN(NULL);
908 }
909 /* }}} */
910
911
912 /* {{{ mysqlnd_fetch_row_buffered_c */
913 static MYSQLND_ROW_C
mysqlnd_fetch_row_buffered_c(MYSQLND_RES * result TSRMLS_DC)914 mysqlnd_fetch_row_buffered_c(MYSQLND_RES * result TSRMLS_DC)
915 {
916 MYSQLND_ROW_C ret = NULL;
917 MYSQLND_RES_BUFFERED *set = result->stored_data;
918
919 DBG_ENTER("mysqlnd_fetch_row_buffered_c");
920
921 /* If we haven't read everything */
922 if (set->data_cursor &&
923 (set->data_cursor - set->data) < (set->row_count * result->meta->field_count))
924 {
925 zval **current_row = set->data_cursor;
926 MYSQLND_FIELD *field = result->meta->fields;
927 struct mysqlnd_field_hash_key * hash_key = result->meta->zend_hash_keys;
928 unsigned int i;
929
930 if (NULL == current_row[0]) {
931 uint64_t row_num = (set->data_cursor - set->data) / result->meta->field_count;
932 enum_func_status rc = result->m.row_decoder(set->row_buffers[row_num],
933 current_row,
934 result->meta->field_count,
935 result->meta->fields,
936 result->conn->options->int_and_float_native,
937 result->conn->stats TSRMLS_CC);
938 if (rc != PASS) {
939 DBG_RETURN(ret);
940 }
941 set->initialized_rows++;
942 for (i = 0; i < result->field_count; i++) {
943 /*
944 NULL fields are 0 length, 0 is not more than 0
945 String of zero size, definitely can't be the next max_length.
946 Thus for NULL and zero-length we are quite efficient.
947 */
948 if (Z_TYPE_P(current_row[i]) >= IS_STRING) {
949 unsigned long len = Z_STRLEN_P(current_row[i]);
950 if (field->max_length < len) {
951 field->max_length = len;
952 }
953 }
954 }
955 }
956
957 set->data_cursor += result->meta->field_count;
958 MYSQLND_INC_GLOBAL_STATISTIC(STAT_ROWS_FETCHED_FROM_CLIENT_NORMAL_BUF);
959
960 ret = mnd_malloc(result->field_count * sizeof(char *));
961 if (ret) {
962 for (i = 0; i < result->field_count; i++, field++, hash_key++) {
963 zval *data = current_row[i];
964
965 if (Z_TYPE_P(data) != IS_NULL) {
966 convert_to_string(data);
967 ret[i] = Z_STRVAL_P(data);
968 } else {
969 ret[i] = NULL;
970 }
971 }
972 }
973 /* there is no conn handle in this function thus we can't set OOM in error_info */
974 } else {
975 set->data_cursor = NULL;
976 DBG_INF("EOF reached");
977 }
978 DBG_RETURN(ret);
979 }
980 /* }}} */
981
982
983 /* {{{ mysqlnd_fetch_row_buffered */
984 static enum_func_status
mysqlnd_fetch_row_buffered(MYSQLND_RES * result,void * param,unsigned int flags,zend_bool * fetched_anything TSRMLS_DC)985 mysqlnd_fetch_row_buffered(MYSQLND_RES * result, void *param, unsigned int flags, zend_bool *fetched_anything TSRMLS_DC)
986 {
987 unsigned int i;
988 zval *row = (zval *) param;
989 MYSQLND_RES_BUFFERED *set = result->stored_data;
990 enum_func_status ret = FAIL;
991
992 DBG_ENTER("mysqlnd_fetch_row_buffered");
993
994 /* If we haven't read everything */
995 if (set->data_cursor &&
996 (set->data_cursor - set->data) < (set->row_count * result->meta->field_count))
997 {
998 zval **current_row = set->data_cursor;
999 MYSQLND_FIELD *field = result->meta->fields;
1000 struct mysqlnd_field_hash_key * hash_key = result->meta->zend_hash_keys;
1001
1002 if (NULL == current_row[0]) {
1003 uint64_t row_num = (set->data_cursor - set->data) / result->meta->field_count;
1004 enum_func_status rc = result->m.row_decoder(set->row_buffers[row_num],
1005 current_row,
1006 result->meta->field_count,
1007 result->meta->fields,
1008 result->conn->options->int_and_float_native,
1009 result->conn->stats TSRMLS_CC);
1010 if (rc != PASS) {
1011 DBG_RETURN(FAIL);
1012 }
1013 set->initialized_rows++;
1014 for (i = 0; i < result->field_count; i++) {
1015 /*
1016 NULL fields are 0 length, 0 is not more than 0
1017 String of zero size, definitely can't be the next max_length.
1018 Thus for NULL and zero-length we are quite efficient.
1019 */
1020 if (Z_TYPE_P(current_row[i]) >= IS_STRING) {
1021 unsigned long len = Z_STRLEN_P(current_row[i]);
1022 if (field->max_length < len) {
1023 field->max_length = len;
1024 }
1025 }
1026 }
1027 }
1028
1029 for (i = 0; i < result->field_count; i++, field++, hash_key++) {
1030 zval *data = current_row[i];
1031
1032 if (flags & MYSQLND_FETCH_NUM) {
1033 Z_ADDREF_P(data);
1034 zend_hash_next_index_insert(Z_ARRVAL_P(row), &data, sizeof(zval *), NULL);
1035 }
1036 if (flags & MYSQLND_FETCH_ASSOC) {
1037 /* zend_hash_quick_update needs length + trailing zero */
1038 /* QQ: Error handling ? */
1039 /*
1040 zend_hash_quick_update does not check, as add_assoc_zval_ex do, whether
1041 the index is a numeric and convert it to it. This however means constant
1042 hashing of the column name, which is not needed as it can be precomputed.
1043 */
1044 Z_ADDREF_P(data);
1045 if (hash_key->is_numeric == FALSE) {
1046 zend_hash_quick_update(Z_ARRVAL_P(row),
1047 field->name,
1048 field->name_length + 1,
1049 hash_key->key,
1050 (void *) &data, sizeof(zval *), NULL);
1051 } else {
1052 zend_hash_index_update(Z_ARRVAL_P(row),
1053 hash_key->key,
1054 (void *) &data, sizeof(zval *), NULL);
1055 }
1056 }
1057 }
1058 set->data_cursor += result->meta->field_count;
1059 *fetched_anything = TRUE;
1060 MYSQLND_INC_GLOBAL_STATISTIC(STAT_ROWS_FETCHED_FROM_CLIENT_NORMAL_BUF);
1061 ret = PASS;
1062 } else {
1063 set->data_cursor = NULL;
1064 *fetched_anything = FALSE;
1065 ret = PASS;
1066 DBG_INF("EOF reached");
1067 }
1068 DBG_INF_FMT("ret=PASS fetched=%u", *fetched_anything);
1069 DBG_RETURN(ret);
1070 }
1071 /* }}} */
1072
1073
1074 #define STORE_RESULT_PREALLOCATED_SET_IF_NOT_EMPTY 2
1075
1076 /* {{{ mysqlnd_res::store_result_fetch_data */
1077 enum_func_status
MYSQLND_METHOD(mysqlnd_res,store_result_fetch_data)1078 MYSQLND_METHOD(mysqlnd_res, store_result_fetch_data)(MYSQLND_CONN_DATA * const conn, MYSQLND_RES * result,
1079 MYSQLND_RES_METADATA *meta,
1080 zend_bool binary_protocol TSRMLS_DC)
1081 {
1082 enum_func_status ret;
1083 MYSQLND_PACKET_ROW *row_packet = NULL;
1084 unsigned int next_extend = STORE_RESULT_PREALLOCATED_SET_IF_NOT_EMPTY, free_rows = 1;
1085 MYSQLND_RES_BUFFERED *set;
1086
1087 DBG_ENTER("mysqlnd_res::store_result_fetch_data");
1088
1089 result->stored_data = set = mnd_ecalloc(1, sizeof(MYSQLND_RES_BUFFERED));
1090 if (!set) {
1091 SET_OOM_ERROR(*conn->error_info);
1092 ret = FAIL;
1093 goto end;
1094 }
1095 if (free_rows) {
1096 set->row_buffers = mnd_emalloc((size_t)(free_rows * sizeof(MYSQLND_MEMORY_POOL_CHUNK *)));
1097 if (!set->row_buffers) {
1098 SET_OOM_ERROR(*conn->error_info);
1099 ret = FAIL;
1100 goto end;
1101 }
1102 }
1103 set->references = 1;
1104
1105 /* non-persistent */
1106 row_packet = conn->protocol->m.get_row_packet(conn->protocol, FALSE TSRMLS_CC);
1107 if (!row_packet) {
1108 SET_OOM_ERROR(*conn->error_info);
1109 ret = FAIL;
1110 goto end;
1111 }
1112 row_packet->result_set_memory_pool = result->result_set_memory_pool;
1113 row_packet->field_count = meta->field_count;
1114 row_packet->binary_protocol = binary_protocol;
1115 row_packet->fields_metadata = meta->fields;
1116 row_packet->bit_fields_count = meta->bit_fields_count;
1117 row_packet->bit_fields_total_len = meta->bit_fields_total_len;
1118
1119 row_packet->skip_extraction = TRUE; /* let php_mysqlnd_rowp_read() not allocate row_packet->fields, we will do it */
1120
1121 while (FAIL != (ret = PACKET_READ(row_packet, conn)) && !row_packet->eof) {
1122 if (!free_rows) {
1123 uint64_t total_allocated_rows = free_rows = next_extend = next_extend * 11 / 10; /* extend with 10% */
1124 MYSQLND_MEMORY_POOL_CHUNK ** new_row_buffers;
1125 total_allocated_rows += set->row_count;
1126
1127 /* don't try to allocate more than possible - mnd_XXalloc expects size_t, and it can have narrower range than uint64_t */
1128 if (total_allocated_rows * sizeof(MYSQLND_MEMORY_POOL_CHUNK *) > SIZE_MAX) {
1129 SET_OOM_ERROR(*conn->error_info);
1130 ret = FAIL;
1131 goto end;
1132 }
1133 new_row_buffers = mnd_erealloc(set->row_buffers, (size_t)(total_allocated_rows * sizeof(MYSQLND_MEMORY_POOL_CHUNK *)));
1134 if (!new_row_buffers) {
1135 SET_OOM_ERROR(*conn->error_info);
1136 ret = FAIL;
1137 goto end;
1138 }
1139 set->row_buffers = new_row_buffers;
1140 }
1141 free_rows--;
1142 set->row_buffers[set->row_count] = row_packet->row_buffer;
1143
1144 set->row_count++;
1145
1146 /* So row_packet's destructor function won't efree() it */
1147 row_packet->fields = NULL;
1148 row_packet->row_buffer = NULL;
1149
1150 /*
1151 No need to FREE_ALLOCA as we can reuse the
1152 'lengths' and 'fields' arrays. For lengths its absolutely safe.
1153 'fields' is reused because the ownership of the strings has been
1154 transferred above.
1155 */
1156 }
1157 /* Overflow ? */
1158 if (set->row_count) {
1159 /* don't try to allocate more than possible - mnd_XXalloc expects size_t, and it can have narrower range than uint64_t */
1160 if (set->row_count * meta->field_count * sizeof(zval *) > SIZE_MAX) {
1161 SET_OOM_ERROR(*conn->error_info);
1162 ret = FAIL;
1163 goto end;
1164 }
1165 /* if pecalloc is used valgrind barks gcc version 4.3.1 20080507 (prerelease) [gcc-4_3-branch revision 135036] (SUSE Linux) */
1166 set->data = mnd_emalloc((size_t)(set->row_count * meta->field_count * sizeof(zval *)));
1167 if (!set->data) {
1168 SET_OOM_ERROR(*conn->error_info);
1169 ret = FAIL;
1170 goto end;
1171 }
1172 memset(set->data, 0, (size_t)(set->row_count * meta->field_count * sizeof(zval *)));
1173 }
1174
1175 MYSQLND_INC_CONN_STATISTIC_W_VALUE(conn->stats,
1176 binary_protocol? STAT_ROWS_BUFFERED_FROM_CLIENT_PS:
1177 STAT_ROWS_BUFFERED_FROM_CLIENT_NORMAL,
1178 set->row_count);
1179
1180 /* Finally clean */
1181 if (row_packet->eof) {
1182 memset(conn->upsert_status, 0, sizeof(*conn->upsert_status));
1183 conn->upsert_status->warning_count = row_packet->warning_count;
1184 conn->upsert_status->server_status = row_packet->server_status;
1185 }
1186 /* save some memory */
1187 if (free_rows) {
1188 /* don't try to allocate more than possible - mnd_XXalloc expects size_t, and it can have narrower range than uint64_t */
1189 if (set->row_count * sizeof(MYSQLND_MEMORY_POOL_CHUNK *) > SIZE_MAX) {
1190 SET_OOM_ERROR(*conn->error_info);
1191 ret = FAIL;
1192 goto end;
1193 }
1194 set->row_buffers = mnd_erealloc(set->row_buffers, (size_t) (set->row_count * sizeof(MYSQLND_MEMORY_POOL_CHUNK *)));
1195 }
1196
1197 if (conn->upsert_status->server_status & SERVER_MORE_RESULTS_EXISTS) {
1198 CONN_SET_STATE(conn, CONN_NEXT_RESULT_PENDING);
1199 } else {
1200 CONN_SET_STATE(conn, CONN_READY);
1201 }
1202
1203 if (ret == FAIL) {
1204 COPY_CLIENT_ERROR(set->error_info, row_packet->error_info);
1205 } else {
1206 /* Position at the first row */
1207 set->data_cursor = set->data;
1208
1209 /* libmysql's documentation says it should be so for SELECT statements */
1210 conn->upsert_status->affected_rows = set->row_count;
1211 }
1212 DBG_INF_FMT("ret=%s row_count=%u warnings=%u server_status=%u",
1213 ret == PASS? "PASS":"FAIL", (uint) set->row_count, conn->upsert_status->warning_count, conn->upsert_status->server_status);
1214 end:
1215 PACKET_FREE(row_packet);
1216
1217 DBG_RETURN(ret);
1218 }
1219 /* }}} */
1220
1221
1222 /* {{{ mysqlnd_res::store_result */
1223 static MYSQLND_RES *
MYSQLND_METHOD(mysqlnd_res,store_result)1224 MYSQLND_METHOD(mysqlnd_res, store_result)(MYSQLND_RES * result,
1225 MYSQLND_CONN_DATA * const conn,
1226 zend_bool ps_protocol TSRMLS_DC)
1227 {
1228 enum_func_status ret;
1229
1230 DBG_ENTER("mysqlnd_res::store_result");
1231
1232 /* We need the conn because we are doing lazy zval initialization in buffered_fetch_row */
1233 result->conn = conn->m->get_reference(conn TSRMLS_CC);
1234 result->type = MYSQLND_RES_NORMAL;
1235 result->m.fetch_row = result->m.fetch_row_normal_buffered;
1236 result->m.fetch_lengths = mysqlnd_fetch_lengths_buffered;
1237 result->m.row_decoder = ps_protocol? php_mysqlnd_rowp_read_binary_protocol:
1238 php_mysqlnd_rowp_read_text_protocol;
1239
1240 result->result_set_memory_pool = mysqlnd_mempool_create(MYSQLND_G(mempool_default_size) TSRMLS_CC);
1241 result->lengths = mnd_ecalloc(result->field_count, sizeof(unsigned long));
1242
1243 if (!result->result_set_memory_pool || !result->lengths) {
1244 SET_OOM_ERROR(*conn->error_info);
1245 DBG_RETURN(NULL);
1246 }
1247
1248 CONN_SET_STATE(conn, CONN_FETCHING_DATA);
1249
1250 ret = result->m.store_result_fetch_data(conn, result, result->meta, ps_protocol TSRMLS_CC);
1251 if (FAIL == ret) {
1252 if (result->stored_data) {
1253 COPY_CLIENT_ERROR(*conn->error_info, result->stored_data->error_info);
1254 } else {
1255 SET_OOM_ERROR(*conn->error_info);
1256 }
1257 DBG_RETURN(NULL);
1258 }
1259 /* libmysql's documentation says it should be so for SELECT statements */
1260 conn->upsert_status->affected_rows = result->stored_data->row_count;
1261
1262 DBG_RETURN(result);
1263 }
1264 /* }}} */
1265
1266
1267 /* {{{ mysqlnd_res::skip_result */
1268 static enum_func_status
MYSQLND_METHOD(mysqlnd_res,skip_result)1269 MYSQLND_METHOD(mysqlnd_res, skip_result)(MYSQLND_RES * const result TSRMLS_DC)
1270 {
1271 zend_bool fetched_anything;
1272
1273 DBG_ENTER("mysqlnd_res::skip_result");
1274 /*
1275 Unbuffered sets
1276 A PS could be prepared - there is metadata and thus a stmt->result but the
1277 fetch_row function isn't actually set (NULL), thus we have to skip these.
1278 */
1279 if (!result->stored_data && result->unbuf &&
1280 !result->unbuf->eof_reached && result->m.fetch_row)
1281 {
1282 DBG_INF("skipping result");
1283 /* We have to fetch all data to clean the line */
1284 MYSQLND_INC_CONN_STATISTIC(result->conn->stats,
1285 result->type == MYSQLND_RES_NORMAL? STAT_FLUSHED_NORMAL_SETS:
1286 STAT_FLUSHED_PS_SETS);
1287
1288 while ((PASS == result->m.fetch_row(result, NULL, 0, &fetched_anything TSRMLS_CC)) && fetched_anything == TRUE) {
1289 /* do nothing */;
1290 }
1291 }
1292 DBG_RETURN(PASS);
1293 }
1294 /* }}} */
1295
1296
1297 /* {{{ mysqlnd_res::free_result */
1298 static enum_func_status
MYSQLND_METHOD(mysqlnd_res,free_result)1299 MYSQLND_METHOD(mysqlnd_res, free_result)(MYSQLND_RES * result, zend_bool implicit TSRMLS_DC)
1300 {
1301 DBG_ENTER("mysqlnd_res::free_result");
1302
1303 result->m.skip_result(result TSRMLS_CC);
1304 MYSQLND_INC_CONN_STATISTIC(result->conn? result->conn->stats : NULL,
1305 implicit == TRUE? STAT_FREE_RESULT_IMPLICIT:
1306 STAT_FREE_RESULT_EXPLICIT);
1307
1308 result->m.free_result_internal(result TSRMLS_CC);
1309 DBG_RETURN(PASS);
1310 }
1311 /* }}} */
1312
1313
1314 /* {{{ mysqlnd_res::data_seek */
1315 static enum_func_status
MYSQLND_METHOD(mysqlnd_res,data_seek)1316 MYSQLND_METHOD(mysqlnd_res, data_seek)(MYSQLND_RES * result, uint64_t row TSRMLS_DC)
1317 {
1318 DBG_ENTER("mysqlnd_res::data_seek");
1319 DBG_INF_FMT("row=%lu", row);
1320
1321 if (!result->stored_data) {
1322 return FAIL;
1323 }
1324
1325 /* libmysql just moves to the end, it does traversing of a linked list */
1326 if (row >= result->stored_data->row_count) {
1327 result->stored_data->data_cursor = NULL;
1328 } else {
1329 result->stored_data->data_cursor = result->stored_data->data + row * result->meta->field_count;
1330 }
1331
1332 DBG_RETURN(PASS);
1333 }
1334 /* }}} */
1335
1336
1337 /* {{{ mysqlnd_res::num_rows */
1338 static uint64_t
MYSQLND_METHOD(mysqlnd_res,num_rows)1339 MYSQLND_METHOD(mysqlnd_res, num_rows)(const MYSQLND_RES * const result TSRMLS_DC)
1340 {
1341 /* Be compatible with libmysql. We count row_count, but will return 0 */
1342 return result->stored_data? result->stored_data->row_count:(result->unbuf && result->unbuf->eof_reached? result->unbuf->row_count:0);
1343 }
1344 /* }}} */
1345
1346
1347 /* {{{ mysqlnd_res::num_fields */
1348 static unsigned int
MYSQLND_METHOD(mysqlnd_res,num_fields)1349 MYSQLND_METHOD(mysqlnd_res, num_fields)(const MYSQLND_RES * const result TSRMLS_DC)
1350 {
1351 return result->field_count;
1352 }
1353 /* }}} */
1354
1355
1356 /* {{{ mysqlnd_res::fetch_field */
1357 static const MYSQLND_FIELD *
MYSQLND_METHOD(mysqlnd_res,fetch_field)1358 MYSQLND_METHOD(mysqlnd_res, fetch_field)(MYSQLND_RES * const result TSRMLS_DC)
1359 {
1360 DBG_ENTER("mysqlnd_res::fetch_field");
1361 do {
1362 if (result->meta) {
1363 /*
1364 We optimize the result set, so we don't convert all the data from raw buffer format to
1365 zval arrays during store. In the case someone doesn't read all the lines this will
1366 save time. However, when a metadata call is done, we need to calculate max_length.
1367 We don't have control whether max_length will be used, unfortunately. Otherwise we
1368 could have been able to skip that step.
1369 Well, if the mysqli API switches from returning stdClass to class like mysqli_field_metadata,
1370 then we can have max_length as dynamic property, which will be calculated during runtime and
1371 not during mysqli_fetch_field() time.
1372 */
1373 if (result->stored_data && (result->stored_data->initialized_rows < result->stored_data->row_count)) {
1374 DBG_INF_FMT("We have decode the whole result set to be able to satisfy this meta request");
1375 /* we have to initialize the rest to get the updated max length */
1376 if (PASS != result->m.initialize_result_set_rest(result TSRMLS_CC)) {
1377 break;
1378 }
1379 }
1380 DBG_RETURN(result->meta->m->fetch_field(result->meta TSRMLS_CC));
1381 }
1382 } while (0);
1383 DBG_RETURN(NULL);
1384 }
1385 /* }}} */
1386
1387
1388 /* {{{ mysqlnd_res::fetch_field_direct */
1389 static const MYSQLND_FIELD *
MYSQLND_METHOD(mysqlnd_res,fetch_field_direct)1390 MYSQLND_METHOD(mysqlnd_res, fetch_field_direct)(MYSQLND_RES * const result, MYSQLND_FIELD_OFFSET fieldnr TSRMLS_DC)
1391 {
1392 DBG_ENTER("mysqlnd_res::fetch_field_direct");
1393 do {
1394 if (result->meta) {
1395 /*
1396 We optimize the result set, so we don't convert all the data from raw buffer format to
1397 zval arrays during store. In the case someone doesn't read all the lines this will
1398 save time. However, when a metadata call is done, we need to calculate max_length.
1399 We don't have control whether max_length will be used, unfortunately. Otherwise we
1400 could have been able to skip that step.
1401 Well, if the mysqli API switches from returning stdClass to class like mysqli_field_metadata,
1402 then we can have max_length as dynamic property, which will be calculated during runtime and
1403 not during mysqli_fetch_field_direct() time.
1404 */
1405 if (result->stored_data && (result->stored_data->initialized_rows < result->stored_data->row_count)) {
1406 DBG_INF_FMT("We have decode the whole result set to be able to satisfy this meta request");
1407 /* we have to initialized the rest to get the updated max length */
1408 if (PASS != result->m.initialize_result_set_rest(result TSRMLS_CC)) {
1409 break;
1410 }
1411 }
1412 DBG_RETURN(result->meta->m->fetch_field_direct(result->meta, fieldnr TSRMLS_CC));
1413 }
1414 } while (0);
1415
1416 DBG_RETURN(NULL);
1417 }
1418 /* }}} */
1419
1420
1421 /* {{{ mysqlnd_res::fetch_field */
1422 static const MYSQLND_FIELD *
MYSQLND_METHOD(mysqlnd_res,fetch_fields)1423 MYSQLND_METHOD(mysqlnd_res, fetch_fields)(MYSQLND_RES * const result TSRMLS_DC)
1424 {
1425 DBG_ENTER("mysqlnd_res::fetch_fields");
1426 do {
1427 if (result->meta) {
1428 if (result->stored_data && (result->stored_data->initialized_rows < result->stored_data->row_count)) {
1429 /* we have to initialize the rest to get the updated max length */
1430 if (PASS != result->m.initialize_result_set_rest(result TSRMLS_CC)) {
1431 break;
1432 }
1433 }
1434 DBG_RETURN(result->meta->m->fetch_fields(result->meta TSRMLS_CC));
1435 }
1436 } while (0);
1437 DBG_RETURN(NULL);
1438 }
1439 /* }}} */
1440
1441
1442
1443 /* {{{ mysqlnd_res::field_seek */
1444 static MYSQLND_FIELD_OFFSET
MYSQLND_METHOD(mysqlnd_res,field_seek)1445 MYSQLND_METHOD(mysqlnd_res, field_seek)(MYSQLND_RES * const result, MYSQLND_FIELD_OFFSET field_offset TSRMLS_DC)
1446 {
1447 MYSQLND_FIELD_OFFSET return_value = 0;
1448 if (result->meta) {
1449 return_value = result->meta->current_field;
1450 result->meta->current_field = field_offset;
1451 }
1452 return return_value;
1453 }
1454 /* }}} */
1455
1456
1457 /* {{{ mysqlnd_res::field_tell */
1458 static MYSQLND_FIELD_OFFSET
MYSQLND_METHOD(mysqlnd_res,field_tell)1459 MYSQLND_METHOD(mysqlnd_res, field_tell)(const MYSQLND_RES * const result TSRMLS_DC)
1460 {
1461 return result->meta? result->meta->m->field_tell(result->meta TSRMLS_CC) : 0;
1462 }
1463 /* }}} */
1464
1465
1466 /* {{{ mysqlnd_res::fetch_into */
1467 static void
MYSQLND_METHOD(mysqlnd_res,fetch_into)1468 MYSQLND_METHOD(mysqlnd_res, fetch_into)(MYSQLND_RES * result, unsigned int flags,
1469 zval *return_value,
1470 enum_mysqlnd_extension extension TSRMLS_DC ZEND_FILE_LINE_DC)
1471 {
1472 zend_bool fetched_anything;
1473
1474 DBG_ENTER("mysqlnd_res::fetch_into");
1475
1476 if (!result->m.fetch_row) {
1477 RETVAL_NULL();
1478 DBG_VOID_RETURN;
1479 }
1480 /*
1481 Hint Zend how many elements we will have in the hash. Thus it won't
1482 extend and rehash the hash constantly.
1483 */
1484 mysqlnd_array_init(return_value, mysqlnd_num_fields(result) * 2);
1485 if (FAIL == result->m.fetch_row(result, (void *)return_value, flags, &fetched_anything TSRMLS_CC)) {
1486 php_error_docref(NULL TSRMLS_CC, E_WARNING, "Error while reading a row");
1487 zval_dtor(return_value);
1488 RETVAL_FALSE;
1489 } else if (fetched_anything == FALSE) {
1490 zval_dtor(return_value);
1491 switch (extension) {
1492 case MYSQLND_MYSQLI:
1493 RETVAL_NULL();
1494 break;
1495 case MYSQLND_MYSQL:
1496 RETVAL_FALSE;
1497 break;
1498 default:exit(0);
1499 }
1500 }
1501 /*
1502 return_value is IS_NULL for no more data and an array for data. Thus it's ok
1503 to return here.
1504 */
1505 DBG_VOID_RETURN;
1506 }
1507 /* }}} */
1508
1509
1510 /* {{{ mysqlnd_res::fetch_row_c */
1511 static MYSQLND_ROW_C
MYSQLND_METHOD(mysqlnd_res,fetch_row_c)1512 MYSQLND_METHOD(mysqlnd_res, fetch_row_c)(MYSQLND_RES * result TSRMLS_DC)
1513 {
1514 MYSQLND_ROW_C ret = NULL;
1515 DBG_ENTER("mysqlnd_res::fetch_row_c");
1516
1517 if (result->m.fetch_row) {
1518 if (result->m.fetch_row == result->m.fetch_row_normal_buffered) {
1519 DBG_RETURN(mysqlnd_fetch_row_buffered_c(result TSRMLS_CC));
1520 } else if (result->m.fetch_row == result->m.fetch_row_normal_unbuffered) {
1521 DBG_RETURN(mysqlnd_fetch_row_unbuffered_c(result TSRMLS_CC));
1522 } else {
1523 php_error_docref(NULL TSRMLS_CC, E_ERROR, "result->m.fetch_row has invalid value. Report to the developers");
1524 }
1525 }
1526 DBG_RETURN(ret);
1527 }
1528 /* }}} */
1529
1530
1531 /* {{{ mysqlnd_res::fetch_all */
1532 static void
MYSQLND_METHOD(mysqlnd_res,fetch_all)1533 MYSQLND_METHOD(mysqlnd_res, fetch_all)(MYSQLND_RES * result, unsigned int flags, zval *return_value TSRMLS_DC ZEND_FILE_LINE_DC)
1534 {
1535 zval *row;
1536 ulong i = 0;
1537 MYSQLND_RES_BUFFERED *set = result->stored_data;
1538
1539 DBG_ENTER("mysqlnd_res::fetch_all");
1540
1541 if ((!result->unbuf && !set)) {
1542 php_error_docref(NULL TSRMLS_CC, E_WARNING, "fetch_all can be used only with buffered sets");
1543 if (result->conn) {
1544 SET_CLIENT_ERROR(*result->conn->error_info, CR_NOT_IMPLEMENTED, UNKNOWN_SQLSTATE, "fetch_all can be used only with buffered sets");
1545 }
1546 RETVAL_NULL();
1547 DBG_VOID_RETURN;
1548 }
1549
1550 /* 4 is a magic value. The cast is safe, if larger then the array will be later extended - no big deal :) */
1551 mysqlnd_array_init(return_value, set? (unsigned int) set->row_count : 4);
1552
1553 do {
1554 MAKE_STD_ZVAL(row);
1555 mysqlnd_fetch_into(result, flags, row, MYSQLND_MYSQLI);
1556 if (Z_TYPE_P(row) != IS_ARRAY) {
1557 zval_ptr_dtor(&row);
1558 break;
1559 }
1560 add_index_zval(return_value, i++, row);
1561 } while (1);
1562
1563 DBG_VOID_RETURN;
1564 }
1565 /* }}} */
1566
1567
1568 /* {{{ mysqlnd_res::fetch_field_data */
1569 static void
MYSQLND_METHOD(mysqlnd_res,fetch_field_data)1570 MYSQLND_METHOD(mysqlnd_res, fetch_field_data)(MYSQLND_RES * result, unsigned int offset, zval *return_value TSRMLS_DC)
1571 {
1572 zval row;
1573 zval **entry;
1574 unsigned int i = 0;
1575
1576 DBG_ENTER("mysqlnd_res::fetch_field_data");
1577 DBG_INF_FMT("offset=%u", offset);
1578
1579 if (!result->m.fetch_row) {
1580 RETVAL_NULL();
1581 DBG_VOID_RETURN;
1582 }
1583 /*
1584 Hint Zend how many elements we will have in the hash. Thus it won't
1585 extend and rehash the hash constantly.
1586 */
1587 INIT_PZVAL(&row);
1588 mysqlnd_fetch_into(result, MYSQLND_FETCH_NUM, &row, MYSQLND_MYSQL);
1589 if (Z_TYPE(row) != IS_ARRAY) {
1590 zval_dtor(&row);
1591 RETVAL_NULL();
1592 DBG_VOID_RETURN;
1593 }
1594 zend_hash_internal_pointer_reset(Z_ARRVAL(row));
1595 while (i++ < offset) {
1596 zend_hash_move_forward(Z_ARRVAL(row));
1597 zend_hash_get_current_data(Z_ARRVAL(row), (void **)&entry);
1598 }
1599
1600 zend_hash_get_current_data(Z_ARRVAL(row), (void **)&entry);
1601
1602 *return_value = **entry;
1603 zval_copy_ctor(return_value);
1604 Z_SET_REFCOUNT_P(return_value, 1);
1605 zval_dtor(&row);
1606
1607 DBG_VOID_RETURN;
1608 }
1609 /* }}} */
1610
1611
1612 MYSQLND_CLASS_METHODS_START(mysqlnd_res)
1613 NULL, /* fetch_row */
1614 mysqlnd_fetch_row_buffered,
1615 mysqlnd_fetch_row_unbuffered,
1616 MYSQLND_METHOD(mysqlnd_res, use_result),
1617 MYSQLND_METHOD(mysqlnd_res, store_result),
1618 MYSQLND_METHOD(mysqlnd_res, fetch_into),
1619 MYSQLND_METHOD(mysqlnd_res, fetch_row_c),
1620 MYSQLND_METHOD(mysqlnd_res, fetch_all),
1621 MYSQLND_METHOD(mysqlnd_res, fetch_field_data),
1622 MYSQLND_METHOD(mysqlnd_res, num_rows),
1623 MYSQLND_METHOD(mysqlnd_res, num_fields),
1624 MYSQLND_METHOD(mysqlnd_res, skip_result),
1625 MYSQLND_METHOD(mysqlnd_res, data_seek),
1626 MYSQLND_METHOD(mysqlnd_res, field_seek),
1627 MYSQLND_METHOD(mysqlnd_res, field_tell),
1628 MYSQLND_METHOD(mysqlnd_res, fetch_field),
1629 MYSQLND_METHOD(mysqlnd_res, fetch_field_direct),
1630 MYSQLND_METHOD(mysqlnd_res, fetch_fields),
1631 MYSQLND_METHOD(mysqlnd_res, read_result_metadata),
1632 NULL, /* fetch_lengths */
1633 MYSQLND_METHOD(mysqlnd_res, store_result_fetch_data),
1634 MYSQLND_METHOD(mysqlnd_res, initialize_result_set_rest),
1635 MYSQLND_METHOD(mysqlnd_res, free_result_buffers),
1636 MYSQLND_METHOD(mysqlnd_res, free_result),
1637
1638 mysqlnd_internal_free_result, /* free_result_internal */
1639 mysqlnd_internal_free_result_contents, /* free_result_contents */
1640 MYSQLND_METHOD(mysqlnd_res, free_buffered_data),
1641 MYSQLND_METHOD(mysqlnd_res, unbuffered_free_last_data),
1642
1643 NULL /* row_decoder */,
1644 mysqlnd_result_meta_init
1645 MYSQLND_CLASS_METHODS_END;
1646
1647
1648 /* {{{ mysqlnd_result_init */
1649 PHPAPI MYSQLND_RES *
mysqlnd_result_init(unsigned int field_count,zend_bool persistent TSRMLS_DC)1650 mysqlnd_result_init(unsigned int field_count, zend_bool persistent TSRMLS_DC)
1651 {
1652 size_t alloc_size = sizeof(MYSQLND_RES) + mysqlnd_plugin_count() * sizeof(void *);
1653 MYSQLND_RES *ret = mnd_pecalloc(1, alloc_size, persistent);
1654
1655 DBG_ENTER("mysqlnd_result_init");
1656
1657 if (!ret) {
1658 DBG_RETURN(NULL);
1659 }
1660
1661 ret->persistent = persistent;
1662 ret->field_count = field_count;
1663 ret->m = *mysqlnd_result_get_methods();
1664
1665 DBG_RETURN(ret);
1666 }
1667 /* }}} */
1668
1669
1670 /*
1671 * Local variables:
1672 * tab-width: 4
1673 * c-basic-offset: 4
1674 * End:
1675 * vim600: noet sw=4 ts=4 fdm=marker
1676 * vim<600: noet sw=4 ts=4
1677 */
1678