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