From 1b1a4b6293b0746dfae537990e7a83804fd4c3eb Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Sun, 10 Mar 2024 17:33:41 -0700 Subject: [PATCH 1/5] fix(joinURL): handle segments with `../` --- src/utils.ts | 14 +++++++++++--- test/join.test.ts | 7 +++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 0bbe92d..ef8b736 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -14,6 +14,7 @@ const PROTOCOL_RELATIVE_REGEX = /^([/\\]\s*){2,}[^/\\]/; const PROTOCOL_SCRIPT_RE = /^[\s\0]*(blob|data|javascript|vbscript):$/i; const TRAILING_SLASH_RE = /\/$|\/\?|\/#/; const JOIN_LEADING_SLASH_RE = /^\.?\//; +const JOIN_LAST_SEGMENT_RE = /\/[^/]*\/?$/; /** * Check if a path starts with `./` or `../`. @@ -323,9 +324,16 @@ export function joinURL(base: string, ...input: string[]): string { for (const segment of input.filter((url) => isNonEmptyURL(url))) { if (url) { - // TODO: Handle .. when joining - const _segment = segment.replace(JOIN_LEADING_SLASH_RE, ""); - url = withTrailingSlash(url) + _segment; + let _segment = segment; + _segment = _segment.replace(JOIN_LEADING_SLASH_RE, ""); + while (url.length > 0 && _segment.startsWith("../")) { + url = url.replace(JOIN_LAST_SEGMENT_RE, ""); + _segment = _segment.slice(3).replace(JOIN_LEADING_SLASH_RE, ""); + } + url = + !url && _segment.startsWith("../") + ? _segment + : withTrailingSlash(url) + _segment; } else { url = segment; } diff --git a/test/join.test.ts b/test/join.test.ts index e7c400b..9d65582 100644 --- a/test/join.test.ts +++ b/test/join.test.ts @@ -10,6 +10,13 @@ describe("joinURL", () => { { input: ["/a"], out: "/a" }, { input: ["a", "b"], out: "a/b" }, { input: ["/", "/b"], out: "/b" }, + { input: ["/a", "../b"], out: "/b" }, + { input: ["../a", "../b"], out: "../b" }, + { input: ["../a", "./../b"], out: "../b" }, + { input: ["../a/", "../b"], out: "../b" }, + { input: ["/a/b/c", "../../d"], out: "/a/d" }, + { input: ["/c", "../../d"], out: "../d" }, + { input: ["/c", ".././../d"], out: "../d" }, { input: ["a", "b/", "c"], out: "a/b/c" }, { input: ["a", "b/", "/c"], out: "a/b/c" }, { input: ["/", "./"], out: "/" }, From 3730b60fdfb5408d32e9ae00f81e235349bb003d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 11 Mar 2024 05:23:27 -0700 Subject: [PATCH 2/5] test: add example of existing relative behaviour --- test/join.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/join.test.ts b/test/join.test.ts index 9d65582..15e4eb8 100644 --- a/test/join.test.ts +++ b/test/join.test.ts @@ -7,6 +7,7 @@ describe("joinURL", () => { { input: ["/"], out: "/" }, // eslint-disable-next-line unicorn/no-null { input: [null, "./"], out: "./" }, + { input: ["./", "a"], out: "./a" }, { input: ["/a"], out: "/a" }, { input: ["a", "b"], out: "a/b" }, { input: ["/", "/b"], out: "/b" }, From 7efd963b97750f28b54eedaa9be38a346012321d Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 11 Mar 2024 05:36:12 -0700 Subject: [PATCH 3/5] test: add another existing relative example --- test/join.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/join.test.ts b/test/join.test.ts index 15e4eb8..03295ff 100644 --- a/test/join.test.ts +++ b/test/join.test.ts @@ -8,6 +8,7 @@ describe("joinURL", () => { // eslint-disable-next-line unicorn/no-null { input: [null, "./"], out: "./" }, { input: ["./", "a"], out: "./a" }, + { input: ["./a", "./b"], out: "./a/b" }, { input: ["/a"], out: "/a" }, { input: ["a", "b"], out: "a/b" }, { input: ["/", "/b"], out: "/b" }, From 16dfa77fd685e68b29bba536b47e8737eaad30d6 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 11 Mar 2024 05:54:43 -0700 Subject: [PATCH 4/5] fix: handle non-absolute bases --- src/utils.ts | 5 +++-- test/join.test.ts | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 8f66333..158e0b3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -14,7 +14,7 @@ const PROTOCOL_RELATIVE_REGEX = /^([/\\]\s*){2,}[^/\\]/; const PROTOCOL_SCRIPT_RE = /^[\s\0]*(blob|data|javascript|vbscript):$/i; const TRAILING_SLASH_RE = /\/$|\/\?|\/#/; const JOIN_LEADING_SLASH_RE = /^\.?\//; -const JOIN_LAST_SEGMENT_RE = /\/[^/]*\/?$/; +const JOIN_LAST_SEGMENT_RE = /(^|\/)[^/]*\/?$/; /** * Check if a path starts with `./` or `../`. @@ -324,6 +324,7 @@ export function joinURL(base: string, ...input: string[]): string { for (const segment of input.filter((url) => isNonEmptyURL(url))) { if (url) { + const hasAbsoluteBase = url.startsWith("/"); let _segment = segment; _segment = _segment.replace(JOIN_LEADING_SLASH_RE, ""); while (url.length > 0 && _segment.startsWith("../")) { @@ -331,7 +332,7 @@ export function joinURL(base: string, ...input: string[]): string { _segment = _segment.slice(3).replace(JOIN_LEADING_SLASH_RE, ""); } url = - !url && _segment.startsWith("../") + !url && (!hasAbsoluteBase || _segment.startsWith("../")) ? _segment : withTrailingSlash(url) + _segment; } else { diff --git a/test/join.test.ts b/test/join.test.ts index 03295ff..0135200 100644 --- a/test/join.test.ts +++ b/test/join.test.ts @@ -15,6 +15,9 @@ describe("joinURL", () => { { input: ["/a", "../b"], out: "/b" }, { input: ["../a", "../b"], out: "../b" }, { input: ["../a", "./../b"], out: "../b" }, + { input: ["../a", "./../../b"], out: "b" }, + { input: ["../a", "../../../b"], out: "../b" }, + { input: ["../a", "../../../../b"], out: "../../b" }, { input: ["../a/", "../b"], out: "../b" }, { input: ["/a/b/c", "../../d"], out: "/a/d" }, { input: ["/c", "../../d"], out: "../d" }, From deee078eb781f6f19cdf267fc24c21346f53036b Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 11 Mar 2024 05:56:43 -0700 Subject: [PATCH 5/5] perf: use non capturing group --- src/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.ts b/src/utils.ts index 158e0b3..9e3345e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -14,7 +14,7 @@ const PROTOCOL_RELATIVE_REGEX = /^([/\\]\s*){2,}[^/\\]/; const PROTOCOL_SCRIPT_RE = /^[\s\0]*(blob|data|javascript|vbscript):$/i; const TRAILING_SLASH_RE = /\/$|\/\?|\/#/; const JOIN_LEADING_SLASH_RE = /^\.?\//; -const JOIN_LAST_SEGMENT_RE = /(^|\/)[^/]*\/?$/; +const JOIN_LAST_SEGMENT_RE = /(?:^|\/)[^/]*\/?$/; /** * Check if a path starts with `./` or `../`.