diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index f8c9ad85d811..4cb37cd59b1e 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "eeab04040fa61fd595695980f9d054b7fc0ed855"; +export const WEBKIT_VERSION = "autobuild-preview-pr-460-8da5349b"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/test/js/web/url/url.test.ts b/test/js/web/url/url.test.ts index 41ba8ec9ee37..6b45584e8cd5 100755 --- a/test/js/web/url/url.test.ts +++ b/test/js/web/url/url.test.ts @@ -323,6 +323,121 @@ describe("url", () => { }`); // A path whose first segment merely starts with "." gets no guard. expect(util.inspect(new URL("foo:/.foo"), { showHidden: true })).toContain("pathname_start: 4,"); + // search_start is derived from the pathname's length, so it is only right if the pathname getter keeps the "/.". + const inspected = util.inspect(new URL("foo:/.foo?x"), { showHidden: true }); + expect(inspected).toContain("pathname: '/.foo',"); + expect(inspected).toContain("pathname_start: 4,"); + expect(inspected).toContain("search_start: 9,"); + }); + + // URL Standard section 4.5 step 3: a URL with a null host serializes a path whose first segment is empty with a "/." + // guard in front of it (foo:/.//b), and the pathname getter leaves that guard out. The guard is only ever in front of + // "//"; a first segment that merely starts with "." (foo:/.a, git:/.git/config) is part of the pathname. + describe("pathname of a host-less URL whose first segment starts with a dot", () => { + const pathnames = (inputs: string[]) => Object.fromEntries(inputs.map(input => [input, new URL(input).pathname])); + + it("keeps the dotted first segment", () => { + expect( + pathnames([ + "foo:/.a/b", + "git:/.git/config", + "myapp:/.well-known/assetlinks.json", + "foo:/..a", + "foo:/.a", + "foo:/.a?q#f", + "foo:/.%2Fa", + "foo:/.a//b", + "foo:/./.a", + ]), + ).toEqual({ + "foo:/.a/b": "/.a/b", + "git:/.git/config": "/.git/config", + "myapp:/.well-known/assetlinks.json": "/.well-known/assetlinks.json", + "foo:/..a": "/..a", + "foo:/.a": "/.a", + "foo:/.a?q#f": "/.a", + "foo:/.%2Fa": "/.%2Fa", + "foo:/.a//b": "/.a//b", + "foo:/./.a": "/.a", + }); + }); + + it("still omits the /. guard and normalizes dot segments", () => { + expect( + pathnames([ + "foo:/.//b", + "foo:/..//b", + "foo:/.//.a", + "foo:/./b", + "foo:/.", + "foo://h/.a", + "foo:///.a", + "https://h/.a", + "file:/.a", + ]), + ).toEqual({ + "foo:/.//b": "//b", + "foo:/..//b": "//b", + "foo:/.//.a": "//.a", + "foo:/./b": "/b", + "foo:/.": "/", + "foo://h/.a": "/.a", + "foo:///.a": "/.a", + "https://h/.a": "/.a", + "file:/.a": "/.a", + }); + expect(new URL("foo:/.//b").href).toBe("foo:/.//b"); + expect(new URL("foo:/.a/b").href).toBe("foo:/.a/b"); + }); + + it("resolves relative references against such a URL", () => { + const resolve = (input: string, base: string) => { + const url = new URL(input, base); + return { href: url.href, pathname: url.pathname }; + }; + expect({ + "x against foo:/.a/b": resolve("x", "foo:/.a/b"), + "../y against foo:/.a/b/c": resolve("../y", "foo:/.a/b/c"), + "/.a against foo:/.//b": resolve("/.a", "foo:/.//b"), + "//b against foo:/.a": resolve("//b", "foo:/.a"), + }).toEqual({ + "x against foo:/.a/b": { href: "foo:/.a/x", pathname: "/.a/x" }, + "../y against foo:/.a/b/c": { href: "foo:/.a/y", pathname: "/.a/y" }, + "/.a against foo:/.//b": { href: "foo:/.a", pathname: "/.a" }, + "//b against foo:/.a": { href: "foo://b", pathname: "" }, + }); + }); + + it("setters", () => { + const set = (input: string, property: "pathname" | "host" | "search" | "protocol", value: string) => { + const url = new URL(input); + url[property] = value; + return { href: url.href, host: url.host, pathname: url.pathname }; + }; + expect({ + "foo:/x pathname=/.a/b": set("foo:/x", "pathname", "/.a/b"), + "foo:/.a pathname=/b": set("foo:/.a", "pathname", "/b"), + "foo:/.a pathname=//b": set("foo:/.a", "pathname", "//b"), + "foo:/.//b pathname=/.c": set("foo:/.//b", "pathname", "/.c"), + "foo:/.a search=?x": set("foo:/.a", "search", "?x"), + "foo:/.a protocol=bar": set("foo:/.a", "protocol", "bar"), + "foo:/.a host=h": set("foo:/.a", "host", "h"), + "foo:/.a host=h:1": set("foo:/.a", "host", "h:1"), + "foo:/.//b host=h": set("foo:/.//b", "host", "h"), + "foo:/.//b host=h:1": set("foo:/.//b", "host", "h:1"), + }).toEqual({ + "foo:/x pathname=/.a/b": { href: "foo:/.a/b", host: "", pathname: "/.a/b" }, + "foo:/.a pathname=/b": { href: "foo:/b", host: "", pathname: "/b" }, + "foo:/.a pathname=//b": { href: "foo:/.//b", host: "", pathname: "//b" }, + "foo:/.//b pathname=/.c": { href: "foo:/.c", host: "", pathname: "/.c" }, + "foo:/.a search=?x": { href: "foo:/.a?x", host: "", pathname: "/.a" }, + "foo:/.a protocol=bar": { href: "bar:/.a", host: "", pathname: "/.a" }, + "foo:/.a host=h": { href: "foo://h/.a", host: "h", pathname: "/.a" }, + "foo:/.a host=h:1": { href: "foo://h:1/.a", host: "h:1", pathname: "/.a" }, + "foo:/.//b host=h": { href: "foo://h//b", host: "h", pathname: "//b" }, + "foo:/.//b host=h:1": { href: "foo://h:1//b", host: "h:1", pathname: "//b" }, + }); + }); }); it("works", () => { const inputs = [ diff --git a/test/js/web/urlpattern/urlpattern.test.ts b/test/js/web/urlpattern/urlpattern.test.ts index c2a727ab5b15..854587f14bf1 100644 --- a/test/js/web/urlpattern/urlpattern.test.ts +++ b/test/js/web/urlpattern/urlpattern.test.ts @@ -161,6 +161,81 @@ describe("URLPattern", () => { }); }); + // For a non-special scheme, the pathname is canonicalized by parsing it as the path of a host-less dummy URL and + // reading the pathname back, so a first segment starting with "." must survive that round trip (see the URL tests + // for the "/." guard that a host-less URL serializes in front of a path starting with "//"). + describe("non-special scheme with a pathname whose first segment starts with a dot", () => { + test("constructor string with a host", () => { + const pattern = new URLPattern("myapp://host/.well-known/:file"); + expect(pattern.pathname).toBe("/.well-known/:file"); + expect(pattern.test("myapp://host/.well-known/assetlinks.json")).toBe(true); + expect(pattern.exec("myapp://host/.well-known/assetlinks.json")!.pathname).toEqual({ + input: "/.well-known/assetlinks.json", + groups: { file: "assetlinks.json" }, + }); + expect(pattern.test("myapp://host/well-known/assetlinks.json")).toBe(false); + }); + + test("wildcard after the dotted segment", () => { + const pattern = new URLPattern("git://h/.git/:rest*"); + expect(pattern.pathname).toBe("/.git/:rest*"); + expect(pattern.test("git://h/.git/config")).toBe(true); + expect(pattern.exec("git://h/.git/refs/heads/main")!.pathname.groups).toEqual({ rest: "refs/heads/main" }); + }); + + test("URLPatternInit", () => { + expect({ + wellKnown: new URLPattern({ protocol: "myapp", pathname: "/.well-known/*" }).pathname, + doubleDot: new URLPattern({ protocol: "myapp", pathname: "/..a" }).pathname, + dotSlashSlash: new URLPattern({ protocol: "myapp", pathname: "/.//a" }).pathname, + laterSegment: new URLPattern({ protocol: "myapp", pathname: "/x/.a" }).pathname, + }).toEqual({ + wellKnown: "/.well-known/*", + doubleDot: "/..a", + dotSlashSlash: "//a", + laterSegment: "/x/.a", + }); + const pattern = new URLPattern({ protocol: "foo", pathname: "/.a" }); + expect(pattern.test("foo:/.a")).toBe(true); + expect(pattern.test("foo://h/.a")).toBe(true); + expect(pattern.test({ protocol: "foo", pathname: "/.a" })).toBe(true); + expect(pattern.test("foo:/a")).toBe(false); + }); + + test("baseURL", () => { + expect({ + relativeString: new URLPattern("./c", "foo://h/.a/b").pathname, + relativeInit: new URLPattern({ pathname: "c", baseURL: "foo:/.a/b" }).pathname, + inheritedFromBase: new URLPattern({ baseURL: "foo:/.a/b" }).pathname, + }).toEqual({ + relativeString: "/.a/c", + relativeInit: "/.a/c", + inheritedFromBase: "/.a/b", + }); + }); + + test("the matched input reports the full pathname", () => { + expect(new URLPattern({ protocol: "foo" }).exec("foo:/.a/b")!.pathname).toEqual({ + input: "/.a/b", + groups: { "0": "/.a/b" }, + }); + }); + + test("unaffected patterns", () => { + expect({ + special: new URLPattern("https://h/.well-known/:file").pathname, + noProtocol: new URLPattern({ pathname: "/.well-known/*" }).pathname, + hostAndLaterSegment: new URLPattern("foo://h/x/.b").pathname, + }).toEqual({ + special: "/.well-known/:file", + noProtocol: "/.well-known/*", + hostAndLaterSegment: "/x/.b", + }); + expect(new URLPattern("https://h/.well-known/:file").test("https://h/.well-known/x")).toBe(true); + expect(new URLPattern("foo://h/x/.b").test("foo://h/x/.b")).toBe(true); + }); + }); + describe("hasRegExpGroups", () => { test("match-everything pattern", () => { expect(new URLPattern({}).hasRegExpGroups).toBe(false);