Skip to main content

url_parse_nginx/
lib.rs

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