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