fix: sanitize lone Unicode surrogates using toWellFormed() (#720)
* fix: sanitize lone Unicode surrogates in snapshot and response serialization (#635) Pages with emoji/special characters can contain lone surrogates (e.g. \uD800 without a matching \uDC00-\uDFFF), causing serde_json to fail with "unexpected end of hex escape" when parsing the JSON response. - Add sanitizeSurrogates() to replace lone surrogates with U+FFFD in ariaSnapshot output - Add sanitizeJsonSurrogates() safety net in serializeResponse for other response fields (page.title, page.content, etc.) - Add tests for both sanitization paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove trivial "no surrogates unchanged" test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: retry flaky Rust test * refactor: remove unnecessary sanitizeSurrogates from snapshot.ts Chromium's ariaSnapshot() converts lone surrogates to literal text (e.g. the 6-char string "\ud800"), not actual surrogate code points. The real fix is sanitizeJsonSurrogates() in protocol.ts which handles eval and other response paths where actual surrogates appear. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: use toWellFormed() instead of regex for lone surrogate sanitization Upgrade tsconfig target/lib from ES2022 to ES2024 and replace the manual regex-based surrogate sanitization with String.prototype.toWellFormed(). This is simpler, more readable, and relies on the standard API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove trivial no-surrogate test Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: hyunjinee <leehj0110@kakao.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
hyunjinee
parent
417428463b
commit
78c9aef3c9
+23
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseCommand } from './protocol.js';
|
||||
import { parseCommand, serializeResponse, successResponse } from './protocol.js';
|
||||
|
||||
// Helper to create command JSON string
|
||||
const cmd = (obj: object) => JSON.stringify(obj);
|
||||
@@ -1412,6 +1412,28 @@ describe('parseCommand', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('serializeResponse - lone surrogate handling', () => {
|
||||
it('should serialize response with lone surrogate to valid JSON', () => {
|
||||
const response = successResponse('1', {
|
||||
snapshot: 'button "test \uD800 click"',
|
||||
});
|
||||
const json = serializeResponse(response);
|
||||
expect(() => JSON.parse(json)).not.toThrow();
|
||||
const parsed = JSON.parse(json);
|
||||
expect(parsed.data.snapshot).toContain('\uFFFD');
|
||||
expect(parsed.data.snapshot).not.toMatch(/[\uD800-\uDFFF]/);
|
||||
});
|
||||
|
||||
it('should preserve valid emoji characters', () => {
|
||||
const response = successResponse('1', {
|
||||
snapshot: 'button "😀 Click"',
|
||||
});
|
||||
const json = serializeResponse(response);
|
||||
const parsed = JSON.parse(json);
|
||||
expect(parsed.data.snapshot).toContain('😀');
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid commands', () => {
|
||||
it('should reject unknown action', () => {
|
||||
const result = parseCommand(cmd({ id: '1', action: 'unknown' }));
|
||||
|
||||
+6
-2
@@ -1149,8 +1149,12 @@ export function errorResponse(id: string, error: string): Response {
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a response to JSON string
|
||||
* Serialize a response to JSON string.
|
||||
* Replaces lone Unicode surrogates with U+FFFD to prevent
|
||||
* serde_json parsing errors on the Rust side.
|
||||
*/
|
||||
export function serializeResponse(response: Response): string {
|
||||
return JSON.stringify(response);
|
||||
return JSON.stringify(response, (_key, value) =>
|
||||
typeof value === 'string' && !value.isWellFormed() ? value.toWellFormed() : value
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"target": "ES2024",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": [
|
||||
"ES2022"
|
||||
"ES2024"
|
||||
],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
|
||||
Reference in New Issue
Block a user