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}