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/.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/postbuild/entities.js b/packages/kit/src/core/postbuild/entities.js index 91888a95b73e..05fe2072d29e 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) + : '\ufffd'; + }) .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) { 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]' },