Skip to content

[fix] revert #2354 and fix double character decoding a different way #2435

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Sep 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hot-taxis-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[fix] revert #2354 and fix double character decoding a different way
16 changes: 15 additions & 1 deletion packages/kit/src/core/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,21 @@ async function build_server(
};
}

const d = decodeURIComponent;
// input has already been decoded by decodeURI
// now handle the rest that decodeURIComponent would do
const d = s => s
.replace(/%23/g, '#')
.replace(/%3[Bb]/g, ';')
.replace(/%2[Cc]/g, ',')
.replace(/%2[Ff]/g, '/')
.replace(/%3[Ff]/g, '?')
.replace(/%3[Aa]/g, ':')
.replace(/%40/g, '@')
.replace(/%26/g, '&')
.replace(/%3[Dd]/g, '=')
.replace(/%2[Bb]/g, '+')
.replace(/%24/g, '$');

const empty = () => ({});

const manifest = {
Expand Down
33 changes: 24 additions & 9 deletions packages/kit/src/core/create_manifest_data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import mime from 'mime';
import { posixify } from '../utils.js';
import glob from 'tiny-glob/sync.js';

/** @typedef {{
/**
* A portion of a file or directory name where the name has been split into
* static and dynamic parts
* @typedef {{
* content: string;
* dynamic: boolean;
* spread: boolean;
* }} Part */

/** @typedef {{
* }} Part
* @typedef {{
* basename: string;
* ext: string;
* parts: Part[],
Expand All @@ -19,7 +21,8 @@ import glob from 'tiny-glob/sync.js';
* is_index: boolean;
* is_page: boolean;
* route_suffix: string
* }} Item */
* }} Item
*/

const specials = new Set(['__layout', '__layout.reset', '__error']);

Expand Down Expand Up @@ -389,10 +392,22 @@ function get_pattern(segments, add_trailing_slash) {
.map((part) => {
return part.dynamic
? '([^/]+?)'
: encodeURIComponent(part.content.normalize()).replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&'
);
: // allow users to specify characters on the file system in an encoded manner
part.content
.normalize()
// We use [ and ] to denote parameters, so users must encode these on the file
// system to match against them. We don't decode all characters since others
// can already be epressed and so that '%' can be easily used directly in filenames
.replace(/%5[Bb]/g, '[')
.replace(/%5[Dd]/g, ']')
// '#', '/', and '?' can only appear in URL path segments in an encoded manner.
// They will not be touched by decodeURI so need to be encoded here, so
// that we can match against them.
// We skip '/' since you can't create a file with it on any OS
.replace(/#/g, '%23')
.replace(/\?/g, '%3F')
// escape characters that have special meaning in regex
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
})
.join('');
})
Expand Down
5 changes: 1 addition & 4 deletions packages/kit/src/core/create_manifest_data/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,20 +132,18 @@ test('creates routes with layout', () => {
]);
});

test('encoding of characters', () => {
test('encodes invalid characters', () => {
const { components, routes } = create('samples/encoding');

// had to remove ? and " because windows

// const quote = 'samples/encoding/".svelte';
const hash = 'samples/encoding/#.svelte';
const potato = 'samples/encoding/土豆.svelte';
// const question_mark = 'samples/encoding/?.svelte';

assert.equal(components, [
layout,
error,
potato,
// quote,
hash
// question_mark
Expand All @@ -154,7 +152,6 @@ test('encoding of characters', () => {
assert.equal(
routes.map((p) => p.pattern),
[
/^\/%E5%9C%9F%E8%B1%86\/?$/,
// /^\/%22\/?$/,
/^\/%23\/?$/
// /^\/%3F\/?$/
Expand Down
20 changes: 18 additions & 2 deletions packages/kit/src/core/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,15 +231,31 @@ function get_params(array) {
// src/routes/[x]/[y]/[z]/svelte, create a function
// that turns a RegExpExecArray into ({ x, y, z })

// input has already been decoded by decodeURI
// now handle the rest that decodeURIComponent would do
const d = /** @param {string} s */ (s) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks the same like in build - maybe put it into a common file for both to use? utils/encoding.js or something like that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it is the same, but I'm not sure it's that easy to share code between the two because the one in build is a string that gets written out to a file and isn't normal code. I think I could still do it, but would probably need to setup a new file to be output in the Rollup config as well. I'm just not sure it's worth it at the moment given all the other stuff there is to do though I do agree it'd be nicer

s
.replace(/%23/g, '#')
.replace(/%3[Bb]/g, ';')
.replace(/%2[Cc]/g, ',')
.replace(/%2[Ff]/g, '/')
.replace(/%3[Ff]/g, '?')
.replace(/%3[Aa]/g, ':')
.replace(/%40/g, '@')
.replace(/%26/g, '&')
.replace(/%3[Dd]/g, '=')
.replace(/%2[Bb]/g, '+')
.replace(/%24/g, '$');

/** @param {RegExpExecArray} match */
const fn = (match) => {
/** @type {Record<string, string>} */
const params = {};
array.forEach((key, i) => {
if (key.startsWith('...')) {
params[key.slice(3)] = decodeURIComponent(match[i + 1] || '');
params[key.slice(3)] = d(match[i + 1] || '');
} else {
params[key] = decodeURIComponent(match[i + 1]);
params[key] = d(match[i + 1]);
}
});
return params;
Expand Down
6 changes: 3 additions & 3 deletions packages/kit/src/runtime/client/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -541,8 +541,8 @@ export class Renderer {
* @param {boolean} no_cache
* @returns {Promise<import('./types').NavigationResult | undefined>} undefined if fallthrough
*/
async _load({ route, info: { path, query } }, no_cache) {
const key = `${path}?${query}`;
async _load({ route, info: { path, decoded_path, query } }, no_cache) {
const key = `${decoded_path}?${query}`;

if (!no_cache) {
const cached = this.cache.get(key);
Expand All @@ -552,7 +552,7 @@ export class Renderer {
const [pattern, a, b, get_params] = route;
const params = get_params
? // the pattern is for the route which we've already matched to this path
get_params(/** @type {RegExpExecArray} */ (pattern.exec(path)))
get_params(/** @type {RegExpExecArray} */ (pattern.exec(decoded_path)))
: {};

const changed = this.current.page && {
Expand Down
5 changes: 3 additions & 2 deletions packages/kit/src/runtime/client/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,13 @@ export class Router {
if (this.owns(url)) {
const path = url.pathname.slice(this.base.length) || '/';

const routes = this.routes.filter(([pattern]) => pattern.test(path));
const decoded_path = decodeURI(path);
const routes = this.routes.filter(([pattern]) => pattern.test(decoded_path));

const query = new URLSearchParams(url.search);
const id = `${path}?${query}`;

return { id, routes, path, query };
return { id, routes, path, decoded_path, query };
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/runtime/client/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type NavigationInfo = {
id: string;
routes: CSRRoute[];
path: string;
decoded_path: string;
query: URLSearchParams;
};

Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ export async function respond(incoming, options, state = {}) {
});
}

const decoded = decodeURI(request.path);
for (const route of options.manifest.routes) {
const match = route.pattern.exec(request.path);
const match = route.pattern.exec(decoded);
if (!match) continue;

const response =
Expand Down
10 changes: 10 additions & 0 deletions packages/kit/test/apps/basics/src/routes/encoded/_tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ export default function (test) {
assert.equal(await page.innerHTML('h3'), '/encoded/AC%2fDC: AC/DC');
});

test('visits a route with an encoded bracket', '/encoded/%5b', async ({ page }) => {
assert.equal(await page.innerHTML('h2'), '/encoded/%5b: [');
assert.equal(await page.innerHTML('h3'), '/encoded/%5b: [');
});

test('visits a route with an encoded question mark', '/encoded/%3f', async ({ page }) => {
assert.equal(await page.innerHTML('h2'), '/encoded/%3f: ?');
assert.equal(await page.innerHTML('h3'), '/encoded/%3f: ?');
});

test(
'visits a dynamic route with non-ASCII character',
'/encoded',
Expand Down