diff --git a/src/shared/json-text-structure-limit.test.ts b/src/shared/json-text-structure-limit.test.ts index 2a1112ba772..c86764a1613 100644 --- a/src/shared/json-text-structure-limit.test.ts +++ b/src/shared/json-text-structure-limit.test.ts @@ -37,4 +37,28 @@ describe('JSON text structure admission', () => { }) ).not.toThrow() }) + + it.each([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])('handles a quote preceded by %i backslashes', (count) => { + const content = `"${'\\'.repeat(count)}"[[]]` + const check = () => + assertJsonTextStructureWithinLimits(content, { + structuralTokens: 3, + nestingDepth: 2 + }) + if (count % 2 === 0) { + expect(check).toThrowError(new JsonTextStructureCapacityError('structuralTokens', 3)) + } else { + expect(check).not.toThrow() + } + }) + + it('resumes counting after escaped quotes and long string values', () => { + const content = JSON.stringify({ value: 'ordinary text [{,}] \\" '.repeat(10_000), next: [] }) + expect(() => + assertJsonTextStructureWithinLimits(content, { structuralTokens: 7, nestingDepth: 2 }) + ).not.toThrow() + expect(() => + assertJsonTextStructureWithinLimits(content, { structuralTokens: 6, nestingDepth: 2 }) + ).toThrowError(new JsonTextStructureCapacityError('structuralTokens', 6)) + }) }) diff --git a/src/shared/json-text-structure-limit.ts b/src/shared/json-text-structure-limit.ts index e0ede23db54..f33ecd1e2ca 100644 --- a/src/shared/json-text-structure-limit.ts +++ b/src/shared/json-text-structure-limit.ts @@ -25,23 +25,37 @@ export function assertJsonTextStructureWithinLimits( assertLimit(limits.nestingDepth) let structuralTokens = 0 let depth = 0 - let inString = false - let escaped = false - for (let index = 0; index < content.length; index += 1) { const character = content[index] - if (inString) { - if (escaped) { - escaped = false - } else if (character === '\\') { - escaped = true - } else if (character === '"') { - inString = false - } - continue - } if (character === '"') { - inString = true + let quote = content.indexOf('"', index + 1) + if (quote !== -1) { + // Only an odd backslash run escapes the quote. + let backslashes = 0 + for (let at = quote - 1; at > index && content[at] === '\\'; at -= 1) { + backslashes += 1 + } + if (backslashes % 2 !== 0) { + // Escape-heavy strings use the linear scan to avoid repeated native searches. + let escaped = false + for (quote += 1; quote < content.length; quote += 1) { + if (escaped) { + escaped = false + } else if (content[quote] === '\\') { + escaped = true + } else if (content[quote] === '"') { + break + } + } + if (quote === content.length) { + quote = -1 + } + } + } + if (quote === -1) { + return + } + index = quote continue } if (!isStructuralToken(character)) {