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}