-
Notifications
You must be signed in to change notification settings - Fork 5k
Bump WebKit: faster URL parser; don't re-run ICU on parser-produced punycode #39273
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| // WPT url/url-constructor.any.js over the vendored urltestdata.json: every non-failure entry must produce the expected | ||
| // href and components, every failure entry must throw. | ||
| import { describe, expect, test } from "bun:test"; | ||
| import { readFileSync } from "node:fs"; | ||
| import { join } from "node:path"; | ||
|
|
||
| type Entry = { | ||
| input: string; | ||
| base?: string | null; | ||
| href?: string; | ||
| failure?: boolean; | ||
| origin?: string; | ||
| protocol?: string; | ||
| username?: string; | ||
| password?: string; | ||
| host?: string; | ||
| hostname?: string; | ||
| port?: string; | ||
| pathname?: string; | ||
| search?: string; | ||
| hash?: string; | ||
| }; | ||
|
|
||
| const fixture = join(import.meta.dir, "../../node/test/fixtures/wpt/url/resources/urltestdata.json"); | ||
| const entries = (JSON.parse(readFileSync(fixture, "utf8")) as (Entry | string)[]).filter( | ||
| (entry): entry is Entry => typeof entry === "object", | ||
| ); | ||
|
|
||
| // url.origin for these does not match the spec yet (parsing does); tracked separately from the parser. | ||
| const knownOriginDeviations = new Set([ | ||
| "ftps:/example.com/", | ||
| "ftps:example.com/", | ||
| "blob:ftp://host/path", | ||
| "blob:ws://example.org/", | ||
| "blob:wss://example.org/", | ||
| ]); | ||
|
|
||
| describe("WPT url-constructor", () => { | ||
| test("fixture is present", () => { | ||
| expect(entries.length).toBeGreaterThan(800); | ||
| }); | ||
|
|
||
| for (const entry of entries) { | ||
| const name = `${JSON.stringify(entry.input)}${entry.base != null ? ` against ${JSON.stringify(entry.base)}` : ""}`; | ||
| test(name, () => { | ||
| const construct = () => (entry.base != null ? new URL(entry.input, entry.base) : new URL(entry.input)); | ||
| if (entry.failure) { | ||
| expect(construct).toThrow(TypeError); | ||
| return; | ||
|
claude[bot] marked this conversation as resolved.
|
||
| } | ||
| const url = construct(); | ||
| expect(url.href).toBe(entry.href); | ||
| if (entry.origin !== undefined && !knownOriginDeviations.has(entry.input)) expect(url.origin).toBe(entry.origin); | ||
| expect(url.protocol).toBe(entry.protocol); | ||
| expect(url.username).toBe(entry.username); | ||
| expect(url.password).toBe(entry.password); | ||
| expect(url.host).toBe(entry.host); | ||
| expect(url.hostname).toBe(entry.hostname); | ||
| expect(url.port).toBe(entry.port); | ||
| expect(url.pathname).toBe(entry.pathname); | ||
| expect(url.search).toBe(entry.search); | ||
| expect(url.hash).toBe(entry.hash); | ||
| }); | ||
| } | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -140,6 +140,41 @@ describe("url", () => { | |||||||||||||||||||||||
| expect(hn.hostname).toBe("xn--s5a.com"); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| it("rejects invalid punycode labels however they are spelled in the input (like Node)", () => { | ||||||||||||||||||||||||
| for (const input of [ | ||||||||||||||||||||||||
| "https://xn--a.com/", | ||||||||||||||||||||||||
| "https://XN--a.com/", | ||||||||||||||||||||||||
| "https://x%6E--a.com/", | ||||||||||||||||||||||||
| "https://x\tn--a.com/", | ||||||||||||||||||||||||
| "https://xn-\n-a/", | ||||||||||||||||||||||||
| "https://xn-\r-a/", | ||||||||||||||||||||||||
| " https://xn--a/", | ||||||||||||||||||||||||
| "https:xn--a/", | ||||||||||||||||||||||||
| "https:\\\\u:p@xn--a\\p", | ||||||||||||||||||||||||
| ]) { | ||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||
| expect(() => new URL(input)).toThrow(TypeError); | ||||||||||||||||||||||||
| expect(URL.canParse(input)).toBe(false); | ||||||||||||||||||||||||
| expect(URL.parse(input)).toBe(null); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| for (const [input, base] of [ | ||||||||||||||||||||||||
| ["/p", "https://x%6E--a.com/"], | ||||||||||||||||||||||||
| ["//xn--a/p", "https://example.com/"], | ||||||||||||||||||||||||
| ["xn--a", "https://example.com/"], | ||||||||||||||||||||||||
| ]) { | ||||||||||||||||||||||||
| if (input === "xn--a") { | ||||||||||||||||||||||||
| // A relative path never supplies a host. | ||||||||||||||||||||||||
| expect(new URL(input, base).href).toBe("https://example.com/xn--a"); | ||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| expect(() => new URL(input, base)).toThrow(TypeError); | ||||||||||||||||||||||||
| expect(URL.canParse(input, base)).toBe(false); | ||||||||||||||||||||||||
| expect(URL.parse(input, base)).toBe(null); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| expect(new URL("https://xn--ls8h.com/?q=%E3%81#xn--a").href).toBe("https://xn--ls8h.com/?q=%E3%81#xn--a"); | ||||||||||||||||||||||||
| expect(new URL("https://\u{1F4A9}.com/p%20q?xn--a").hostname).toBe("xn--ls8h.com"); | ||||||||||||||||||||||||
| expect(new URL("https://\u{1F4A9}.com/xn--a/%41").pathname).toBe("/xn--a/%41"); | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
|
Comment on lines
+173
to
+176
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Add a regression case for the Constructor and parsing assertions do not execute Proposed test update expect(new URL("https://\u{1F4A9}.com/?q=%E3%81#xn--a").href).toBe("https://xn--ls8h.com/?q=%E3%81#xn--a");
expect(new URL("https://\u{1F4A9}.com/p%20q?xn--a").hostname).toBe("xn--ls8h.com");
expect(new URL("https://\u{1F4A9}.com/xn--a/%41").pathname).toBe("/xn--a/%41");
+ const url = new URL("https://example.com/");
+ url.href = "https://x%6E--a.com/";
+ expect(url.href).toBe("https://example.com/");
});Based on learnings, invalid IDNA mutations are dropped during reparsing, so this must assert a no-op instead of a throw. As per coding guidelines, “Every behavioral change must include an automated regression test in the same change.” 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSources: Coding guidelines, Learnings |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| it("prints", () => { | ||||||||||||||||||||||||
| // URL.prototype carries [Symbol.for("nodejs.util.inspect.custom")], so | ||||||||||||||||||||||||
| // Bun.inspect matches node's util.inspect output. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Replace the manual authority scan with parser metadata.
Line 52 through Line 65 reparses untrusted URL syntax with character loops. This can diverge from WebKit URL parsing. It also treats
%orxn--in userinfo as host indicators.Obtain the original host range from the URL parser, or expose that range from the parser. Do not retain a second authority parser here.
As per coding guidelines, “use real parsers instead of prefix stripping or regex heuristics for user input.”
🧰 Tools
🪛 Cppcheck (2.21.0)
[error] 63-63: failed to evaluate
#ifcondition, undefined function-like macro invocation(syntaxError)
🤖 Prompt for AI Agents
Source: Coding guidelines