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