diff --git a/.github/actions/verify-generated-files/action.yml b/.github/actions/verify-generated-files/action.yml index 79c49dbfcfff..44a315a5ee15 100644 --- a/.github/actions/verify-generated-files/action.yml +++ b/.github/actions/verify-generated-files/action.yml @@ -11,6 +11,7 @@ runs: scripts/gdb/debug_gdb_scripts_gen.php Zend/zend_vm_gen.php ext/tokenizer/tokenizer_data_gen.php + ext/json/gen_json_escape_table.php build/gen_stub.php -f --generate-optimizer-info --verify ext/phar/makestub.php .github/scripts/test-directory-unchanged.sh . diff --git a/ext/json/Makefile.frag b/ext/json/Makefile.frag index 683709aa3ef3..4f4ac9d4b71b 100644 --- a/ext/json/Makefile.frag +++ b/ext/json/Makefile.frag @@ -3,3 +3,8 @@ $(srcdir)/json_scanner.c $(srcdir)/php_json_scanner_defs.h: $(srcdir)/json_scann $(srcdir)/json_parser.tab.c $(srcdir)/json_parser.tab.h: $(srcdir)/json_parser.y @$(YACC) $(YFLAGS) --defines -l $(srcdir)/json_parser.y -o $(srcdir)/json_parser.tab.c + +$(srcdir)/php_json_escape_table.h: $(srcdir)/gen_json_escape_table.php + @if test ! -z "$(PHP)"; then \ + $(PHP) $(srcdir)/gen_json_escape_table.php; \ + fi; diff --git a/ext/json/gen_json_escape_table.php b/ext/json/gen_json_escape_table.php new file mode 100755 index 000000000000..55de918b2fd7 --- /dev/null +++ b/ext/json/gen_json_escape_table.php @@ -0,0 +1,50 @@ +#!/usr/bin/env php += 0x80, which need + // UTF-8 decoding), and the ASCII specials " \ / < > & '. + return $b < 0x20 || $b >= 0x80 + || $b === 0x22 // " + || $b === 0x5c // backslash + || $b === 0x2f // / + || $b === 0x3c // < + || $b === 0x3e // > + || $b === 0x26 // & + || $b === 0x27; // ' +} + +$result = <<<'HEADER' +/* This file was generated by ext/json/gen_json_escape_table.php. + * + * DO NOT EDIT THIS FILE! + * + * Classifies which bytes php_json_escape_string() must escape: control + * characters (< 0x20), non-ASCII bytes (>= 0x80, which need UTF-8 + * decoding), and the ASCII specials " \ / < > & '. + */ + +static const bool php_json_escape_dirty_table[256] = { + +HEADER; + +for ($row = 0; $row < 16; $row++) { + $values = []; + for ($col = 0; $col < 16; $col++) { + $b = $row * 16 + $col; + $values[] = is_dirty($b) ? '1' : '0'; + } + $result .= "\t" . implode(', ', $values) . ",\n"; +} + +$result .= "};\n"; + +file_put_contents(__DIR__ . '/php_json_escape_table.h', $result); +echo "Generated php_json_escape_table.h\n"; diff --git a/ext/json/json_encoder.c b/ext/json/json_encoder.c index b8ae31040c8b..f53a21e92597 100644 --- a/ext/json/json_encoder.c +++ b/ext/json/json_encoder.c @@ -22,6 +22,7 @@ #include "zend_smart_str.h" #include "php_json.h" #include "php_json_encoder.h" +#include "php_json_escape_table.h" #include "zend_portability.h" #include #include "zend_enum.h" @@ -384,12 +385,8 @@ zend_result php_json_escape_string( pos = 0; do { - static const uint32_t charmap[8] = { - 0xffffffff, 0x500080c4, 0x10000000, 0x00000000, - 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff}; - unsigned int us = (unsigned char)s[pos]; - if (EXPECTED(!ZEND_BIT_TEST(charmap, us))) { + if (EXPECTED(!php_json_escape_dirty_table[us])) { pos++; len--; if (len == 0) { diff --git a/ext/json/php_json_escape_table.h b/ext/json/php_json_escape_table.h new file mode 100644 index 000000000000..cdb83727f295 --- /dev/null +++ b/ext/json/php_json_escape_table.h @@ -0,0 +1,27 @@ +/* This file was generated by ext/json/gen_json_escape_table.php. + * + * DO NOT EDIT THIS FILE! + * + * Classifies which bytes php_json_escape_string() must escape: control + * characters (< 0x20), non-ASCII bytes (>= 0x80, which need UTF-8 + * decoding), and the ASCII specials " \ / < > & '. + */ + +static const bool php_json_escape_dirty_table[256] = { + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +}; diff --git a/ext/json/tests/json_encode_byte_classification.phpt b/ext/json/tests/json_encode_byte_classification.phpt new file mode 100644 index 000000000000..52930c7e4ac7 --- /dev/null +++ b/ext/json/tests/json_encode_byte_classification.phpt @@ -0,0 +1,68 @@ +--TEST-- +json_encode() escapes every byte value 0x00-0xFF correctly +--FILE-- +'; + case 0x26: return ($options & JSON_HEX_AMP) ? "\\u0026" : '&'; + case 0x27: return ($options & JSON_HEX_APOS) ? "\\u0027" : "'"; + } + if ($b < 0x20) { + return sprintf('\u%04x', $b); + } + return chr($b); +} + +function check_ascii_range(int $options): void { + for ($b = 0x00; $b < 0x80; $b++) { + $expected = '"' . expected_escape($b, $options) . '"'; + $actual = json_encode(chr($b), $options); + if ($actual !== $expected) { + printf("MISMATCH (options=%d) at byte 0x%02x: expected %s got %s\n", + $options, $b, var_export($expected, true), var_export($actual, true)); + } + } +} + +function check_lone_high_bytes(): void { + /* A single byte >= 0x80 is never valid UTF-8 on its own -- every one + * of these must be rejected as invalid UTF-8, not silently passed + * through unescaped. */ + for ($b = 0x80; $b <= 0xff; $b++) { + $actual = json_encode(chr($b)); + if ($actual !== false) { + printf("MISMATCH at byte 0x%02x: expected false (invalid UTF-8) got %s\n", + $b, var_export($actual, true)); + } + if (json_last_error() !== JSON_ERROR_UTF8) { + printf("MISMATCH at byte 0x%02x: expected JSON_ERROR_UTF8, got error code %d\n", + $b, json_last_error()); + } + } +} + +check_ascii_range(0); +check_ascii_range(JSON_UNESCAPED_SLASHES); +check_ascii_range(JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS); +check_ascii_range(JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_UNESCAPED_SLASHES); +check_lone_high_bytes(); + +echo "Done\n"; +?> +--EXPECT-- +Done