url-parse-nginx 0.2.4

A faithful, 1-to-1 Rust port of nginx's URL path parser (ngx_http_parse_uri / ngx_http_parse_complex_uri) for origin-form paths.
Documentation
# url-parse-nginx

[![Crates.io](https://img.shields.io/crates/v/url-parse-nginx.svg)](https://crates.io/crates/url-parse-nginx)
[![docs.rs](https://docs.rs/url-parse-nginx/badge.svg)](https://docs.rs/url-parse-nginx)

A faithful, 1-to-1 Rust port of nginx's URL **path normalization**.

`url-parse-nginx` reproduces exactly what nginx does when it turns a raw
request path into the normalized `$uri`: percent-decoding (`%XX`), resolution
of `.` and `..` segments, and `//` collapsing. It is a close port of two
functions from nginx's `src/http/ngx_http_parse.c`:

- `ngx_http_parse_uri()` — validates an origin-form path and detects whether
  normalization is needed.
- `ngx_http_parse_complex_uri()` — performs the normalization.

The goal is **byte-for-byte agreement with nginx**, so that Rust code (proxies,
routers, WAFs, security tooling) can reason about a path the same way nginx
will. Agreement is not just claimed but continuously checked by a differential
fuzzer that runs the real nginx C code against this port (see below).

## Scope

- **Origin-form paths only** (starting with `/`) — i.e. the semantics of the
  HTTP/2 and HTTP/3 `:path` pseudo-header. Absolute-form (`http://host/path`),
  authority-form (`CONNECT`), and `OPTIONS *` are out of scope.
- Targets the Linux, non-debug build of nginx (`NGX_WIN32` / `NGX_DEBUG` off).
- Supports `no_std` environments with `alloc`. Normalized paths allocate only
  when their bytes differ from the input.

## Usage

```rust
use std::borrow::Cow;
use url_parse_nginx::parse_origin_form;

// `parse_origin_form(target, merge_slashes)` parses an origin-form request target.
// `merge_slashes = true` matches nginx's default `merge_slashes on`.
// The result is a `Parsed { path: Cow<[u8]>, args: Option<&[u8]> }`.
// Deref the path (&*) to compare against a byte slice.
assert_eq!(&*parse_origin_form(b"/a/./b/../c", true).unwrap().path, b"/a/c");
assert_eq!(&*parse_origin_form(b"/hello%20world", true).unwrap().path, b"/hello world");
assert_eq!(&*parse_origin_form(b"/a//b", true).unwrap().path, b"/a/b");
assert_eq!(&*parse_origin_form(b"/a//b", false).unwrap().path, b"/a//b");

// The path corresponds to nginx's `$uri`; the query is returned separately in `args`,
// corresponding to nginx's `$args`.
let n = parse_origin_form(b"/foo/../bar?x=1", true).unwrap();
assert_eq!(&*n.path, b"/bar");
assert_eq!(n.args.unwrap(), b"x=1");

// No query component -> args is None.
assert_eq!(parse_origin_form(b"/foo", true).unwrap().args, None);

// A "simple" path that needs no normalization borrows the input — no allocation.
assert!(matches!(parse_origin_form(b"/foo/bar", true).unwrap().path, Cow::Borrowed(_)));

// Paths nginx rejects return Err (e.g. escaping above the root).
assert!(parse_origin_form(b"/../", true).is_err());
```

The default `percent-encoding` feature provides `PATH_ESCAPE_SET` and enables
the optional `percent-encoding` dependency. Disable it when only parsing is
needed:

```toml
url-parse-nginx = { version = "...", default-features = false }
```

`parse_origin_form` returns:

- `Ok(Parsed { path, args })`:
  - `path: Cow<[u8]>` — the normalized path corresponding to nginx's initial
    `$uri`, with the query string excluded. A path that needs no normalization
    borrows the input unchanged (`Cow::Borrowed`) with no allocation; a
    normalized path returns an owned buffer (`Cow::Owned`).
  - `args: Option<&[u8]>` — the query string corresponding to nginx's initial
    `$args`: the bytes after the first `?`, up to a `#` fragment or the end of
    the target. It always borrows the input and is never normalized. `None`
    means nginx found no query arguments; `Some(b"")` marks the empty query
    before a fragment in a target such as `/a?#f`.
- `Err(ParseError)` — the request target could not be parsed.

## How the equivalence is verified

The repository contains developer tooling (not shipped in the published crate):

- `nginx-reference/` — a tiny C shared object that calls the **real** nginx
  `ngx_http_parse_uri` / `ngx_http_parse_complex_uri`. The C is generated
  verbatim from a pinned official nginx release fetched from nginx.org by
  `tools/extract.sh`.
- `fuzz/` — a differential fuzzer that feeds the same inputs to nginx (C)
  and to this crate and asserts identical results (normalized path, query
  string including absent versus present-but-empty, and accept/reject), across
  both `merge_slashes` values. It also checks `PATH_ESCAPE_SET` against nginx's
  percent-encoder for all 256 possible byte values.
- `bench/` — a micro-benchmark comparing this crate's parse speed against the
  real nginx C code (see below).

```sh
# exhaustive corpus ("/", "/"+1..3 arbitrary bytes, "/"+4 printable bytes) + random inputs
cd fuzz
cargo run --release -- <iterations> <seed>
```

Before random generation, the fuzzer exhaustively checks `/` followed by every
1-, 2-, and 3-byte suffix (~16.7M cases), and then `/` followed by every 4-byte
suffix of printable ASCII (`0x20`..=`0x7e`, 95^4 ≈ 81.5M cases). Random fuzzing
then extends coverage to longer inputs.

## Benchmark

`bench/` times the Rust port against the real nginx C code on long (~1 KiB)
URLs, chosen so that the fixed per-call allocation cost does not dominate the
measured parse time. It cross-checks that C and Rust agree on each input before
timing, then reports the best (minimum) average ns/op over several rounds.

```sh
cd bench
cargo run --release
```

Both sides are compiled at `-O3` for a fair comparison. Four inputs exercise the
fast path (no normalization → the Rust port borrows the input) and the three
normalization paths (`%XX` decoding, `.`/`..` resolution, `//` merging).

One representative run on an AMD Ryzen 9 5900X (x86-64) with rustc 1.97.1
produced:

| case | bytes | C ns/op | Rust ns/op | speedup |
|---|---:|---:|---:|---:|
| simple (no normalization) | 1001 | 561.9 | 517.3 | 1.09x |
| percent-decode | 1001 | 1945.7 | 2242.5 | 0.87x |
| dot-dot resolution | 1000 | 1928.2 | 2779.9 | 0.69x |
| slash merge | 1000 | 1823.7 | 2264.6 | 0.81x |

`speedup` is C ns/op divided by Rust ns/op, so values above `1.0x` mean the
Rust port is faster. These figures are illustrative: absolute timings and
ratios vary with the CPU, compiler version, system load, and code layout. Run
the benchmark locally when comparing changes.

## Relationship to nginx and license

This crate is a derivative work of nginx and reuses nginx source (a Rust port
in `src/lib.rs`, and verbatim C in the fuzz harness). nginx is licensed under
the 2-clause BSD license, so this crate is distributed under the same license
and retains the original nginx copyright notice.

See [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE) for details, and
`nginx-reference/tools/extract.sh` for the exact pinned nginx version.