Files
2026-07-13 13:32:57 +08:00

250 lines
9.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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+DC00U+DFFF)", () => {
// Regression guard: a previous version of this scanner used `[cd]` to
// match the low-surrogate nibble, missing the entire U+DE00U+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+DC00U+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+DD00U+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+DE00U+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+DF00U+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);
});
});
});