--- pdo_sql_parser.c.orig 2025-07-02 12:59:51 UTC +++ pdo_sql_parser.c @@ -0,0 +1,726 @@ +/* Generated by re2c 3.1 */ +/* + +----------------------------------------------------------------------+ + | Copyright (c) The PHP Group | + +----------------------------------------------------------------------+ + | This source file is subject to version 3.01 of the PHP license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | https://www.php.net/license/3_01.txt | + | If you did not receive a copy of the PHP license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@php.net so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Author: George Schlossnagle | + +----------------------------------------------------------------------+ +*/ + +#include "php.h" +#include "php_pdo_driver.h" +#include "pdo_sql_parser.h" + +static int default_scanner(pdo_scanner_t *s) +{ + const char *cursor = s->cur; + + s->tok = cursor; + + + +{ + YYCTYPE yych; + unsigned int yyaccept = 0; + if ((YYLIMIT - YYCURSOR) < 2) YYFILL(2); + yych = *YYCURSOR; + switch (yych) { + case 0x00: goto yy1; + case '"': goto yy4; + case '\'': goto yy6; + case '-': goto yy7; + case '/': goto yy8; + case ':': goto yy9; + case '?': goto yy10; + default: goto yy2; + } +yy1: + YYCURSOR = YYMARKER; + switch (yyaccept) { + case 0: goto yy5; + case 1: goto yy15; + default: goto yy19; + } +yy2: + ++YYCURSOR; + if (YYLIMIT <= YYCURSOR) YYFILL(1); + yych = *YYCURSOR; + switch (yych) { + case 0x00: + case '"': + case '\'': + case '-': + case '/': + case ':': + case '?': goto yy3; + default: goto yy2; + } +yy3: + { RET(PDO_PARSER_TEXT); } +yy4: + yyaccept = 0; + yych = *(YYMARKER = ++YYCURSOR); + if (yych >= 0x01) goto yy13; +yy5: + { SKIP_ONE(PDO_PARSER_TEXT); } +yy6: + yyaccept = 0; + yych = *(YYMARKER = ++YYCURSOR); + if (yych <= 0x00) goto yy5; + goto yy17; +yy7: + yych = *++YYCURSOR; + switch (yych) { + case '-': goto yy20; + default: goto yy5; + } +yy8: + yych = *++YYCURSOR; + switch (yych) { + case '*': goto yy22; + default: goto yy5; + } +yy9: + yych = *++YYCURSOR; + switch (yych) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + case 'G': + case 'H': + case 'I': + case 'J': + case 'K': + case 'L': + case 'M': + case 'N': + case 'O': + case 'P': + case 'Q': + case 'R': + case 'S': + case 'T': + case 'U': + case 'V': + case 'W': + case 'X': + case 'Y': + case 'Z': + case '_': + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + case 'g': + case 'h': + case 'i': + case 'j': + case 'k': + case 'l': + case 'm': + case 'n': + case 'o': + case 'p': + case 'q': + case 'r': + case 's': + case 't': + case 'u': + case 'v': + case 'w': + case 'x': + case 'y': + case 'z': goto yy23; + case ':': goto yy25; + default: goto yy5; + } +yy10: + yych = *++YYCURSOR; + switch (yych) { + case '?': goto yy27; + default: goto yy11; + } +yy11: + { RET(PDO_PARSER_BIND_POS); } +yy12: + ++YYCURSOR; + if (YYLIMIT <= YYCURSOR) YYFILL(1); + yych = *YYCURSOR; +yy13: + switch (yych) { + case 0x00: goto yy1; + case '"': goto yy14; + default: goto yy12; + } +yy14: + yyaccept = 1; + YYMARKER = ++YYCURSOR; + if (YYLIMIT <= YYCURSOR) YYFILL(1); + yych = *YYCURSOR; + switch (yych) { + case '"': goto yy12; + default: goto yy15; + } +yy15: + { RET(PDO_PARSER_TEXT); } +yy16: + ++YYCURSOR; + if (YYLIMIT <= YYCURSOR) YYFILL(1); + yych = *YYCURSOR; +yy17: + switch (yych) { + case 0x00: goto yy1; + case '\'': goto yy18; + default: goto yy16; + } +yy18: + yyaccept = 2; + YYMARKER = ++YYCURSOR; + if (YYLIMIT <= YYCURSOR) YYFILL(1); + yych = *YYCURSOR; + switch (yych) { + case '\'': goto yy16; + default: goto yy19; + } +yy19: + { RET(PDO_PARSER_TEXT); } +yy20: + ++YYCURSOR; + if (YYLIMIT <= YYCURSOR) YYFILL(1); + yych = *YYCURSOR; + switch (yych) { + case '\n': goto yy21; + default: goto yy20; + } +yy21: + { RET(PDO_PARSER_TEXT); } +yy22: + ++YYCURSOR; + if (YYLIMIT <= YYCURSOR) YYFILL(1); + yych = *YYCURSOR; + switch (yych) { + case '*': goto yy28; + default: goto yy22; + } +yy23: + ++YYCURSOR; + if (YYLIMIT <= YYCURSOR) YYFILL(1); + yych = *YYCURSOR; + switch (yych) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + case 'G': + case 'H': + case 'I': + case 'J': + case 'K': + case 'L': + case 'M': + case 'N': + case 'O': + case 'P': + case 'Q': + case 'R': + case 'S': + case 'T': + case 'U': + case 'V': + case 'W': + case 'X': + case 'Y': + case 'Z': + case '_': + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + case 'g': + case 'h': + case 'i': + case 'j': + case 'k': + case 'l': + case 'm': + case 'n': + case 'o': + case 'p': + case 'q': + case 'r': + case 's': + case 't': + case 'u': + case 'v': + case 'w': + case 'x': + case 'y': + case 'z': goto yy23; + default: goto yy24; + } +yy24: + { RET(PDO_PARSER_BIND); } +yy25: + ++YYCURSOR; + if (YYLIMIT <= YYCURSOR) YYFILL(1); + yych = *YYCURSOR; + switch (yych) { + case ':': goto yy25; + default: goto yy26; + } +yy26: + { RET(PDO_PARSER_TEXT); } +yy27: + ++YYCURSOR; + if (YYLIMIT <= YYCURSOR) YYFILL(1); + yych = *YYCURSOR; + switch (yych) { + case '?': goto yy27; + default: goto yy26; + } +yy28: + ++YYCURSOR; + if (YYLIMIT <= YYCURSOR) YYFILL(1); + yych = *YYCURSOR; + switch (yych) { + case '*': goto yy28; + case '/': goto yy29; + default: goto yy22; + } +yy29: + ++YYCURSOR; + goto yy21; +} + +} + +struct placeholder { + const char *pos; + size_t len; + zend_string *quoted; /* quoted value */ + int bindno; + struct placeholder *next; +}; + +struct custom_quote { + const char *pos; + size_t len; +}; + +static void free_param_name(zval *el) { + zend_string_release(Z_PTR_P(el)); +} + +PDO_API int pdo_parse_params(pdo_stmt_t *stmt, zend_string *inquery, zend_string **outquery) +{ + pdo_scanner_t s; + char *newbuffer; + ptrdiff_t t; + uint32_t bindno = 0; + int ret = 0, escapes = 0; + size_t newbuffer_len; + HashTable *params; + struct pdo_bound_param_data *param; + int query_type = PDO_PLACEHOLDER_NONE; + struct placeholder *placeholders = NULL, *placetail = NULL, *plc = NULL; + int (*scan)(pdo_scanner_t *s); + struct custom_quote custom_quote = {NULL, 0}; + + scan = stmt->dbh->methods->scanner ? stmt->dbh->methods->scanner : default_scanner; + + s.cur = ZSTR_VAL(inquery); + s.end = s.cur + ZSTR_LEN(inquery) + 1; + + /* phase 1: look for args */ + while((t = scan(&s)) != PDO_PARSER_EOI) { + if (custom_quote.pos) { + /* Inside a custom quote */ + if (t == PDO_PARSER_CUSTOM_QUOTE && custom_quote.len == s.cur - s.tok && !strncmp(s.tok, custom_quote.pos, custom_quote.len)) { + /* Matching closing quote found, end custom quoting */ + custom_quote.pos = NULL; + custom_quote.len = 0; + } else if (t == PDO_PARSER_ESCAPED_QUESTION) { + /* An escaped question mark has been used inside a dollar quoted string, most likely as a workaround + * as a single "?" would have been parsed as placeholder, due to the lack of support for dollar quoted + * strings. For now, we emit a deprecation notice, but still process it */ + php_error_docref(NULL, E_DEPRECATED, "Escaping question marks inside dollar quoted strings is not required anymore and is deprecated"); + + goto placeholder; + } + + continue; + } + + if (t == PDO_PARSER_CUSTOM_QUOTE) { + /* Start of a custom quote, keep a reference to search for the matching closing quote */ + custom_quote.pos = s.tok; + custom_quote.len = s.cur - s.tok; + + continue; + } + + if (t == PDO_PARSER_BIND || t == PDO_PARSER_BIND_POS || t == PDO_PARSER_ESCAPED_QUESTION) { + if (t == PDO_PARSER_ESCAPED_QUESTION && stmt->supports_placeholders == PDO_PLACEHOLDER_POSITIONAL) { + /* escaped question marks unsupported, treat as text */ + continue; + } + + if (t == PDO_PARSER_BIND) { + ptrdiff_t len = s.cur - s.tok; + if ((ZSTR_VAL(inquery) < (s.cur - len)) && isalnum(*(s.cur - len - 1))) { + continue; + } + query_type |= PDO_PLACEHOLDER_NAMED; + } else if (t == PDO_PARSER_BIND_POS) { + query_type |= PDO_PLACEHOLDER_POSITIONAL; + } + +placeholder: + plc = emalloc(sizeof(*plc)); + memset(plc, 0, sizeof(*plc)); + plc->next = NULL; + plc->pos = s.tok; + plc->len = s.cur - s.tok; + + if (t == PDO_PARSER_ESCAPED_QUESTION) { + plc->bindno = PDO_PARSER_BINDNO_ESCAPED_CHAR; + plc->quoted = ZSTR_CHAR('?'); + escapes++; + } else { + plc->bindno = bindno++; + } + + if (placetail) { + placetail->next = plc; + } else { + placeholders = plc; + } + placetail = plc; + } + } + + /* did the query make sense to me? */ + if (query_type == (PDO_PLACEHOLDER_NAMED|PDO_PLACEHOLDER_POSITIONAL)) { + /* they mixed both types; punt */ + pdo_raise_impl_error(stmt->dbh, stmt, "HY093", "mixed named and positional parameters"); + ret = -1; + goto clean_up; + } + + params = stmt->bound_params; + if (stmt->supports_placeholders == PDO_PLACEHOLDER_NONE && params && bindno != zend_hash_num_elements(params)) { + /* extra bit of validation for instances when same params are bound more than once */ + if (query_type != PDO_PLACEHOLDER_POSITIONAL && bindno > zend_hash_num_elements(params)) { + int ok = 1; + for (plc = placeholders; plc; plc = plc->next) { + if ((param = zend_hash_str_find_ptr(params, plc->pos, plc->len)) == NULL) { + ok = 0; + break; + } + } + if (ok) { + goto safe; + } + } + pdo_raise_impl_error(stmt->dbh, stmt, "HY093", "number of bound variables does not match number of tokens"); + ret = -1; + goto clean_up; + } + + if (!placeholders) { + /* nothing to do; good! */ + return 0; + } + + if (stmt->supports_placeholders == query_type && !stmt->named_rewrite_template) { + /* query matches native syntax */ + if (escapes) { + newbuffer_len = ZSTR_LEN(inquery); + goto rewrite; + } + + ret = 0; + goto clean_up; + } + + if (query_type == PDO_PLACEHOLDER_NAMED && stmt->named_rewrite_template) { + /* magic/hack. + * We we pretend that the query was positional even if + * it was named so that we fall into the + * named rewrite case below. Not too pretty, + * but it works. */ + query_type = PDO_PLACEHOLDER_POSITIONAL; + } + +safe: + /* what are we going to do ? */ + if (stmt->supports_placeholders == PDO_PLACEHOLDER_NONE) { + /* query generation */ + + newbuffer_len = ZSTR_LEN(inquery); + + /* let's quote all the values */ + for (plc = placeholders; plc && params; plc = plc->next) { + if (plc->bindno == PDO_PARSER_BINDNO_ESCAPED_CHAR) { + /* escaped character */ + continue; + } + + if (query_type == PDO_PLACEHOLDER_NONE) { + continue; + } + + if (query_type == PDO_PLACEHOLDER_POSITIONAL) { + param = zend_hash_index_find_ptr(params, plc->bindno); + } else { + param = zend_hash_str_find_ptr(params, plc->pos, plc->len); + } + if (param == NULL) { + /* parameter was not defined */ + ret = -1; + pdo_raise_impl_error(stmt->dbh, stmt, "HY093", "parameter was not defined"); + goto clean_up; + } + if (stmt->dbh->methods->quoter) { + zval *parameter; + if (Z_ISREF(param->parameter)) { + parameter = Z_REFVAL(param->parameter); + } else { + parameter = ¶m->parameter; + } + if (param->param_type == PDO_PARAM_LOB && Z_TYPE_P(parameter) == IS_RESOURCE) { + php_stream *stm; + + php_stream_from_zval_no_verify(stm, parameter); + if (stm) { + zend_string *buf; + + buf = php_stream_copy_to_mem(stm, PHP_STREAM_COPY_ALL, 0); + if (!buf) { + buf = ZSTR_EMPTY_ALLOC(); + } + + plc->quoted = stmt->dbh->methods->quoter(stmt->dbh, buf, param->param_type); + + if (buf) { + zend_string_release_ex(buf, 0); + } + if (plc->quoted == NULL) { + /* bork */ + ret = -1; + strncpy(stmt->error_code, stmt->dbh->error_code, 6); + goto clean_up; + } + + } else { + pdo_raise_impl_error(stmt->dbh, stmt, "HY105", "Expected a stream resource"); + ret = -1; + goto clean_up; + } + } else { + enum pdo_param_type param_type = param->param_type; + zend_string *buf = NULL; + + /* assume all types are nullable */ + if (Z_TYPE_P(parameter) == IS_NULL) { + param_type = PDO_PARAM_NULL; + } + + switch (param_type) { + case PDO_PARAM_BOOL: + plc->quoted = zend_is_true(parameter) ? ZSTR_CHAR('1') : ZSTR_CHAR('0'); + break; + + case PDO_PARAM_INT: + plc->quoted = zend_long_to_str(zval_get_long(parameter)); + break; + + case PDO_PARAM_NULL: + plc->quoted = ZSTR_KNOWN(ZEND_STR_NULL); + break; + + default: { + buf = zval_try_get_string(parameter); + /* parameter does not have a string representation, buf == NULL */ + if (EG(exception)) { + /* bork */ + ret = -1; + strncpy(stmt->error_code, stmt->dbh->error_code, 6); + goto clean_up; + } + + plc->quoted = stmt->dbh->methods->quoter(stmt->dbh, buf, param_type); + } + } + + if (buf) { + zend_string_release_ex(buf, 0); + } + } + } else { + zval *parameter; + if (Z_ISREF(param->parameter)) { + parameter = Z_REFVAL(param->parameter); + } else { + parameter = ¶m->parameter; + } + plc->quoted = zend_string_copy(Z_STR_P(parameter)); + } + newbuffer_len += ZSTR_LEN(plc->quoted); + } + +rewrite: + /* allocate output buffer */ + *outquery = zend_string_alloc(newbuffer_len, 0); + newbuffer = ZSTR_VAL(*outquery); + + /* and build the query */ + const char *ptr = ZSTR_VAL(inquery); + plc = placeholders; + + do { + t = plc->pos - ptr; + if (t) { + memcpy(newbuffer, ptr, t); + newbuffer += t; + } + if (plc->quoted) { + memcpy(newbuffer, ZSTR_VAL(plc->quoted), ZSTR_LEN(plc->quoted)); + newbuffer += ZSTR_LEN(plc->quoted); + } else { + memcpy(newbuffer, plc->pos, plc->len); + newbuffer += plc->len; + } + ptr = plc->pos + plc->len; + + plc = plc->next; + } while (plc); + + t = ZSTR_VAL(inquery) + ZSTR_LEN(inquery) - ptr; + if (t) { + memcpy(newbuffer, ptr, t); + newbuffer += t; + } + *newbuffer = '\0'; + ZSTR_LEN(*outquery) = newbuffer - ZSTR_VAL(*outquery); + + ret = 1; + goto clean_up; + + } else if (query_type == PDO_PLACEHOLDER_POSITIONAL) { + /* rewrite ? to :pdoX */ + const char *tmpl = stmt->named_rewrite_template ? stmt->named_rewrite_template : ":pdo%d"; + int bind_no = 1; + + newbuffer_len = ZSTR_LEN(inquery); + + if (stmt->bound_param_map == NULL) { + ALLOC_HASHTABLE(stmt->bound_param_map); + zend_hash_init(stmt->bound_param_map, 13, NULL, free_param_name, 0); + } + + for (plc = placeholders; plc; plc = plc->next) { + int skip_map = 0; + zend_string *p; + zend_string *idxbuf; + + if (plc->bindno == PDO_PARSER_BINDNO_ESCAPED_CHAR) { + continue; + } + + zend_string *name = zend_string_init(plc->pos, plc->len, 0); + + /* check if bound parameter is already available */ + if (zend_string_equals_literal(name, "?") || (p = zend_hash_find_ptr(stmt->bound_param_map, name)) == NULL) { + idxbuf = zend_strpprintf(0, tmpl, bind_no++); + } else { + idxbuf = zend_string_copy(p); + skip_map = 1; + } + + plc->quoted = idxbuf; + newbuffer_len += ZSTR_LEN(plc->quoted); + + if (!skip_map && stmt->named_rewrite_template) { + /* create a mapping */ + zend_hash_update_ptr(stmt->bound_param_map, name, zend_string_copy(plc->quoted)); + } + + /* map number to name */ + zend_hash_index_update_ptr(stmt->bound_param_map, plc->bindno, zend_string_copy(plc->quoted)); + + zend_string_release(name); + } + + goto rewrite; + + } else { + /* rewrite :name to ? */ + + newbuffer_len = ZSTR_LEN(inquery); + + if (stmt->bound_param_map == NULL) { + ALLOC_HASHTABLE(stmt->bound_param_map); + zend_hash_init(stmt->bound_param_map, 13, NULL, free_param_name, 0); + } + + for (plc = placeholders; plc; plc = plc->next) { + zend_string *name = zend_string_init(plc->pos, plc->len, 0); + zend_hash_index_update_ptr(stmt->bound_param_map, plc->bindno, name); + plc->quoted = ZSTR_CHAR('?'); + newbuffer_len -= plc->len - 1; + } + + goto rewrite; + } + +clean_up: + + while (placeholders) { + plc = placeholders; + placeholders = plc->next; + if (plc->quoted) { + zend_string_release_ex(plc->quoted, 0); + } + efree(plc); + } + + return ret; +}