|
| 1 | +// Copyright 2026 the V8 project authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style license that can be |
| 3 | +// found in the LICENSE file. |
| 4 | + |
| 5 | +// Regression test: JSON.parse must not match a hidden-class transition for an |
| 6 | +// escaped property key using the raw (undecoded) source bytes. The builder's |
| 7 | +// fast transition path compared `chars_ + key.start()` over `key.length()` |
| 8 | +// (the *decoded* length), so escaped keys (e.g. "\t", "\uXXXX") collapsed to |
| 9 | +// the byte 0x5C and matched a previously-seen "\\" key, making JSON.parse adopt |
| 10 | +// the wrong key. Transitions are isolate-global, so a prior parse corrupted a |
| 11 | +// later, unrelated parse. |
| 12 | + |
| 13 | +// A single-char escaped key must keep its decoded value even after a sibling |
| 14 | +// "\\" (backslash) key has been parsed on the same prefix map. |
| 15 | +(function TestEscapedKeyNotRenamedByPriorBackslashKey() { |
| 16 | + function canaryKeys() { |
| 17 | + return Object.keys(JSON.parse('{"\\r":1,"\\f":2}')); |
| 18 | + } |
| 19 | + assertArrayEquals(['\r', '\f'], canaryKeys()); |
| 20 | + // Plants a {<prefix>} -> "\" transition in the global transition tree. |
| 21 | + JSON.parse('{"\\r":3,"\\\\":4}'); |
| 22 | + // Must be unaffected: "\f" stays U+000C, not U+005C. |
| 23 | + assertArrayEquals(['\r', '\f'], canaryKeys()); |
| 24 | +})(); |
| 25 | + |
| 26 | +// Multi-character escaped key: decoded length 2 must not be compared against |
| 27 | +// the first two raw bytes ("\" + "r"). |
| 28 | +(function TestMultiCharEscapedKey() { |
| 29 | + function keys() { |
| 30 | + return Object.keys(JSON.parse('{"\\t":1,"\\r\\r":2}')); |
| 31 | + } |
| 32 | + assertArrayEquals(['\t', '\r\r'], keys()); |
| 33 | + JSON.parse('{"\\t":1,"\\\\r":2}'); // plants the literal "\r" (0x5C 0x72) key |
| 34 | + assertArrayEquals(['\t', '\r\r'], keys()); |
| 35 | +})(); |
| 36 | + |
| 37 | +// \uXXXX-escaped single-character key (here decodes to plain 'A'). |
| 38 | +(function TestUnicodeEscapedKey() { |
| 39 | + function keys() { |
| 40 | + return Object.keys(JSON.parse('{"x":1,"\\u0041":2}')); |
| 41 | + } |
| 42 | + assertArrayEquals(['x', 'A'], keys()); |
| 43 | + JSON.parse('{"x":1,"\\\\":2}'); |
| 44 | + assertArrayEquals(['x', 'A'], keys()); |
| 45 | +})(); |
| 46 | + |
| 47 | +// Override/collide: an escaped key must not be renamed onto a real "\" sibling |
| 48 | +// and silently destroy a property (key count must be preserved). |
| 49 | +(function TestNoPropertyDestruction() { |
| 50 | + // Prime the transition state that previously triggered the collapse. |
| 51 | + JSON.parse('{"p":1,"\\\\":0}'); |
| 52 | + JSON.parse('{"p":1,"\\\\":0,"\\t":0}'); |
| 53 | + const o = JSON.parse('{"p":1,"\\t":"a","\\\\":"b"}'); |
| 54 | + assertEquals(3, Object.keys(o).length); |
| 55 | + assertEquals('a', o['\t']); |
| 56 | + assertEquals('b', o['\\']); |
| 57 | +})(); |
| 58 | + |
| 59 | +// Self-contained single document: a later sibling object value must not be |
| 60 | +// corrupted by an earlier one within the same parse. |
| 61 | +(function TestNestedSiblingNotCorrupted() { |
| 62 | + const o = JSON.parse( |
| 63 | + '{"a":{"p":1,"\\\\":9},"b":{"p":1,"\\\\":"A","\\t":7},' + |
| 64 | + '"c":{"p":1,"\\t":7,"\\\\":"B"}}'); |
| 65 | + assertEquals(3, Object.keys(o.c).length); |
| 66 | + assertEquals(7, o.c['\t']); |
| 67 | + assertEquals('B', o.c['\\']); |
| 68 | +})(); |
| 69 | + |
| 70 | +// A plain (unescaped) empty key must still work (regression guard for the |
| 71 | +// empty-key_chars path the fix relies on). |
| 72 | +(function TestEmptyKeyStillWorks() { |
| 73 | + const o = JSON.parse('{"a":1,"":2,"b":3}'); |
| 74 | + assertArrayEquals(['a', '', 'b'], Object.keys(o)); |
| 75 | + assertEquals(2, o['']); |
| 76 | +})(); |
0 commit comments