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