Skip to content

Commit 5c5d8aa

Browse files
committed
yaml guard: charge lone surrogates at their escaped length
serializedStringLength treated surrogate code units as cost 1, but well-formed JSON.stringify escapes a lone surrogate to \uXXXX (six units). Charge lone surrogates as 6 and valid high+low pairs as-is (two units), matching JSON.stringify exactly. Verified against JSON.stringify across plain text, escapes, control chars, lone high/low surrogates, and valid pairs.
1 parent 7846504 commit 5c5d8aa

2 files changed

Lines changed: 27 additions & 4 deletions

File tree

apps/sim/lib/file-parsers/yaml-parser.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,16 @@ describe('assertYamlWithinLimits', () => {
112112
expect(() => assertYamlWithinLimits({ text: escapeHeavy })).toThrow(YamlComplexityError)
113113
expect(escapeHeavy.length).toBeLessThan(64 * 1024 * 1024)
114114
})
115+
116+
it('charges lone surrogates at their escaped length', () => {
117+
// JSON.stringify escapes a lone surrogate to \uXXXX (six units). ~11M of
118+
// them estimate to ~66 MB (> 64 MB) though the raw length is under the cap.
119+
const loneSurrogates = String.fromCharCode(0xd800).repeat(11 * 1024 * 1024)
120+
expect(loneSurrogates.length).toBeLessThan(64 * 1024 * 1024)
121+
expect(() => assertYamlWithinLimits({ text: loneSurrogates })).toThrow(YamlComplexityError)
122+
// A valid surrogate pair (astral char) is emitted as-is, so ~10M of them
123+
// (~20M code units, ~20 MB) stays well under the cap.
124+
const astral = String.fromCodePoint(0x1f600).repeat(10 * 1024 * 1024)
125+
expect(() => assertYamlWithinLimits({ text: astral })).not.toThrow()
126+
})
115127
})

apps/sim/lib/file-parsers/yaml-parser.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,11 @@ export function isYamlComplexityError(error: unknown): error is YamlComplexityEr
5252
/**
5353
* Exact serialized length (in UTF-16 code units — the unit V8 allocates for the
5454
* resulting string) that `JSON.stringify` produces for a string, accounting for
55-
* the escape expansion of quotes, backslashes, and control characters. Computed
56-
* precisely rather than with a flat multiplier so plain text is charged its
57-
* true size (no false rejection of large legitimate documents) while
58-
* escape-heavy strings are charged their real, larger cost (no cap bypass).
55+
* the escape expansion of quotes, backslashes, control characters, and lone
56+
* surrogates. Computed precisely rather than with a flat multiplier so plain
57+
* text is charged its true size (no false rejection of large legitimate
58+
* documents) while escape-heavy strings are charged their real, larger cost
59+
* (no cap bypass).
5960
*/
6061
function serializedStringLength(value: string): number {
6162
let length = 2 // surrounding quotes
@@ -67,6 +68,16 @@ function serializedStringLength(value: string): number {
6768
// \b \t \n \f \r use two-char escapes; other control chars use \uXXXX (six)
6869
length +=
6970
code === 0x08 || code === 0x09 || code === 0x0a || code === 0x0c || code === 0x0d ? 2 : 6
71+
} else if (code >= 0xd800 && code <= 0xdfff) {
72+
// Well-formed JSON.stringify emits a valid high+low surrogate pair as-is
73+
// (two code units) but escapes a lone surrogate to \uXXXX (six).
74+
const next = i + 1 < value.length ? value.charCodeAt(i + 1) : 0
75+
if (code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
76+
length += 2
77+
i++
78+
} else {
79+
length += 6
80+
}
7081
} else {
7182
length += 1
7283
}

0 commit comments

Comments
 (0)