Skip to content

fix(url): remove dot segments and fold special-scheme separators - #13195

Merged
proggeramlug merged 3 commits into
mainfrom
fix/url-dot-segments
Oct 24, 2026
Merged

proggeramlug merged 3 commits into
mainfrom
fix/url-dot-segments

Conversation

@TheHypnoo

@TheHypnoo TheHypnoo commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Summary

new URL() kept . and .. path segments in pathname/href (https://x/a/b/../c reported /a/b/../c, Node reports /a/c), and the same parse pass was missing the rest of the WHATWG special-scheme shape — \ separators, the optional //, file: slash states and drive letters. Both land together because they are one rule family: the path/authority states of the standard's parser.

Changes

  • crates/perry-runtime/src/url/parse.rs
    • normalize_hierarchical_path: WHATWG path-state rules over a path string. Single-dot segments (., %2e) are dropped, double-dot (.., .%2e, %2e., %2e%2e) shorten the path, a dot segment in final position appends an empty segment (/a/.. → /, /a/b/.. → /a/), empty segments from a // run survive (//a/../b → //b), a segment that merely contains a dot is untouched (/a/.../b). file:-only: a Windows drive letter canonicalizes to : (C| → C:) and a path that is only a drive letter is never shortened (file:///C:/.. → /C:/).
    • parse_url applies it to the hierarchical branches only; opaque paths (mailto:, data:) keep their text.
    • Special-scheme shape: \ folds to / before the host is read (query/fragment keep theirs, a non-special path keeps them, a non-special host may not contain one); a special scheme is always hierarchical with an optional // and a skipped separator run (https:x, https:/x, https:///x host x); file: keeps its own slash states (extra separators are path, empty host allowed) and now canonicalizes its host like any other special scheme (file://SERVER/x → server).
    • is_valid_absolute_url mirrors the parser instead of requiring a literal //, and no longer rejects an opaque URL with nothing after the colon (mailto: is valid, as in Node).
  • crates/perry-runtime/src/url/url_class.rs: js_url_set_pathname runs the same states (assignment clears the path and re-parses the value); the duplicated is_special_url_scheme now lives in parse.rs only.
  • test-parity/node-suite/url/url/dot-segments.ts and .../backslash-and-separators.ts: new node-parity cases.
  • changelog.d/12864-url-dot-segments.md: changelog fragment.

Known divergences found while probing but not addressed here (separate defects, candidates for their own issues): the constructor does not apply the WHATWG path percent-encode set (/a b<c vs Node /a%20b%3Cc), url.domainToASCII("a/b") returns a/b where Node returns a, and the pathname setter stores a value on an opaque URL where Node ignores the assignment.

Related issue

Closes #12864

Test plan

  • cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static — clean (the perry compile link surface used for the runs below, plus scripts/gc_call_effects/sidecars.sh target/release)
  • PERRY_SKIP_BUILD=1 ./run_parity_tests.sh --suite node-suite --module url — 69/69 PASS (67 pre-existing + the 2 new cases), no previously-passing case changed
  • New cases print byte-identical output to node --experimental-strip-types (diff empty, 64 and 43 lines)
  • The issue's reproduction: perry compile prints 0 /a/c /c, node prints 0 /a/c /c
  • Full cargo test --workspace … — not run locally (crate-scoped tests plus the module parity suite instead); CI's cargo-test job is the backstop
  • (if user-facing) Added the node-parity cases under test-parity/node-suite/url/url/ — the url module's own suite, which is where Node-core module behavior is pinned
  • (if CLI / stdlib / runtime API changed) Updated docs/src/ — no dedicated node:url page exists to update; the behavior is pinned by the node-parity suite
  • (if touching a platform UI backend) n/a
  • cargo fmt --check -p perry-runtime clean; no new clippy warnings in the touched files

Screenshots / output

$ perry compile url.ts -o url && ./url     # node url.ts
0 /a/c /c                                  # 0 /a/c /c

Related URL test corpus (test-files/*url*.ts plus four adjacent cases) shows no new failures: the only two non-passing entries pre-date this change (test_gap_11519_net_url_short_strings.ts cannot compile locally without libperry_ext_net.a, and test_gap_6375_legacy_url_parse_format.ts differs only on domainToASCII/domainToUnicode lines).

Checklist

  • I have NOT bumped the workspace version or edited CLAUDE.md / CHANGELOG.md (maintainer handles these at merge)
  • My commits follow the loose feat: / fix: / docs: / chore: prefix convention used in the log
  • I've read CONTRIBUTING.md and agree to the Code of Conduct

`new URL()` kept `.` and `..` path segments in `pathname`/`href` because
`parse_url` never had the WHATWG path-state pass: it split the authority off
and stored the remaining text verbatim (#12864).

Add `normalize_hierarchical_path`, which applies the path-shape rules the
standard's path state contributes: single-dot (`.`, `%2e`) segments are
dropped, double-dot (`..`, `.%2e`, `%2e.`, `%2e%2e`) segments shorten the
path, a dot segment in final position appends an empty segment instead
(`/a/..` is `/`, `/a/b/..` is `/a/`), empty segments from a `//` run survive,
and the `file:`-only rules hold (a Windows drive letter canonicalizes to `:`,
so `C|` becomes `C:`, and a path that is only a drive letter is never
shortened). Opaque paths (`mailto:`, `data:`) keep their text.

The same parse pass was missing the rest of the special-scheme shape, so it
lands with it: `\` is a path separator and authority terminator for special
schemes (folded before the host is read, while query/fragment keep theirs and
a non-special host may not contain one), the `//` after a special scheme is
optional with its separator run skipped (`https:x`, `https:/x`, `https:///x`
all host `x`), `file:` keeps its own slash states (extra separators are path,
empty host allowed) and now canonicalizes its host like any other special
scheme. `is_valid_absolute_url` mirrors all of that instead of requiring a
literal `//`, and no longer rejects an opaque URL with nothing after the colon.
`js_url_set_pathname` runs the same states, since assignment clears the path
and re-parses the value.

Validated against the pinned Node oracle:
- `RUST_TEST_THREADS=1 cargo test --release -p perry-runtime --lib url::` — 25 passed
- `PERRY_SKIP_BUILD=1 ./run_parity_tests.sh --suite node-suite --module url` — 69/69 PASS
- new node-parity cases print byte-identical output to Node
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Caution

Review failed

The pull request is closed.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Drop the Rust unit tests for the path rules (the node-parity cases already
cover them), merge the two path helpers into one entry point, detect the
special scheme before the fold so the `scheme_slashes_consumed` flag and its
post-fold authority check disappear, and keep only the comments that explain
a spec quirk the code cannot show.
@TheHypnoo
TheHypnoo marked this pull request as ready for review October 24, 2026 17:17
@proggeramlug
proggeramlug merged commit 704c3cb into main Oct 24, 2026
71 of 72 checks passed
@proggeramlug
proggeramlug deleted the fix/url-dot-segments branch October 24, 2026 17:17
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.

URL: dot segments (. and ..) are not removed from the pathname

2 participants