use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequestTargetError {
Empty,
NotOriginForm,
ContainsWhitespace,
AbsoluteUri,
AuthorityForm,
AsteriskForm,
}
impl fmt::Display for RequestTargetError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "request target is empty"),
Self::NotOriginForm => write!(f, "request target must start with '/'"),
Self::ContainsWhitespace => write!(f, "request target contains whitespace"),
Self::AbsoluteUri => write!(f, "absolute URI not supported"),
Self::AuthorityForm => write!(f, "authority-form URI not supported"),
Self::AsteriskForm => write!(f, "asterisk-form URI not supported"),
}
}
}
impl std::error::Error for RequestTargetError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RequestTarget {
raw: String,
path: String,
query: Option<String>,
}
impl RequestTarget {
pub fn parse(raw: impl Into<String>) -> Result<Self, RequestTargetError> {
let raw = raw.into();
if raw.is_empty() {
return Err(RequestTargetError::Empty);
}
if raw == "*" {
return Err(RequestTargetError::AsteriskForm);
}
if raw.contains(char::is_whitespace) {
return Err(RequestTargetError::ContainsWhitespace);
}
if raw.starts_with('/') {
if raw.starts_with("//") {
return Err(RequestTargetError::AuthorityForm);
}
return Self::parse_origin_form(raw);
}
if raw.contains("://") {
return Err(RequestTargetError::AbsoluteUri);
}
if raw.contains('@') || raw.contains(':') {
return Err(RequestTargetError::AuthorityForm);
}
Err(RequestTargetError::NotOriginForm)
}
fn parse_origin_form(raw: String) -> Result<Self, RequestTargetError> {
debug_assert!(raw.starts_with('/'));
let (path, query) = match raw.find('?') {
Some(pos) => {
let path = raw[..pos].to_string();
let q = &raw[pos + 1..];
if q.is_empty() {
(path, None)
} else {
(path, Some(q.to_string()))
}
}
None => (raw.clone(), None),
};
Ok(Self { raw, path, query })
}
pub fn raw(&self) -> &str {
&self.raw
}
pub fn path(&self) -> &str {
&self.path
}
pub fn query(&self) -> Option<&str> {
self.query.as_deref()
}
pub fn path_and_query(&self) -> &str {
&self.raw
}
}
impl fmt::Display for RequestTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.raw)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn root_path() {
let t = RequestTarget::parse("/").unwrap();
assert_eq!(t.raw(), "/");
assert_eq!(t.path(), "/");
assert!(t.query().is_none());
}
#[test]
fn network_path_reference_is_rejected() {
assert_eq!(
RequestTarget::parse("//example.com/file").unwrap_err(),
RequestTargetError::AuthorityForm
);
}
#[test]
fn path_with_query() {
let t = RequestTarget::parse("/foo?bar=baz").unwrap();
assert_eq!(t.path(), "/foo");
assert_eq!(t.query(), Some("bar=baz"));
}
#[test]
fn path_with_empty_query() {
let t = RequestTarget::parse("/foo?").unwrap();
assert_eq!(t.path(), "/foo");
assert!(t.query().is_none());
}
#[test]
fn path_with_multiple_query_params() {
let t = RequestTarget::parse("/a?b=1&c=2").unwrap();
assert_eq!(t.path(), "/a");
assert_eq!(t.query(), Some("b=1&c=2"));
}
#[test]
fn complex_path() {
let t = RequestTarget::parse("/foo/bar/file.txt?x=1&y=2").unwrap();
assert_eq!(t.path(), "/foo/bar/file.txt");
assert_eq!(t.query(), Some("x=1&y=2"));
}
#[test]
fn reject_empty() {
assert_eq!(
RequestTarget::parse("").unwrap_err(),
RequestTargetError::Empty
);
}
#[test]
fn reject_no_slash_prefix() {
assert_eq!(
RequestTarget::parse("foo").unwrap_err(),
RequestTargetError::NotOriginForm
);
}
#[test]
fn reject_absolute_uri() {
assert_eq!(
RequestTarget::parse("http://example.com/").unwrap_err(),
RequestTargetError::AbsoluteUri
);
}
#[test]
fn reject_authority_form() {
assert_eq!(
RequestTarget::parse("example.com:443").unwrap_err(),
RequestTargetError::AuthorityForm
);
}
#[test]
fn reject_asterisk_form() {
assert_eq!(
RequestTarget::parse("*").unwrap_err(),
RequestTargetError::AsteriskForm
);
}
#[test]
fn reject_whitespace() {
assert_eq!(
RequestTarget::parse("/foo bar").unwrap_err(),
RequestTargetError::ContainsWhitespace
);
assert_eq!(
RequestTarget::parse("/foo\tbar").unwrap_err(),
RequestTargetError::ContainsWhitespace
);
}
#[test]
fn path_and_query_combined() {
let t = RequestTarget::parse("/foo?bar").unwrap();
assert_eq!(t.path_and_query(), "/foo?bar");
}
#[test]
fn display() {
let t = RequestTarget::parse("/foo?bar").unwrap();
assert_eq!(format!("{t}"), "/foo?bar");
}
#[test]
fn error_display() {
assert!(!RequestTargetError::Empty.to_string().is_empty());
assert!(!RequestTargetError::NotOriginForm.to_string().is_empty());
assert!(!RequestTargetError::AbsoluteUri.to_string().is_empty());
assert!(!RequestTargetError::AuthorityForm.to_string().is_empty());
assert!(!RequestTargetError::AsteriskForm.to_string().is_empty());
assert!(!RequestTargetError::ContainsWhitespace
.to_string()
.is_empty());
}
#[test]
fn error_is_error() {
let err: &dyn std::error::Error = &RequestTargetError::Empty;
assert!(!err.to_string().is_empty());
}
#[test]
fn percent_encoded_path() {
let t = RequestTarget::parse("/foo%20bar").unwrap();
assert_eq!(t.raw(), "/foo%20bar");
assert_eq!(t.path(), "/foo%20bar");
assert!(t.query().is_none());
}
#[test]
fn percent_encoded_slash() {
let t = RequestTarget::parse("/foo%2Fbar").unwrap();
assert_eq!(t.raw(), "/foo%2Fbar");
assert_eq!(t.path(), "/foo%2Fbar");
assert!(t.query().is_none());
}
#[test]
fn dot_segment_paths() {
let t = RequestTarget::parse("/./foo").unwrap();
assert_eq!(t.path(), "/./foo");
assert!(t.query().is_none());
}
#[test]
fn dot_segment_parent() {
let t = RequestTarget::parse("/foo/../bar").unwrap();
assert_eq!(t.path(), "/foo/../bar");
assert!(t.query().is_none());
}
#[test]
fn backslash_in_path() {
let t = RequestTarget::parse("/foo\\bar").unwrap();
assert_eq!(t.path(), "/foo\\bar");
assert!(t.query().is_none());
}
#[test]
fn non_ascii_bytes() {
let t = RequestTarget::parse("/foo\u{00E9}\u{00FF}").unwrap();
assert_eq!(t.raw(), "/foo\u{00E9}\u{00FF}");
assert_eq!(t.path(), "/foo\u{00E9}\u{00FF}");
assert!(t.query().is_none());
}
#[test]
fn origin_form_only_enforced() {
assert_eq!(
RequestTarget::parse("foo").unwrap_err(),
RequestTargetError::NotOriginForm
);
}
#[test]
fn query_with_equals() {
let t = RequestTarget::parse("/path?key=val=ue").unwrap();
assert_eq!(t.path(), "/path");
assert_eq!(t.query(), Some("key=val=ue"));
}
#[test]
fn query_with_encoded_chars() {
let t = RequestTarget::parse("/path?key=hello%20world").unwrap();
assert_eq!(t.path(), "/path");
assert_eq!(t.query(), Some("key=hello%20world"));
}
#[test]
fn multiple_question_marks() {
let t = RequestTarget::parse("/path?a=1?b=2").unwrap();
assert_eq!(t.path(), "/path");
assert_eq!(t.query(), Some("a=1?b=2"));
}
}