urlparse-rs 0.1.0

A Rust port of Python's urllib.parse: URL splitting/joining and percent-encoding (quote/unquote).
Documentation
  • Coverage
  • 71.11%
    32 out of 45 items documented6 out of 31 items with examples
  • Size
  • Source code size: 47.3 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 410.2 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 5s Average build duration of successful builds.
  • all releases: 5s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • VoiceLessQ/urlparse-rs
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • VoiceLessQ

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 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

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, 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.