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