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