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