urlparse-rs
A Rust port of Python's urllib.parse —
URL component splitting/joining, query-string handling, and percent-encoding. Behaviour is
verified against the reference urllib.parse module itself across thousands of inputs.
Covers urlsplit/urlunsplit, urlparse/urlunparse, urljoin, urldefrag,
parse_qs/parse_qsl, urlencode, and the quote/unquote family, plus the
SplitResult/ParseResult netloc accessors (username/password/hostname/port) and
urlsplit's authority validation — bracketed IPv6/IPvFuture hosts and the NFKC homograph check on
non-ASCII authorities. The deprecated split*/unwrap/bytes-result helpers are intentionally
omitted.
Usage
use ;
// Percent-encode / decode (Python's quote defaults safe to "/").
assert_eq!;
assert_eq!;
// Split a URL and resolve a relative reference.
let s = urlsplit.unwrap;
assert_eq!;
assert_eq!;
assert_eq!;
Installation
Requires a Rust toolchain with 2024-edition support (Rust 1.85 or newer).
How it works
Percent-encoding operates on bytes, exactly like urllib.parse. A str is encoded as UTF-8;
each byte that is neither "always safe" (A-Z a-z 0-9 _ . - ~, the RFC 3986 unreserved set) nor
in the caller's safe set becomes %XX with uppercase hex. Decoding reverses this: only maximal
runs of ASCII characters are %-decoded and UTF-8 decoded as a unit, while non-ASCII characters
pass through unchanged — matching the reference unquote.
URL parsing mirrors urlsplit's structure, including its authority checks: a bracketed host must
be a valid IPv6 or IPvFuture literal, and a non-ASCII authority is rejected when NFKC normalization
would introduce a delimiter (the homograph guard urllib.parse performs via unicodedata). That
NFKC step is the crate's one dependency, unicode-normalization,
because the standard library has no Unicode normalization.
Performance
This is a compiled port, so for cold parsing — each URL seen for the first time — it runs
several times faster than CPython's urllib.parse. But urllib.parse wraps urlsplit in an
lru_cache, so for repeated identical URLs CPython returns a cached result faster than this
crate re-parses from scratch (the crate keeps no cache of its own). Both effects are real; which
one dominates depends entirely on your access pattern.
Indicative figures on one machine, over a corpus of real-world URLs, in nanoseconds per operation (lower is better):
| Operation | urlparse-rs | urllib.parse (cold) |
urllib.parse (cached) |
|---|---|---|---|
quote |
~180 | ~2,140 | — (not cached) |
unquote |
~820 | ~4,890 | — |
urlsplit |
~620 | ~3,740 | ~230 |
urlencode |
~2,570 | ~14,800 | — |
parse_qsl |
~1,190 | ~4,230 | — |
urljoin |
~2,200 | ~7,400 | — |
So roughly 3–12× faster on cold parses, but on a hot loop re-parsing the same URL CPython's cache
wins (~230 ns vs ~620 ns for urlsplit); a caller with that pattern can wrap this crate in its own
memoization. These are release-build microbenchmarks on a single machine with ~10–20% run-to-run
variance — treat them as ballpark, not a guarantee.
License
Licensed under the MIT License.