# urlparse-rs
A Rust port of Python's [`urllib.parse`](https://docs.python.org/3/library/urllib.parse.html) —
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
```rust
use urlparse_rs::{quote, unquote, urljoin, urlsplit};
// Percent-encode / decode (Python's quote defaults safe to "/").
assert_eq!(quote("/El Niño/", "/"), "/El%20Ni%C3%B1o/");
assert_eq!(unquote("/El%20Ni%C3%B1o/"), "/El Niño/");
// Split a URL and resolve a relative reference.
let s = urlsplit("http://host:8080/p?q#f", "", true).unwrap();
assert_eq!(s.hostname().as_deref(), Some("host"));
assert_eq!(s.port(), Ok(Some(8080)));
assert_eq!(urljoin("http://a/b/c/d", "../g", true).unwrap(), "http://a/b/g");
```
## Installation
```sh
cargo add urlparse-rs
```
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`](https://crates.io/crates/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):
| `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](LICENSE-MIT).