chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { detectBadJsonStrings } from "~/utils/detectBadJsonStrings";
|
||||
|
||||
describe("detectBadJsonStrings", () => {
|
||||
it("should not detect valid JSON string", () => {
|
||||
const goodJson = `{"title": "hello"}`;
|
||||
const result = detectBadJsonStrings(goodJson);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should detect incomplete Unicode escape sequences", () => {
|
||||
const badJson = `{"title": "hello\\ud835"}`;
|
||||
const result = detectBadJsonStrings(badJson);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should not detect complete Unicode escape sequences", () => {
|
||||
const goodJson = `{"title": "hello\\ud835\\udc00"}`;
|
||||
const result = detectBadJsonStrings(goodJson);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should detect incomplete low surrogate", () => {
|
||||
const badJson = `{"title": "hello\\udc00"}`;
|
||||
const result = detectBadJsonStrings(badJson);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle multiple Unicode sequences correctly", () => {
|
||||
const goodJson = `{"title": "hello\\ud835\\udc00\\ud835\\udc01"}`;
|
||||
const result = detectBadJsonStrings(goodJson);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should detect mixed complete and incomplete sequences", () => {
|
||||
const badJson = `{"title": "hello\\ud835\\udc00\\ud835"}`;
|
||||
const result = detectBadJsonStrings(badJson);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should have acceptable performance overhead", () => {
|
||||
const longText = `hello world `.repeat(1_000);
|
||||
const goodJson = `{"title": "hello", "text": "${longText}"}`;
|
||||
const badJson = `{"title": "hello\\ud835", "text": "${longText}"}`;
|
||||
|
||||
const iterations = 100_000;
|
||||
|
||||
// Warm up
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
detectBadJsonStrings(goodJson);
|
||||
detectBadJsonStrings(badJson);
|
||||
}
|
||||
|
||||
// Measure good JSON (most common case)
|
||||
const goodStart = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
detectBadJsonStrings(goodJson);
|
||||
}
|
||||
const goodTime = performance.now() - goodStart;
|
||||
|
||||
// Measure bad JSON (edge case)
|
||||
const badStart = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
detectBadJsonStrings(badJson);
|
||||
}
|
||||
const badTime = performance.now() - badStart;
|
||||
|
||||
// Measure baseline (just function call overhead)
|
||||
const baselineStart = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
// Empty function call to measure baseline
|
||||
}
|
||||
const baselineTime = performance.now() - baselineStart;
|
||||
|
||||
const goodOverhead = goodTime - baselineTime;
|
||||
const badOverhead = badTime - baselineTime;
|
||||
|
||||
console.log(`Baseline (${iterations} iterations): ${baselineTime.toFixed(2)}ms`);
|
||||
console.log(
|
||||
`Good JSON (${iterations} iterations): ${goodTime.toFixed(
|
||||
2
|
||||
)}ms (overhead: ${goodOverhead.toFixed(2)}ms)`
|
||||
);
|
||||
console.log(
|
||||
`Bad JSON (${iterations} iterations): ${badTime.toFixed(
|
||||
2
|
||||
)}ms (overhead: ${badOverhead.toFixed(2)}ms)`
|
||||
);
|
||||
console.log(
|
||||
`Average per call - Good: ${(goodOverhead / iterations).toFixed(4)}ms, Bad: ${(
|
||||
badOverhead / iterations
|
||||
).toFixed(4)}ms`
|
||||
);
|
||||
|
||||
// Assertions for performance expectations
|
||||
// Good JSON should be reasonably fast (most common case)
|
||||
expect(goodOverhead / iterations).toBeLessThan(0.01); // Less than 10 microseconds per call
|
||||
|
||||
// Bad JSON can be slower due to regex matching, but still reasonable
|
||||
expect(badOverhead / iterations).toBeLessThan(0.01); // Less than 20 microseconds per call
|
||||
|
||||
// Total overhead for 100k calls should be reasonable
|
||||
expect(goodOverhead).toBeLessThan(1000); // Less than 1 second for 100k calls
|
||||
});
|
||||
|
||||
it("should handle various JSON sizes efficiently", () => {
|
||||
const sizes = [100, 1000, 10000, 100000];
|
||||
const iterations = 10_000;
|
||||
|
||||
for (const size of sizes) {
|
||||
const text = `hello world `.repeat(size / 11); // Approximate size
|
||||
const goodJson = `{"title": "hello", "text": "${text}"}`;
|
||||
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
detectBadJsonStrings(goodJson);
|
||||
}
|
||||
const time = performance.now() - start;
|
||||
|
||||
console.log(
|
||||
`Size ${size} chars (${iterations} iterations): ${time.toFixed(2)}ms (${(
|
||||
time / iterations
|
||||
).toFixed(4)}ms per call)`
|
||||
);
|
||||
|
||||
// Performance should scale reasonably with size
|
||||
expect(time / iterations).toBeLessThan(size / 1000); // Roughly linear scaling
|
||||
}
|
||||
});
|
||||
|
||||
it("should show significant performance improvement with quick rejection", () => {
|
||||
const longText = `hello world `.repeat(1_000);
|
||||
const goodJson = `{"title": "hello", "text": "${longText}"}`;
|
||||
const badJson = `{"title": "hello\\ud835", "text": "${longText}"}`;
|
||||
const noUnicodeJson = `{"title": "hello", "text": "${longText}"}`;
|
||||
|
||||
const iterations = 100_000;
|
||||
|
||||
// Warm up
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
detectBadJsonStrings(goodJson);
|
||||
detectBadJsonStrings(badJson);
|
||||
detectBadJsonStrings(noUnicodeJson);
|
||||
}
|
||||
|
||||
// Test strings with no Unicode escapes (99.9% case)
|
||||
const noUnicodeStart = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
detectBadJsonStrings(noUnicodeJson);
|
||||
}
|
||||
const noUnicodeTime = performance.now() - noUnicodeStart;
|
||||
|
||||
// Test strings with Unicode escapes (0.1% case)
|
||||
const withUnicodeStart = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
detectBadJsonStrings(badJson);
|
||||
}
|
||||
const withUnicodeTime = performance.now() - withUnicodeStart;
|
||||
|
||||
console.log(
|
||||
`No Unicode escapes (${iterations} iterations): ${noUnicodeTime.toFixed(2)}ms (${(
|
||||
noUnicodeTime / iterations
|
||||
).toFixed(4)}ms per call)`
|
||||
);
|
||||
console.log(
|
||||
`With Unicode escapes (${iterations} iterations): ${withUnicodeTime.toFixed(2)}ms (${(
|
||||
withUnicodeTime / iterations
|
||||
).toFixed(4)}ms per call)`
|
||||
);
|
||||
console.log(
|
||||
`Performance ratio: ${(withUnicodeTime / noUnicodeTime).toFixed(
|
||||
2
|
||||
)}x slower for Unicode strings`
|
||||
);
|
||||
|
||||
// Both cases should be extremely fast (under 1 microsecond per call)
|
||||
expect(noUnicodeTime / iterations).toBeLessThan(0.001); // Less than 1 microsecond
|
||||
expect(withUnicodeTime / iterations).toBeLessThan(0.001); // Less than 1 microsecond
|
||||
|
||||
// The difference should be reasonable (not more than 5x)
|
||||
expect(noUnicodeTime / withUnicodeTime).toBeLessThan(5);
|
||||
});
|
||||
|
||||
describe("full UTF-16 low-surrogate range coverage (U+DC00–U+DFFF)", () => {
|
||||
// Regression guard: a previous version of this scanner used `[cd]` to
|
||||
// match the low-surrogate nibble, missing the entire U+DE00–U+DFFF
|
||||
// half of the range. Valid surrogate pairs with low surrogates in that
|
||||
// upper half (which includes most common emoji) were falsely flagged,
|
||||
// and lone surrogates in the upper half were falsely passed.
|
||||
|
||||
it("does NOT flag a valid pair with low surrogate in the c range (U+DC00–U+DCFF)", () => {
|
||||
// 🐍 SNAKE = U+1F40D = 🐍
|
||||
expect(detectBadJsonStrings(`{"s":"\\ud83d\\udc0d"}`)).toBe(false);
|
||||
});
|
||||
|
||||
it("does NOT flag a valid pair with low surrogate in the d range (U+DD00–U+DDFF)", () => {
|
||||
// U+1F540 = 🕀
|
||||
expect(detectBadJsonStrings(`{"s":"\\ud83d\\udd40"}`)).toBe(false);
|
||||
});
|
||||
|
||||
it("does NOT flag a valid pair with low surrogate in the e range (U+DE00–U+DEFF)", () => {
|
||||
// 😀 GRINNING FACE = U+1F600 = 😀 — previously false-flagged
|
||||
expect(detectBadJsonStrings(`{"s":"\\ud83d\\ude00"}`)).toBe(false);
|
||||
});
|
||||
|
||||
it("does NOT flag a valid pair with low surrogate in the f range (U+DF00–U+DFFF)", () => {
|
||||
// U+1F700 = 🜀 — previously false-flagged
|
||||
expect(detectBadJsonStrings(`{"s":"\\ud83d\\udf00"}`)).toBe(false);
|
||||
});
|
||||
|
||||
it("flags a lone low surrogate in the e range (\\uDE00)", () => {
|
||||
// Previously this was NOT flagged because the forward scan only
|
||||
// recognised low surrogates with third nibble === "c" || "d".
|
||||
expect(detectBadJsonStrings(`{"s":"prefix \\ude00 suffix"}`)).toBe(true);
|
||||
});
|
||||
|
||||
it("flags a lone low surrogate in the f range (\\uDFFF)", () => {
|
||||
expect(detectBadJsonStrings(`{"s":"prefix \\udfff suffix"}`)).toBe(true);
|
||||
});
|
||||
|
||||
it("flags a high surrogate followed by something that looks like a low surrogate but is in the e range with a missing prefix", () => {
|
||||
// The previous high-surrogate-then-pair check used `[cd]` for the
|
||||
// matching low surrogate nibble, so any high surrogate followed by
|
||||
// \uDe.. would be falsely flagged as unpaired. Verify the fix works
|
||||
// for the valid case AND still flags genuinely broken inputs.
|
||||
expect(detectBadJsonStrings(`{"s":"\\ud800X"}`)).toBe(true); // truly broken
|
||||
expect(detectBadJsonStrings(`{"s":"\\ud83d\\ude00"}`)).toBe(false); // valid, but used to flag
|
||||
});
|
||||
});
|
||||
|
||||
describe("integration with JSON.stringify", () => {
|
||||
it("does NOT flag JSON.stringify of a valid emoji 😀", () => {
|
||||
// V8 emits the raw character for valid surrogate pairs, so the
|
||||
// fast-path returns false without exercising the regex.
|
||||
expect(detectBadJsonStrings(JSON.stringify("😀"))).toBe(false);
|
||||
});
|
||||
|
||||
it("flags JSON.stringify of a lone high surrogate", () => {
|
||||
expect(detectBadJsonStrings(JSON.stringify("\uD800"))).toBe(true);
|
||||
});
|
||||
|
||||
it("flags JSON.stringify of a lone low surrogate in each of c/d/e/f ranges", () => {
|
||||
expect(detectBadJsonStrings(JSON.stringify("\uDC00"))).toBe(true);
|
||||
expect(detectBadJsonStrings(JSON.stringify("\uDD00"))).toBe(true);
|
||||
expect(detectBadJsonStrings(JSON.stringify("\uDE00"))).toBe(true);
|
||||
expect(detectBadJsonStrings(JSON.stringify("\uDFFF"))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user