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