#![no_std]
#![deny(unsafe_code)]
#![cfg_attr(
feature = "percent-encoding",
doc = r#"
## Percent-encoding the normalized path
Some nginx processing paths, including some `proxy_pass` cases, first
normalize and percent-decode the request path, then percent-encode the
normalized path again. To reproduce this decode-then-encode flow, pass
[`Parsed::path`] to `percent_encoding::percent_encode` with
[`PATH_ESCAPE_SET`]. The set is available when the `percent-encoding`
feature is enabled (enabled by default).
The default `percent-encoding` feature also supports nginx-compatible
re-encoding:
```
use percent_encoding::percent_encode;
use url_parse_nginx::{parse_origin_form, PATH_ESCAPE_SET};
let parsed = parse_origin_form(b"/docs/../hello%20world", true)?;
let encoded = percent_encode(&parsed.path, PATH_ESCAPE_SET);
assert_eq!(encoded.to_string(), "/hello%20world");
# Ok::<(), url_parse_nginx::ParseError>(())
```
Using `percent_encoding::percent_encode` requires a direct dependency on the
[`percent-encoding`](https://docs.rs/percent-encoding/) crate.
"#
)]
extern crate alloc;
#[cfg(test)]
extern crate std;
use alloc::{borrow::Cow, vec};
#[cfg(feature = "percent-encoding")]
use percent_encoding::{AsciiSet, CONTROLS};
#[cfg(feature = "percent-encoding")]
pub const PATH_ESCAPE_SET: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
.add(b'#')
.add(b'%')
.add(b'<')
.add(b'>')
.add(b'?')
.add(b'\\')
.add(b'^')
.add(b'`')
.add(b'{')
.add(b'|')
.add(b'}');
const USUAL: [u32; 8] = [
0x0000_0000,
0x7fff_37d6,
0xffff_ffff,
0x7fff_ffff,
0xffff_ffff,
0xffff_ffff,
0xffff_ffff,
0xffff_ffff,
];
#[inline]
fn usual(ch: u8) -> bool {
USUAL[(ch >> 5) as usize] & (1u32 << (ch & 0x1f)) != 0
}
#[inline(always)]
fn read_with_lf_sentinel(buf: &[u8], p: usize) -> u8 {
buf.get(p).copied().unwrap_or(b'\n')
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParseError;
impl core::fmt::Display for ParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("failed to parse request target")
}
}
impl core::error::Error for ParseError {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Parsed<'a> {
pub path: Cow<'a, [u8]>,
pub args: Option<&'a [u8]>,
}
#[derive(Debug, Default, Clone, Copy)]
struct NgxStr {
len: usize,
data: usize,
}
#[derive(Debug, Default)]
struct Request {
uri: NgxStr, args: NgxStr, exten: NgxStr,
uri_ext: Option<usize>,
args_start: Option<usize>,
complex_uri: bool,
quoted_uri: bool,
plus_in_uri: bool,
empty_path_in_uri: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum UriState {
Start,
AfterSlash,
CheckUri,
Uri,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum State {
Usual,
Slash,
Dot,
DotDot,
Quoted,
QuotedSecond,
}
#[inline(never)]
fn ngx_http_parse_uri(r: &mut Request, buf: &[u8]) -> Result<(), ParseError> {
let uri_start = 0;
let uri_end = buf.len();
let mut state = UriState::Start;
let mut p = uri_start;
while p != uri_end {
let ch = buf[p];
match state {
UriState::Start => {
if ch != b'/' {
return Err(ParseError);
}
state = UriState::AfterSlash;
}
UriState::AfterSlash => {
if usual(ch) {
state = UriState::CheckUri;
} else {
match ch {
b'.' => {
r.complex_uri = true;
state = UriState::Uri;
}
b'%' => {
r.quoted_uri = true;
state = UriState::Uri;
}
b'/' => {
r.complex_uri = true;
state = UriState::Uri;
}
b'?' => {
r.args_start = Some(p + 1);
state = UriState::Uri;
}
b'#' => {
r.complex_uri = true;
state = UriState::Uri;
}
b'+' => {
r.plus_in_uri = true;
}
_ => {
if ch <= 0x20 || ch == 0x7f {
return Err(ParseError);
}
state = UriState::CheckUri;
}
}
}
}
UriState::CheckUri => {
if usual(ch) {
p += 1;
while p != uri_end && usual(buf[p]) {
p += 1;
}
continue;
} else {
match ch {
b'/' => {
r.uri_ext = None;
state = UriState::AfterSlash;
}
b'.' => {
r.uri_ext = Some(p + 1);
}
b'%' => {
r.quoted_uri = true;
state = UriState::Uri;
}
b'?' => {
r.args_start = Some(p + 1);
state = UriState::Uri;
}
b'#' => {
r.complex_uri = true;
state = UriState::Uri;
}
b'+' => {
r.plus_in_uri = true;
}
_ => {
if ch <= 0x20 || ch == 0x7f {
return Err(ParseError);
}
}
}
}
}
UriState::Uri => {
if usual(ch) {
} else {
match ch {
b'#' => {
r.complex_uri = true;
}
_ => {
if ch <= 0x20 || ch == 0x7f {
return Err(ParseError);
}
}
}
}
}
}
p += 1;
}
Ok(())
}
fn finish_done(r: &mut Request, u: usize) -> Result<(), ParseError> {
r.uri.len = u;
if let Some(ext) = r.uri_ext {
r.exten.len = u.wrapping_sub(ext);
r.exten.data = ext;
}
r.uri_ext = None;
Ok(())
}
fn finish_args(r: &mut Request, buf: &[u8], u: usize, mut p: usize) -> Result<(), ParseError> {
let uri_end = buf.len();
while p < uri_end {
let c = buf[p];
p += 1;
if c != b'#' {
continue;
}
let args_start = r.args_start.unwrap();
r.args.len = (p - 1).wrapping_sub(args_start);
r.args.data = args_start;
r.args_start = None;
break;
}
finish_done(r, u)
}
#[inline(never)]
fn ngx_http_parse_complex_uri(
r: &mut Request,
buf: &[u8],
out: &mut [u8],
merge_slashes: bool,
) -> Result<(), ParseError> {
let uri_start = 0;
let uri_end = buf.len();
let mut state = State::Usual;
let mut quoted_state = State::Usual;
let mut decoded: u8 = 0;
let mut p = uri_start;
let mut u: usize = 0;
r.uri_ext = None;
r.args_start = None;
if r.empty_path_in_uri {
out[u] = b'/';
u += 1;
}
let mut ch = read_with_lf_sentinel(buf, p);
p += 1;
while p <= uri_end {
match state {
State::Usual => {
if usual(ch) {
out[u] = ch;
u += 1;
ch = read_with_lf_sentinel(buf, p);
p += 1;
} else {
match ch {
b'/' => {
r.uri_ext = None;
state = State::Slash;
out[u] = ch;
u += 1;
}
b'%' => {
quoted_state = state;
state = State::Quoted;
}
b'?' => {
r.args_start = Some(p);
return finish_args(r, buf, u, p);
}
b'#' => {
return finish_done(r, u);
}
b'.' => {
r.uri_ext = Some(u + 1);
out[u] = ch;
u += 1;
}
b'+' => {
r.plus_in_uri = true;
out[u] = ch;
u += 1;
}
_ => {
out[u] = ch;
u += 1;
}
}
ch = read_with_lf_sentinel(buf, p);
p += 1;
}
}
State::Slash => {
if usual(ch) {
state = State::Usual;
out[u] = ch;
u += 1;
ch = read_with_lf_sentinel(buf, p);
p += 1;
} else {
match ch {
b'/' => {
if !merge_slashes {
out[u] = ch;
u += 1;
}
}
b'.' => {
state = State::Dot;
out[u] = ch;
u += 1;
}
b'%' => {
quoted_state = state;
state = State::Quoted;
}
b'?' => {
r.args_start = Some(p);
return finish_args(r, buf, u, p);
}
b'#' => {
return finish_done(r, u);
}
b'+' => {
r.plus_in_uri = true;
state = State::Usual;
out[u] = ch;
u += 1;
}
_ => {
state = State::Usual;
out[u] = ch;
u += 1;
}
}
ch = read_with_lf_sentinel(buf, p);
p += 1;
}
}
State::Dot => {
if usual(ch) {
state = State::Usual;
out[u] = ch;
u += 1;
ch = read_with_lf_sentinel(buf, p);
p += 1;
} else {
match ch {
b'/' => {
state = State::Slash;
u -= 1;
}
b'.' => {
state = State::DotDot;
out[u] = ch;
u += 1;
}
b'%' => {
quoted_state = state;
state = State::Quoted;
}
b'?' => {
u -= 1;
r.args_start = Some(p);
return finish_args(r, buf, u, p);
}
b'#' => {
u -= 1;
return finish_done(r, u);
}
b'+' => {
r.plus_in_uri = true;
state = State::Usual;
out[u] = ch;
u += 1;
}
_ => {
state = State::Usual;
out[u] = ch;
u += 1;
}
}
ch = read_with_lf_sentinel(buf, p);
p += 1;
}
}
State::DotDot => {
if usual(ch) {
state = State::Usual;
out[u] = ch;
u += 1;
ch = read_with_lf_sentinel(buf, p);
p += 1;
} else {
match ch {
b'/' | b'?' | b'#' => {
let start = u.checked_sub(4).ok_or(ParseError)?;
u = out[..=start]
.iter()
.rposition(|&c| c == b'/')
.map(|i| i + 1)
.ok_or(ParseError)?;
if ch == b'?' {
r.args_start = Some(p);
return finish_args(r, buf, u, p);
}
if ch == b'#' {
return finish_done(r, u);
}
state = State::Slash;
}
b'%' => {
quoted_state = state;
state = State::Quoted;
}
b'+' => {
r.plus_in_uri = true;
state = State::Usual;
out[u] = ch;
u += 1;
}
_ => {
state = State::Usual;
out[u] = ch;
u += 1;
}
}
ch = read_with_lf_sentinel(buf, p);
p += 1;
}
}
State::Quoted => {
r.quoted_uri = true;
if ch.is_ascii_digit() {
decoded = ch - b'0';
state = State::QuotedSecond;
ch = read_with_lf_sentinel(buf, p);
p += 1;
} else {
let c = ch | 0x20;
if (b'a'..=b'f').contains(&c) {
decoded = c - b'a' + 10;
state = State::QuotedSecond;
ch = read_with_lf_sentinel(buf, p);
p += 1;
} else {
return Err(ParseError);
}
}
}
State::QuotedSecond => {
if ch.is_ascii_digit() {
ch = (decoded << 4) + (ch - b'0');
if ch == b'%' || ch == b'#' {
state = State::Usual;
out[u] = ch;
u += 1;
ch = read_with_lf_sentinel(buf, p);
p += 1;
} else if ch == b'\0' {
return Err(ParseError);
} else {
state = quoted_state;
}
} else {
let c = ch | 0x20;
if (b'a'..=b'f').contains(&c) {
ch = (decoded << 4) + (c - b'a') + 10;
if ch == b'?' {
state = State::Usual;
out[u] = ch;
u += 1;
ch = read_with_lf_sentinel(buf, p);
p += 1;
} else {
if ch == b'+' {
r.plus_in_uri = true;
}
state = quoted_state;
}
} else {
return Err(ParseError);
}
}
}
}
}
if state == State::Quoted || state == State::QuotedSecond {
return Err(ParseError);
}
if state == State::Dot {
u -= 1;
} else if state == State::DotDot {
let start = u.checked_sub(4).ok_or(ParseError)?;
u = out[..=start]
.iter()
.rposition(|&c| c == b'/')
.map(|i| i + 1)
.ok_or(ParseError)?;
}
finish_done(r, u)
}
pub fn parse_origin_form(target: &[u8], merge_slashes: bool) -> Result<Parsed<'_>, ParseError> {
if target.is_empty() {
return Err(ParseError);
}
let mut r = Request::default();
ngx_http_parse_uri(&mut r, target)?;
let path = if r.complex_uri || r.quoted_uri || r.empty_path_in_uri {
let mut out = vec![0u8; target.len() + 1];
ngx_http_parse_complex_uri(&mut r, target, &mut out, merge_slashes)?;
out.truncate(r.uri.len);
Cow::Owned(out)
} else {
let len = match r.args_start {
Some(a) => a - 1,
None => target.len(),
};
Cow::Borrowed(&target[..len])
};
Ok(Parsed {
path,
args: parsed_args(&r, target),
})
}
fn parsed_args<'a>(r: &Request, input: &'a [u8]) -> Option<&'a [u8]> {
let uri_end = input.len();
if r.args.data != 0 {
return Some(&input[r.args.data..r.args.data + r.args.len]);
}
match r.args_start {
Some(a) if uri_end > a => Some(&input[a..uri_end]),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::string::{String, ToString};
fn norm(s: &str, merge: bool) -> Result<String, ParseError> {
parse_origin_form(s.as_bytes(), merge)
.map(|n| String::from_utf8(n.path.into_owned()).unwrap())
}
fn args(s: &str, merge: bool) -> Option<String> {
parse_origin_form(s.as_bytes(), merge)
.unwrap()
.args
.map(|a| String::from_utf8(a.to_vec()).unwrap())
}
#[test]
fn parse_error_implements_std_error() {
fn assert_error<T: std::error::Error>() {}
assert_error::<ParseError>();
assert_eq!(ParseError.to_string(), "failed to parse request target");
}
#[test]
fn simple_unchanged() {
assert_eq!(norm("/", true).unwrap(), "/");
assert_eq!(norm("/foo/bar", true).unwrap(), "/foo/bar");
}
#[test]
fn dot_segments() {
assert_eq!(norm("/foo/./bar", true).unwrap(), "/foo/bar");
assert_eq!(norm("/foo/../bar", true).unwrap(), "/bar");
assert_eq!(norm("/a/b/../../c", true).unwrap(), "/c");
assert_eq!(norm("/../", true), Err(ParseError)); }
#[test]
fn merge_slashes_toggle() {
assert_eq!(norm("/a//b", true).unwrap(), "/a/b");
assert_eq!(norm("/a//b", false).unwrap(), "/a//b");
}
#[test]
fn percent_decoding() {
assert_eq!(norm("/%66oo", true).unwrap(), "/foo");
assert_eq!(norm("/a%2fb", true).unwrap(), "/a/b"); assert_eq!(norm("/%2f/x", true).unwrap(), "/x");
assert_eq!(norm("/%2e%2e/x", true), Err(ParseError)); }
#[test]
fn encoded_dots() {
assert_eq!(norm("/foo/%2e%2e/bar", true).unwrap(), "/bar");
assert_eq!(norm("/foo%2f..%2fbar", true).unwrap(), "/bar");
assert_eq!(norm("/foo%2f%2e%2e%2fbar", true).unwrap(), "/bar");
}
#[test]
fn query_split() {
assert_eq!(norm("/foo?a=1", true).unwrap(), "/foo");
assert_eq!(norm("/foo/../bar?x=%20", true).unwrap(), "/bar");
}
#[test]
fn invalid() {
assert_eq!(norm("relative", true), Err(ParseError)); assert_eq!(norm("*", true), Err(ParseError)); assert_eq!(norm("/%zz", true), Err(ParseError)); assert_eq!(norm("/%00", true), Err(ParseError)); }
#[test]
fn empty() {
assert_eq!(norm("", true), Err(ParseError));
}
#[test]
fn simple_path_borrows_input() {
assert!(matches!(
parse_origin_form(b"/foo/bar", true).unwrap().path,
Cow::Borrowed(_)
));
assert!(matches!(
parse_origin_form(b"/foo?a=1", true).unwrap().path,
Cow::Borrowed(_)
));
}
#[test]
fn parsed_path_is_owned() {
assert!(matches!(
parse_origin_form(b"/foo/../bar", true).unwrap().path,
Cow::Owned(_)
));
assert!(matches!(
parse_origin_form(b"/%66oo", true).unwrap().path,
Cow::Owned(_)
));
}
#[test]
fn args_returned() {
assert_eq!(args("/foo", true), None);
assert_eq!(args("/foo/../bar", true), None);
assert_eq!(args("/foo?a=1", true).as_deref(), Some("a=1"));
assert_eq!(args("/foo/../bar?x=%20", true).as_deref(), Some("x=%20"));
assert_eq!(args("/foo?a=1#frag", true).as_deref(), Some("a=1"));
assert_eq!(args("/foo?", true), None);
assert_eq!(args("/foo?#frag", true).as_deref(), Some(""));
assert_eq!(args("/a/../b?p=%2e%2e", true).as_deref(), Some("p=%2e%2e"));
}
#[test]
fn args_borrow_input() {
let input = b"/foo?a=1";
let n = parse_origin_form(input, true).unwrap();
let a = n.args.unwrap();
assert!(std::ptr::eq(a.as_ptr(), input[5..].as_ptr()));
}
}