Skip to main content

url_parse_nginx/
lib.rs

1// SPDX-License-Identifier: BSD-2-Clause
2//
3// Copyright (C) 2026 Yusuke Nojima             (Rust port)
4// Copyright (C) 2002-2021 Igor Sysoev          (original nginx code)
5// Copyright (C) 2011-2026 Nginx, Inc.          (original nginx code)
6// All rights reserved.
7//
8// This file is a close, 1-to-1 Rust port of ngx_http_parse_uri() and
9// ngx_http_parse_complex_uri() (and the `usual[]` table) from nginx's
10// src/http/ngx_http_parse.c. It is distributed under the same 2-clause BSD
11// license as nginx; see the LICENSE and NOTICE files at the crate root.
12
13//! Parse and normalize URL paths using nginx semantics.
14//!
15//! `url-parse-nginx` is a one-to-one Rust port of nginx's URI parser
16//! and normalizer. Within the supported scope described below, it matches
17//! nginx's accept/reject decisions and produces byte-for-byte identical
18//! normalized paths and query strings. This equivalence is continuously
19//! checked by differential fuzzing against nginx's C implementation.
20//!
21//! [`parse_origin_form`] accepts an origin-form request target, normalizes
22//! its path, and returns the query string separately. Path normalization
23//! percent-decodes `%XX`, resolves `.` and `..` segments, and optionally merges
24//! adjacent slashes.
25//!
26//! [Origin-form] is the usual HTTP request-target format: a path starting
27//! with `/`, optionally followed by `?` and a query string, such as
28//! `/search?q=rust`.
29//!
30//! Other request-target forms, such as absolute-form
31//! (`http://example.com/path`), authority-form (`example.com:443`), and
32//! asterisk-form (`*`), are not supported.
33//! The parsing behavior follows nginx on Linux; Windows-specific nginx
34//! behavior is not supported.
35//!
36//! [Origin-form]: https://www.rfc-editor.org/rfc/rfc9112.html#section-3.2.1
37//!
38//! # Example
39//!
40//! ```
41//! use url_parse_nginx::parse_origin_form;
42//!
43//! let parsed = parse_origin_form(b"/docs/../hello%20world?x=1", true)?;
44//! assert_eq!(&*parsed.path, b"/hello world"); // ".." resolved, "%20" decoded
45//! assert_eq!(parsed.args, Some(&b"x=1"[..]));
46//! # Ok::<(), url_parse_nginx::ParseError>(())
47//! ```
48
49// Implementation notes:
50//
51// The parser ports two functions from `src/http/ngx_http_parse.c`:
52//
53// * `ngx_http_parse_uri()` — stage 1. Walks an origin-form path and sets the
54//   `complex_uri` / `quoted_uri` / `plus_in_uri` flags and the `args_start` /
55//   `uri_ext` boundaries. It does not modify the path.
56// * `ngx_http_parse_complex_uri()` — stage 2. Decodes `%XX`, resolves `.` /
57//   `..` and collapses `//` (when `merge_slashes` is set), producing the
58//   normalized path.
59//
60// The C code walks raw buffers with `u_char *` cursors. Here:
61//
62// * `p` (the input cursor) is a `usize` index into a `buf: &[u8]`.
63// * `u` (the output cursor) is a `usize` index into `out: &mut [u8]`.
64//   Where nginx lets its pointer walk backwards past the buffer start during
65//   `..` handling, the Rust port uses `checked_sub` and returns the same error.
66// * Pointer fields that C stores as `u_char *` become `usize` offsets. Their
67//   base buffer follows the C code exactly: `args_start` is always an offset
68//   into the input; `uri_ext` is an input offset in stage 1 and an output
69//   offset in stage 2 (it is reset at the top of stage 2, so the two never
70//   interact — same as C).
71// * nginx relies on "there is always at least one readable byte (the LF)
72//   after the URI": stage 2 reads one byte at `uri_end`. The Rust port uses a
73//   checked read that yields `\n` at that position, avoiding an input copy
74//   made solely to materialize the sentinel.
75
76use std::borrow::Cow;
77
78/// nginx's `usual[]` bitmap (`ngx_http_parse.c`), non-`NGX_WIN32` variant.
79///
80/// Bit `1` marks an "ordinary" URI character that needs no special handling.
81const USUAL: [u32; 8] = [
82    0x0000_0000, /* control chars */
83    0x7fff_37d6, /* symbols / digits: excludes SP " # % + / ? etc. */
84    0xffff_ffff, /* @A-Z[\]^_  (0xefffffff under NGX_WIN32) */
85    0x7fff_ffff, /* `a-z{|}~  (DEL excluded) */
86    0xffff_ffff,
87    0xffff_ffff,
88    0xffff_ffff,
89    0xffff_ffff,
90];
91
92/// `usual[ch >> 5] & (1U << (ch & 0x1f))` — is `ch` an ordinary URI byte?
93#[inline]
94fn usual(ch: u8) -> bool {
95    USUAL[(ch >> 5) as usize] & (1u32 << (ch & 0x1f)) != 0
96}
97
98/// Read through stage 2's input cursor, including nginx's trailing LF.
99#[inline(always)]
100fn read_with_lf_sentinel(buf: &[u8], p: usize) -> u8 {
101    buf.get(p).copied().unwrap_or(b'\n')
102}
103
104/// An error returned when a request target cannot be parsed.
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
106pub struct ParseError;
107
108impl std::fmt::Display for ParseError {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        f.write_str("failed to parse request target")
111    }
112}
113
114impl std::error::Error for ParseError {}
115
116/// The result of parsing an origin-form request target.
117///
118/// `path` and `args` correspond to the initial values nginx exposes through
119/// its `$uri` and `$args` variables.
120#[derive(Debug, Clone, PartialEq, Eq)]
121#[non_exhaustive]
122pub struct Parsed<'a> {
123    /// The normalized path, corresponding to nginx's `$uri` variable before
124    /// any later rewrite processing. The query string is excluded.
125    ///
126    /// A path that needs no normalization borrows the input unchanged
127    /// ([`Cow::Borrowed`], no allocation); a normalized path is owned
128    /// ([`Cow::Owned`]).
129    pub path: Cow<'a, [u8]>,
130
131    /// The query string, corresponding to nginx's initial `$args` variable:
132    /// the bytes after the first `?`, up to a `#` fragment or the end of the
133    /// target. It always borrows the input and is not normalized.
134    ///
135    /// `None` means nginx found no query arguments; this includes a trailing
136    /// `?` with nothing after it (`"/a?"`). `Some(b"")` marks the empty query
137    /// before a fragment in a target such as `"/a?#f"`.
138    pub args: Option<&'a [u8]>,
139}
140
141/// A `{len, data-offset}` pair mirroring nginx's `ngx_str_t`. The base of
142/// `data` (input vs output buffer) depends on the field, exactly as in C.
143#[derive(Debug, Default, Clone, Copy)]
144struct NgxStr {
145    len: usize,
146    data: usize,
147}
148
149/// The subset of `ngx_http_request_t` touched by the two ported functions.
150#[derive(Debug, Default)]
151struct Request {
152    // outputs
153    uri: NgxStr,   // len = normalized path length; data = output-buffer offset
154    args: NgxStr,  // data = input offset (query string)
155    exten: NgxStr, // data = output offset (extension)
156
157    uri_ext: Option<usize>,
158    args_start: Option<usize>,
159
160    // flags
161    complex_uri: bool,
162    quoted_uri: bool,
163    plus_in_uri: bool,
164    empty_path_in_uri: bool,
165}
166
167#[derive(Clone, Copy, PartialEq, Eq)]
168enum UriState {
169    Start,
170    AfterSlash,
171    CheckUri,
172    Uri,
173}
174
175#[derive(Clone, Copy, PartialEq, Eq)]
176enum State {
177    Usual,
178    Slash,
179    Dot,
180    DotDot,
181    Quoted,
182    QuotedSecond,
183}
184
185/// Port of `ngx_http_parse_uri()`.
186///
187/// Scans the entire origin-form request target in `buf` and sets flags/offsets.
188/// Returns `Err` where the C returns `NGX_ERROR`.
189#[inline(never)]
190fn ngx_http_parse_uri(r: &mut Request, buf: &[u8]) -> Result<(), ParseError> {
191    let uri_start = 0;
192    let uri_end = buf.len();
193
194    let mut state = UriState::Start;
195    let mut p = uri_start;
196
197    while p != uri_end {
198        let ch = buf[p];
199
200        match state {
201            UriState::Start => {
202                if ch != b'/' {
203                    return Err(ParseError);
204                }
205                state = UriState::AfterSlash;
206            }
207
208            /* check "/.", "//", "%", and "\" (Win32) in URI */
209            UriState::AfterSlash => {
210                if usual(ch) {
211                    state = UriState::CheckUri;
212                } else {
213                    match ch {
214                        b'.' => {
215                            r.complex_uri = true;
216                            state = UriState::Uri;
217                        }
218                        b'%' => {
219                            r.quoted_uri = true;
220                            state = UriState::Uri;
221                        }
222                        b'/' => {
223                            r.complex_uri = true;
224                            state = UriState::Uri;
225                        }
226                        b'?' => {
227                            r.args_start = Some(p + 1);
228                            state = UriState::Uri;
229                        }
230                        b'#' => {
231                            r.complex_uri = true;
232                            state = UriState::Uri;
233                        }
234                        b'+' => {
235                            r.plus_in_uri = true;
236                        }
237                        _ => {
238                            if ch <= 0x20 || ch == 0x7f {
239                                return Err(ParseError);
240                            }
241                            state = UriState::CheckUri;
242                        }
243                    }
244                }
245            }
246
247            /* check "/", "%" and "\" (Win32) in URI */
248            UriState::CheckUri => {
249                if usual(ch) {
250                    // Stay in CheckUri. Ordinary bytes commonly occur in long
251                    // runs; consume the run here instead of redispatching the
252                    // same state for every byte.
253                    p += 1;
254                    while p != uri_end && usual(buf[p]) {
255                        p += 1;
256                    }
257                    continue;
258                } else {
259                    match ch {
260                        b'/' => {
261                            r.uri_ext = None;
262                            state = UriState::AfterSlash;
263                        }
264                        b'.' => {
265                            r.uri_ext = Some(p + 1);
266                        }
267                        b'%' => {
268                            r.quoted_uri = true;
269                            state = UriState::Uri;
270                        }
271                        b'?' => {
272                            r.args_start = Some(p + 1);
273                            state = UriState::Uri;
274                        }
275                        b'#' => {
276                            r.complex_uri = true;
277                            state = UriState::Uri;
278                        }
279                        b'+' => {
280                            r.plus_in_uri = true;
281                        }
282                        _ => {
283                            if ch <= 0x20 || ch == 0x7f {
284                                return Err(ParseError);
285                            }
286                        }
287                    }
288                }
289            }
290
291            /* URI */
292            UriState::Uri => {
293                if usual(ch) {
294                    // stay in Uri
295                } else {
296                    match ch {
297                        b'#' => {
298                            r.complex_uri = true;
299                        }
300                        _ => {
301                            if ch <= 0x20 || ch == 0x7f {
302                                return Err(ParseError);
303                            }
304                        }
305                    }
306                }
307            }
308        }
309
310        p += 1;
311    }
312
313    Ok(())
314}
315
316/// Shared tail of the `done:` label in `ngx_http_parse_complex_uri()`.
317fn finish_done(r: &mut Request, u: usize) -> Result<(), ParseError> {
318    r.uri.len = u;
319
320    if let Some(ext) = r.uri_ext {
321        // C computes a size_t difference that may wrap when u < uri_ext; match
322        // that instead of panicking (exten is not part of the compared path).
323        r.exten.len = u.wrapping_sub(ext);
324        r.exten.data = ext;
325    }
326
327    r.uri_ext = None;
328    Ok(())
329}
330
331/// The `args:` label of `ngx_http_parse_complex_uri()`.
332fn finish_args(r: &mut Request, buf: &[u8], u: usize, mut p: usize) -> Result<(), ParseError> {
333    let uri_end = buf.len();
334
335    while p < uri_end {
336        let c = buf[p];
337        p += 1;
338        if c != b'#' {
339            continue;
340        }
341
342        let args_start = r.args_start.unwrap();
343        r.args.len = (p - 1).wrapping_sub(args_start);
344        r.args.data = args_start;
345        r.args_start = None;
346        break;
347    }
348
349    finish_done(r, u)
350}
351
352/// Port of `ngx_http_parse_complex_uri()`.
353///
354/// Reads the entire request target in `buf` and writes the normalized path into
355/// `out`, setting `r.uri.len`. `out` must have capacity `>= buf.len() + 1`.
356#[inline(never)]
357fn ngx_http_parse_complex_uri(
358    r: &mut Request,
359    buf: &[u8],
360    out: &mut [u8],
361    merge_slashes: bool,
362) -> Result<(), ParseError> {
363    let uri_start = 0;
364    let uri_end = buf.len();
365
366    let mut state = State::Usual;
367    let mut quoted_state = State::Usual;
368    let mut decoded: u8 = 0;
369
370    let mut p = uri_start;
371    let mut u: usize = 0;
372    r.uri_ext = None;
373    r.args_start = None;
374
375    if r.empty_path_in_uri {
376        out[u] = b'/';
377        u += 1;
378    }
379
380    let mut ch = read_with_lf_sentinel(buf, p);
381    p += 1;
382
383    while p <= uri_end {
384        match state {
385            State::Usual => {
386                if usual(ch) {
387                    out[u] = ch;
388                    u += 1;
389                    ch = read_with_lf_sentinel(buf, p);
390                    p += 1;
391                } else {
392                    match ch {
393                        b'/' => {
394                            r.uri_ext = None;
395                            state = State::Slash;
396                            out[u] = ch;
397                            u += 1;
398                        }
399                        b'%' => {
400                            quoted_state = state;
401                            state = State::Quoted;
402                        }
403                        b'?' => {
404                            r.args_start = Some(p);
405                            return finish_args(r, buf, u, p);
406                        }
407                        b'#' => {
408                            return finish_done(r, u);
409                        }
410                        b'.' => {
411                            r.uri_ext = Some(u + 1);
412                            out[u] = ch;
413                            u += 1;
414                        }
415                        b'+' => {
416                            r.plus_in_uri = true;
417                            out[u] = ch;
418                            u += 1;
419                        }
420                        _ => {
421                            out[u] = ch;
422                            u += 1;
423                        }
424                    }
425                    ch = read_with_lf_sentinel(buf, p);
426                    p += 1;
427                }
428            }
429
430            State::Slash => {
431                if usual(ch) {
432                    state = State::Usual;
433                    out[u] = ch;
434                    u += 1;
435                    ch = read_with_lf_sentinel(buf, p);
436                    p += 1;
437                } else {
438                    match ch {
439                        b'/' => {
440                            if !merge_slashes {
441                                out[u] = ch;
442                                u += 1;
443                            }
444                        }
445                        b'.' => {
446                            state = State::Dot;
447                            out[u] = ch;
448                            u += 1;
449                        }
450                        b'%' => {
451                            quoted_state = state;
452                            state = State::Quoted;
453                        }
454                        b'?' => {
455                            r.args_start = Some(p);
456                            return finish_args(r, buf, u, p);
457                        }
458                        b'#' => {
459                            return finish_done(r, u);
460                        }
461                        b'+' => {
462                            r.plus_in_uri = true;
463                            state = State::Usual;
464                            out[u] = ch;
465                            u += 1;
466                        }
467                        _ => {
468                            state = State::Usual;
469                            out[u] = ch;
470                            u += 1;
471                        }
472                    }
473                    ch = read_with_lf_sentinel(buf, p);
474                    p += 1;
475                }
476            }
477
478            State::Dot => {
479                if usual(ch) {
480                    state = State::Usual;
481                    out[u] = ch;
482                    u += 1;
483                    ch = read_with_lf_sentinel(buf, p);
484                    p += 1;
485                } else {
486                    match ch {
487                        b'/' => {
488                            state = State::Slash;
489                            u -= 1;
490                        }
491                        b'.' => {
492                            state = State::DotDot;
493                            out[u] = ch;
494                            u += 1;
495                        }
496                        b'%' => {
497                            quoted_state = state;
498                            state = State::Quoted;
499                        }
500                        b'?' => {
501                            u -= 1;
502                            r.args_start = Some(p);
503                            return finish_args(r, buf, u, p);
504                        }
505                        b'#' => {
506                            u -= 1;
507                            return finish_done(r, u);
508                        }
509                        b'+' => {
510                            r.plus_in_uri = true;
511                            state = State::Usual;
512                            out[u] = ch;
513                            u += 1;
514                        }
515                        _ => {
516                            state = State::Usual;
517                            out[u] = ch;
518                            u += 1;
519                        }
520                    }
521                    ch = read_with_lf_sentinel(buf, p);
522                    p += 1;
523                }
524            }
525
526            State::DotDot => {
527                if usual(ch) {
528                    state = State::Usual;
529                    out[u] = ch;
530                    u += 1;
531                    ch = read_with_lf_sentinel(buf, p);
532                    p += 1;
533                } else {
534                    match ch {
535                        b'/' | b'?' | b'#' => {
536                            // Same backwards scan as nginx's loop, expressed
537                            // over a bounded slice so indexing stays checked.
538                            let start = u.checked_sub(4).ok_or(ParseError)?;
539                            u = out[..=start]
540                                .iter()
541                                .rposition(|&c| c == b'/')
542                                .map(|i| i + 1)
543                                .ok_or(ParseError)?;
544                            if ch == b'?' {
545                                r.args_start = Some(p);
546                                return finish_args(r, buf, u, p);
547                            }
548                            if ch == b'#' {
549                                return finish_done(r, u);
550                            }
551                            state = State::Slash;
552                        }
553                        b'%' => {
554                            quoted_state = state;
555                            state = State::Quoted;
556                        }
557                        b'+' => {
558                            r.plus_in_uri = true;
559                            state = State::Usual;
560                            out[u] = ch;
561                            u += 1;
562                        }
563                        _ => {
564                            state = State::Usual;
565                            out[u] = ch;
566                            u += 1;
567                        }
568                    }
569                    ch = read_with_lf_sentinel(buf, p);
570                    p += 1;
571                }
572            }
573
574            State::Quoted => {
575                r.quoted_uri = true;
576
577                if ch.is_ascii_digit() {
578                    decoded = ch - b'0';
579                    state = State::QuotedSecond;
580                    ch = read_with_lf_sentinel(buf, p);
581                    p += 1;
582                } else {
583                    let c = ch | 0x20;
584                    if (b'a'..=b'f').contains(&c) {
585                        decoded = c - b'a' + 10;
586                        state = State::QuotedSecond;
587                        ch = read_with_lf_sentinel(buf, p);
588                        p += 1;
589                    } else {
590                        return Err(ParseError);
591                    }
592                }
593            }
594
595            State::QuotedSecond => {
596                if ch.is_ascii_digit() {
597                    ch = (decoded << 4) + (ch - b'0');
598
599                    if ch == b'%' || ch == b'#' {
600                        state = State::Usual;
601                        out[u] = ch;
602                        u += 1;
603                        ch = read_with_lf_sentinel(buf, p);
604                        p += 1;
605                    } else if ch == b'\0' {
606                        return Err(ParseError);
607                    } else {
608                        state = quoted_state;
609                        // no advance: the decoded byte is reprocessed
610                    }
611                } else {
612                    let c = ch | 0x20;
613                    if (b'a'..=b'f').contains(&c) {
614                        ch = (decoded << 4) + (c - b'a') + 10;
615
616                        if ch == b'?' {
617                            state = State::Usual;
618                            out[u] = ch;
619                            u += 1;
620                            ch = read_with_lf_sentinel(buf, p);
621                            p += 1;
622                        } else {
623                            if ch == b'+' {
624                                r.plus_in_uri = true;
625                            }
626                            state = quoted_state;
627                            // no advance: the decoded byte is reprocessed
628                        }
629                    } else {
630                        return Err(ParseError);
631                    }
632                }
633            }
634        }
635    }
636
637    if state == State::Quoted || state == State::QuotedSecond {
638        return Err(ParseError);
639    }
640
641    if state == State::Dot {
642        u -= 1;
643    } else if state == State::DotDot {
644        // Same backwards scan as above for a trailing `..`.
645        let start = u.checked_sub(4).ok_or(ParseError)?;
646        u = out[..=start]
647            .iter()
648            .rposition(|&c| c == b'/')
649            .map(|i| i + 1)
650            .ok_or(ParseError)?;
651    }
652
653    finish_done(r, u)
654}
655
656/// Parse a single origin-form request target exactly as nginx does.
657///
658/// The returned values correspond to the initial values nginx exposes through
659/// its `$uri` and `$args` variables. nginx may subsequently change these
660/// variables during request processing.
661///
662/// * `Ok(`[`Parsed`]`)` — the normalized path and query string. For a "simple"
663///   path that needs no normalization, the path borrows the input unchanged
664///   ([`Cow::Borrowed`]) with no allocation; normalization returns an owned
665///   buffer ([`Cow::Owned`]). The query string always borrows the input.
666/// * `Err(ParseError)` — the request target could not be parsed.
667///
668/// `merge_slashes` corresponds to nginx's [`merge_slashes`](https://nginx.org/en/docs/http/ngx_http_core_module.html#merge_slashes)
669/// directive: `true` is `on` (the nginx default), and `false` is `off`.
670pub fn parse_origin_form(input: &[u8], merge_slashes: bool) -> Result<Parsed<'_>, ParseError> {
671    // HTTP/2 and HTTP/3 reject an empty :path before parsing it.
672    if input.is_empty() {
673        return Err(ParseError);
674    }
675
676    let mut r = Request::default();
677
678    // Stage 1 scans the request target and records whether normalization is
679    // needed. Unlike stage 2, it does not read nginx's trailing LF sentinel.
680    ngx_http_parse_uri(&mut r, input)?;
681
682    let path = if r.complex_uri || r.quoted_uri || r.empty_path_in_uri {
683        // Stage 2 normalizes the request target into a separate output buffer.
684        // `read_with_lf_sentinel` supplies nginx's trailing LF sentinel.
685        //
686        // Output never exceeds input length; +1 covers the
687        // (origin-form-unreachable) empty-path leading slash.
688        let mut out = vec![0u8; input.len() + 1];
689        ngx_http_parse_complex_uri(&mut r, input, &mut out, merge_slashes)?;
690        out.truncate(r.uri.len);
691        Cow::Owned(out)
692    } else {
693        // "simple" path: returned unchanged, query string excluded — borrow the
694        // input directly, no allocation.
695        let len = match r.args_start {
696            Some(a) => a - 1,
697            None => input.len(),
698        };
699        Cow::Borrowed(&input[..len])
700    };
701
702    Ok(Parsed {
703        path,
704        args: parsed_args(&r, input),
705    })
706}
707
708/// Compute nginx's `r->args` for a parsed target, mirroring the trailing args
709/// assignment in `ngx_http_process_request_uri`:
710///
711/// ```c
712/// if (r->args_start && r->uri_end > r->args_start) {
713///     r->args.len  = r->uri_end - r->args_start;
714///     r->args.data = r->args_start;
715/// }
716/// ```
717///
718/// When a complex URI delimits the query with a `#`, `ngx_http_parse_complex_uri`
719/// has already recorded `r.args` and cleared `args_start`; that case skips the
720/// block above, exactly as the NULL `args_start` does in nginx.
721fn parsed_args<'a>(r: &Request, input: &'a [u8]) -> Option<&'a [u8]> {
722    let uri_end = input.len();
723
724    // `args.data` is an offset just after a '?', always >= 2 for origin-form
725    // input (the path starts with '/'), so 0 is nginx's NULL sentinel. A
726    // non-zero `data` means parse_complex_uri delimited the query at a '#'
727    // (possibly empty, e.g. "/a?#f").
728    if r.args.data != 0 {
729        return Some(&input[r.args.data..r.args.data + r.args.len]);
730    }
731    // Otherwise the query, if any, runs from `args_start` to the end of input.
732    match r.args_start {
733        Some(a) if uri_end > a => Some(&input[a..uri_end]),
734        _ => None,
735    }
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741
742    fn norm(s: &str, merge: bool) -> Result<String, ParseError> {
743        parse_origin_form(s.as_bytes(), merge)
744            .map(|n| String::from_utf8(n.path.into_owned()).unwrap())
745    }
746
747    /// The query string as an `Option<&str>` (`None` == no query component).
748    fn args(s: &str, merge: bool) -> Option<String> {
749        parse_origin_form(s.as_bytes(), merge)
750            .unwrap()
751            .args
752            .map(|a| String::from_utf8(a.to_vec()).unwrap())
753    }
754
755    #[test]
756    fn parse_error_implements_std_error() {
757        fn assert_error<T: std::error::Error>() {}
758
759        assert_error::<ParseError>();
760        assert_eq!(ParseError.to_string(), "failed to parse request target");
761    }
762
763    #[test]
764    fn simple_unchanged() {
765        assert_eq!(norm("/", true).unwrap(), "/");
766        assert_eq!(norm("/foo/bar", true).unwrap(), "/foo/bar");
767    }
768
769    #[test]
770    fn dot_segments() {
771        assert_eq!(norm("/foo/./bar", true).unwrap(), "/foo/bar");
772        assert_eq!(norm("/foo/../bar", true).unwrap(), "/bar");
773        assert_eq!(norm("/a/b/../../c", true).unwrap(), "/c");
774        assert_eq!(norm("/../", true), Err(ParseError)); // escapes root
775    }
776
777    #[test]
778    fn merge_slashes_toggle() {
779        assert_eq!(norm("/a//b", true).unwrap(), "/a/b");
780        assert_eq!(norm("/a//b", false).unwrap(), "/a//b");
781    }
782
783    #[test]
784    fn percent_decoding() {
785        assert_eq!(norm("/%66oo", true).unwrap(), "/foo");
786        assert_eq!(norm("/a%2fb", true).unwrap(), "/a/b"); // decoded '/', not merged
787        assert_eq!(norm("/%2f/x", true).unwrap(), "/x");
788        assert_eq!(norm("/%2e%2e/x", true), Err(ParseError)); // decoded ".." escapes
789    }
790
791    #[test]
792    fn encoded_dots() {
793        assert_eq!(norm("/foo/%2e%2e/bar", true).unwrap(), "/bar");
794        assert_eq!(norm("/foo%2f..%2fbar", true).unwrap(), "/bar");
795        assert_eq!(norm("/foo%2f%2e%2e%2fbar", true).unwrap(), "/bar");
796    }
797
798    #[test]
799    fn query_split() {
800        assert_eq!(norm("/foo?a=1", true).unwrap(), "/foo");
801        assert_eq!(norm("/foo/../bar?x=%20", true).unwrap(), "/bar");
802    }
803
804    #[test]
805    fn invalid() {
806        assert_eq!(norm("relative", true), Err(ParseError)); // must start with '/'
807        assert_eq!(norm("*", true), Err(ParseError)); // must start with '/'
808        assert_eq!(norm("/%zz", true), Err(ParseError)); // bad %XX
809        assert_eq!(norm("/%00", true), Err(ParseError)); // null byte
810    }
811
812    #[test]
813    fn empty() {
814        assert_eq!(norm("", true), Err(ParseError));
815    }
816
817    #[test]
818    fn simple_path_borrows_input() {
819        // A path needing no normalization must not allocate.
820        assert!(matches!(
821            parse_origin_form(b"/foo/bar", true).unwrap().path,
822            Cow::Borrowed(_)
823        ));
824        // The query string is excluded, still by borrowing.
825        assert!(matches!(
826            parse_origin_form(b"/foo?a=1", true).unwrap().path,
827            Cow::Borrowed(_)
828        ));
829    }
830
831    #[test]
832    fn parsed_path_is_owned() {
833        assert!(matches!(
834            parse_origin_form(b"/foo/../bar", true).unwrap().path,
835            Cow::Owned(_)
836        ));
837        assert!(matches!(
838            parse_origin_form(b"/%66oo", true).unwrap().path,
839            Cow::Owned(_)
840        ));
841    }
842
843    #[test]
844    fn args_returned() {
845        // No query component.
846        assert_eq!(args("/foo", true), None);
847        assert_eq!(args("/foo/../bar", true), None); // complex, still no query
848
849        // Simple path with a query.
850        assert_eq!(args("/foo?a=1", true).as_deref(), Some("a=1"));
851        // Complex path (normalized) with a query, terminated by end of input.
852        assert_eq!(args("/foo/../bar?x=%20", true).as_deref(), Some("x=%20"));
853
854        // A '#' fragment terminates the query (and the fragment is dropped).
855        assert_eq!(args("/foo?a=1#frag", true).as_deref(), Some("a=1"));
856
857        // Trailing '?' with nothing after it: nginx leaves r->args.data NULL.
858        assert_eq!(args("/foo?", true), None);
859        // Present-but-empty query: '?' immediately followed by '#'.
860        assert_eq!(args("/foo?#frag", true).as_deref(), Some(""));
861
862        // The query string is never normalized, even when the path is.
863        assert_eq!(args("/a/../b?p=%2e%2e", true).as_deref(), Some("p=%2e%2e"));
864    }
865
866    #[test]
867    fn args_borrow_input() {
868        // args always borrows the input (Option<&[u8]>, no allocation).
869        let input = b"/foo?a=1";
870        let n = parse_origin_form(input, true).unwrap();
871        let a = n.args.unwrap();
872        assert!(std::ptr::eq(a.as_ptr(), input[5..].as_ptr()));
873    }
874}