use std::borrow::Cow;
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 std::fmt::Display for ParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("failed to parse request target")
}
}
impl std::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(input: &[u8], merge_slashes: bool) -> Result<Parsed<'_>, ParseError> {
if input.is_empty() {
return Err(ParseError);
}
let mut r = Request::default();
ngx_http_parse_uri(&mut r, input)?;
let path = if r.complex_uri || r.quoted_uri || r.empty_path_in_uri {
let mut out = vec![0u8; input.len() + 1];
ngx_http_parse_complex_uri(&mut r, input, &mut out, merge_slashes)?;
out.truncate(r.uri.len);
Cow::Owned(out)
} else {
let len = match r.args_start {
Some(a) => a - 1,
None => input.len(),
};
Cow::Borrowed(&input[..len])
};
Ok(Parsed {
path,
args: parsed_args(&r, input),
})
}
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::*;
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()));
}
}