From d45ad9d25d1de4b1d8de804772e7b1d9d3e6e9c7 Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Sat, 1 Aug 2026 16:18:47 -0400 Subject: [PATCH 1/3] fix unicode escape sequences above ffff --- .changeset/witty-moons-smile.md | 5 +++++ packages/kit/src/core/sync/create_manifest_data/conflict.js | 2 +- packages/kit/src/core/sync/create_manifest_data/index.js | 2 +- .../kit/src/core/sync/create_manifest_data/index.spec.js | 6 ++++-- .../test/samples/encoding/[u+1f600]/+page.svelte | 0 packages/kit/src/utils/routing.js | 2 +- packages/kit/src/utils/routing.spec.js | 5 +++++ 7 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 .changeset/witty-moons-smile.md create mode 100644 packages/kit/src/core/sync/create_manifest_data/test/samples/encoding/[u+1f600]/+page.svelte diff --git a/.changeset/witty-moons-smile.md b/.changeset/witty-moons-smile.md new file mode 100644 index 000000000000..2b665846fe86 --- /dev/null +++ b/.changeset/witty-moons-smile.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: correctly decode `[u+nnnn]` escape sequences above `ffff` diff --git a/packages/kit/src/core/sync/create_manifest_data/conflict.js b/packages/kit/src/core/sync/create_manifest_data/conflict.js index 64a374491e76..279573b6c6f5 100644 --- a/packages/kit/src/core/sync/create_manifest_data/conflict.js +++ b/packages/kit/src/core/sync/create_manifest_data/conflict.js @@ -60,7 +60,7 @@ function normalize_route_id(id) { .replace(/(?<=^|\/)\(.+?\)(?=$|\/)/g, '') .replace(/\[[ux]\+([0-9a-f]+)\]/g, (_, x) => - String.fromCharCode(parseInt(x, 16)).replace(/\//g, '%2f') + String.fromCodePoint(parseInt(x, 16)).replace(/\//g, '%2f') ) // replace `[param]` with `<*>`, `[param=x]` with ``, and `[[param]]` with `` diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js index 2a5782928487..e959a2899194 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.js @@ -135,7 +135,7 @@ function create_routes_and_nodes(cwd, config, fallback) { ); } - return String.fromCharCode(parseInt(code, 16)); + return String.fromCodePoint(parseInt(code, 16)); } }); diff --git a/packages/kit/src/core/sync/create_manifest_data/index.spec.js b/packages/kit/src/core/sync/create_manifest_data/index.spec.js index 27096bfab493..d4db35caf991 100644 --- a/packages/kit/src/core/sync/create_manifest_data/index.spec.js +++ b/packages/kit/src/core/sync/create_manifest_data/index.spec.js @@ -208,6 +208,7 @@ test('succeeds when routes does not exist', () => { test('encodes invalid characters', () => { const { nodes, routes } = create('samples/encoding'); + const emoji = { component: 'samples/encoding/[u+1f600]/+page.svelte' }; const quote = { component: 'samples/encoding/[x+22]/+page.svelte' }; const hash = { component: 'samples/encoding/[x+23]/+page.svelte' }; const question_mark = { component: 'samples/encoding/[x+3f]/+page.svelte' }; @@ -217,6 +218,7 @@ test('encodes invalid characters', () => { expect(nodes.map(simplify_node)).toEqual([ default_layout, default_error, + emoji, quote, hash, question_mark, @@ -225,8 +227,8 @@ test('encodes invalid characters', () => { ]); expect(routes.map((p) => p.pattern.toString())).toEqual( - [/^\/$/, /^\/\]\/?$/, /^\/\[\/?$/, /^\/%3[Ff]\/?$/, /^\/%23\/?$/, /^\/"\/?$/].map((pattern) => - pattern.toString() + [/^\/$/, /^\/\]\/?$/, /^\/\[\/?$/, /^\/%3[Ff]\/?$/, /^\/%23\/?$/, /^\/"\/?$/, /^\/😀\/?$/].map( + (pattern) => pattern.toString() ) ); }); diff --git a/packages/kit/src/core/sync/create_manifest_data/test/samples/encoding/[u+1f600]/+page.svelte b/packages/kit/src/core/sync/create_manifest_data/test/samples/encoding/[u+1f600]/+page.svelte new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/kit/src/utils/routing.js b/packages/kit/src/utils/routing.js index 29d0be38adf7..79caa6ed1c3a 100644 --- a/packages/kit/src/utils/routing.js +++ b/packages/kit/src/utils/routing.js @@ -12,7 +12,7 @@ const escape_sequence_pattern = /\[([ux])\+([^\]]+)\]/; * @param {string} code the sequence without its `[x+`/`[u+` prefix or `]` suffix */ function decode_escape_sequence(code) { - return String.fromCharCode(...code.split('-').map((codepoint) => parseInt(codepoint, 16))); + return String.fromCodePoint(...code.split('-').map((codepoint) => parseInt(codepoint, 16))); } /** diff --git a/packages/kit/src/utils/routing.spec.js b/packages/kit/src/utils/routing.spec.js index 231cd4d8c424..a34d573f9b61 100644 --- a/packages/kit/src/utils/routing.spec.js +++ b/packages/kit/src/utils/routing.spec.js @@ -480,6 +480,11 @@ describe('resolve_route', () => { params: { one: 'one' }, expected: '/A/one' }, + { + route: '/[u+1f600]/[one]', + params: { one: 'one' }, + expected: '/😀/one' + }, { route: '/blog/[one]', params: { one: '[x+2f]' }, From 100005d8e67fa9c8317975c041db12293d0f347d Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Sat, 1 Aug 2026 17:44:23 -0400 Subject: [PATCH 2/3] decode numeric character references correctly --- .changeset/cold-pans-decode.md | 5 +++++ packages/kit/src/core/postbuild/entities.js | 10 ++++++++-- packages/kit/src/core/postbuild/entities.spec.js | 7 ++++++- 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 .changeset/cold-pans-decode.md diff --git a/.changeset/cold-pans-decode.md b/.changeset/cold-pans-decode.md new file mode 100644 index 000000000000..0487f90c957f --- /dev/null +++ b/.changeset/cold-pans-decode.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: decode all numeric character references, including above `ffff`, when crawling prerendered pages diff --git a/packages/kit/src/core/postbuild/entities.js b/packages/kit/src/core/postbuild/entities.js index 91888a95b73e..5622cb1f3241 100644 --- a/packages/kit/src/core/postbuild/entities.js +++ b/packages/kit/src/core/postbuild/entities.js @@ -2236,7 +2236,7 @@ const entities = { 'zwnj;': '‌' }; -const numeric = /&#(x)?([0-9a-f]+);/i; +const numeric = /&#(x)?([0-9a-f]+);/gi; const named = new RegExp( `&(${Object.keys(entities) .sort((a, b) => b.length - a.length) @@ -2247,6 +2247,12 @@ const named = new RegExp( /** @param {string} str */ export function decode(str) { return str - .replace(numeric, (_match, hex, code) => String.fromCharCode(hex ? parseInt(code, 16) : +code)) + .replace(numeric, (_match, hex, code) => { + const codepoint = hex ? parseInt(code, 16) : +code; + // mirror the HTML parser, which replaces invalid numeric references with U+FFFD + return Number.isInteger(codepoint) && codepoint <= 0x10ffff + ? String.fromCodePoint(codepoint) + : '�'; + }) .replace(named, (_match, entity) => entities[entity]); } diff --git a/packages/kit/src/core/postbuild/entities.spec.js b/packages/kit/src/core/postbuild/entities.spec.js index ceaf3f04bed2..7ff0d2b20850 100644 --- a/packages/kit/src/core/postbuild/entities.spec.js +++ b/packages/kit/src/core/postbuild/entities.spec.js @@ -31,7 +31,12 @@ const tests = [ { input: ':', output: ':' }, { input: ':', output: ':' }, { input: '&>', output: '&>' }, - { input: 'id=770&#anchor', output: 'id=770&#anchor' } + { input: 'id=770&#anchor', output: 'id=770&#anchor' }, + { input: '/a?x=1&y=2&z=3', output: '/a?x=1&y=2&z=3' }, + { input: '😀', output: '😀' }, + { input: '😀', output: '😀' }, + { input: '�', output: '�' }, + { input: '&#deaf;', output: '�' } ]; for (const { input, output } of tests) { From 3ba70a780c4783b418557743ef5e63b8912c462f Mon Sep 17 00:00:00 2001 From: Nic Polumeyv <162764842+Nic-Polumeyv@users.noreply.github.com> Date: Sat, 1 Aug 2026 18:00:22 -0400 Subject: [PATCH 3/3] use escape for replacement character --- packages/kit/src/core/postbuild/entities.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kit/src/core/postbuild/entities.js b/packages/kit/src/core/postbuild/entities.js index 5622cb1f3241..05fe2072d29e 100644 --- a/packages/kit/src/core/postbuild/entities.js +++ b/packages/kit/src/core/postbuild/entities.js @@ -2252,7 +2252,7 @@ export function decode(str) { // mirror the HTML parser, which replaces invalid numeric references with U+FFFD return Number.isInteger(codepoint) && codepoint <= 0x10ffff ? String.fromCodePoint(codepoint) - : '�'; + : '\ufffd'; }) .replace(named, (_match, entity) => entities[entity]); }