Skip to content

Fix Windows drive letter normalization in hostless file: URLs - #1147

Open
lenamonj wants to merge 1 commit into
servo:mainfrom
lenamonj:fix/file-drive-letter-hostless
Open

Fix Windows drive letter normalization in hostless file: URLs#1147
lenamonj wants to merge 1 commit into
servo:mainfrom
lenamonj:fix/file-drive-letter-hostless

Conversation

@lenamonj

@lenamonj lenamonj commented Aug 9, 2026

Copy link
Copy Markdown

Fixes #889.

Url::parse("file:///C|/hello/world") returns file:///C|/hello/world. Per path state the | has to be rewritten: "If url's scheme is 'file', url's path is empty, and buffer is a Windows drive letter, then replace the second code point in buffer with U+003A (:)". The expected output is file:///C:/hello/world.

The repo already records the same defect from the other direction: <file:///w|/m> sits in url/tests/expected_failures.txt, and the WPT entry for it expects file:///w:/m.

Cause

parse_path implements the spec's "url's path is empty" test as segment_start == path_start + 1, assuming path[0] begins one byte after path_start. That holds when the serialization carries a single slash there, but a file: URL can end up with a run of them: for file:///w|/m, parse_path_start finds file:// already ending in / and returns without adding one, so parse_path appends its own.

Instrumenting the check on main shows it directly:

input file:///w|/m
  serialization="file:////"    path_start=7 segment_start=8 segment=""   -> test passes
  serialization="file:////w|/" path_start=7 segment_start=9 segment="w|" -> test fails

The only segment the test accepts is the empty one; the drive letter arrives a slash later and is never rewritten. The spellings that do work today (file:/C|/a, file://host/C|/a) carry exactly one slash at path_start, so the test holds there; the failing shapes are the ones that put a run of slashes at path_start, with the hostless file:/// form the common case.

Fix

Derive where path[0] actually starts instead of assuming path_start + 1.

Verification

Removing <file:///w|/m> from url/tests/expected_failures.txt is the regression test: the harness grades both directions, so the line fails the suite without the fix and fails it again as an unexpected success if left in place with the fix.

On main with only that line removed, cargo test -p url --test url_wpt fails:

<file:///w|/m> ... ❌
  err:  expected href "file:///w:/m", but got "file:///w|/m"

With this change it passes, and the case from #889 returns file:///C:/hello/world. Also green on x86_64-pc-windows-msvc, rustc 1.90.0: cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test, cargo test --features "url/serde,url/expose_internals", cargo test --no-default-features --features=alloc.

Behaviour before and after over a grid of 410 file: inputs and join pairs: the only differences are drive letters spelled | normalized to : when they form the first path segment. The neighbouring WPT cases file:///w|m and file:///w||m, which must keep the |, are unchanged, as are file:/C|/a and file://host/C|/a.

parse_path implemented the spec's "url's path is empty" test as
segment_start == path_start + 1, which assumes path[0] begins one byte
after path_start. A hostless file: URL carries a run of slashes there,
because parse_path_start finds "file://" already ending in a slash and
returns without adding one, so the drive letter in file:///C|/a was
never recognised and the | was never rewritten to :.

Derive where path[0] starts instead of assuming it.

Removes <file:///w|/m> from url/tests/expected_failures.txt, which the
WPT harness now passes.

Fixes servo#889.
lenamonj added a commit to lenamonj/jeffy-loop that referenced this pull request Aug 9, 2026
servo/rust-url at 00a6ce5, still the tip of upstream main and itself a
drive-letter fix merged nine days before the run. Three runs of 30
iterations against a pre-registered budget of five. 20 findings closed,
10 High, none declined; shipped-code change 6 files, +642/-103; all 19
surface-inventory rows swept.

The run's own headline is a claim it kept taking back: make_relative was
declared class-complete three times and withdrawn three times, twice by
the evaluator gate and once by the run's own closing audit, each time
because the enumeration behind the claim could not express a shape that
still failed. It held at the fourth attempt.

The oracle integrity check the target brief specified before iteration 1
is run in the receipt: both vendored WPT corpora byte-identical to
pristine upstream by blob hash, wpt.rs unchanged, zero lines added to
expected_failures.txt and one removed, the same single #[ignore] before
and after. The allowlist moved 67 to 66 in the only direction it can.

One finding went upstream as servo/rust-url#1147, which closes their #889,
open since 2023. It is the only one of the twenty with evidence outside
the loop's own tests, and the receipt says so rather than implying the
other nineteen were held back for another reason.

ATTEMPTS.md also corrects a stale number of its own: the sqlparse entry
said six evaluator rejections where that receipt was corrected to seven.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect parsing of Windows drive letter quirk

1 participant