use core::fmt;
mod validation;
use validation::{validate_path, validate_query};
pub const MAX_REQUEST_TARGET_BYTES: usize = 8192;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RequestPathError {
Empty,
NotOriginForm,
TooLong,
InvalidByte,
DoubledSlash,
DotSegment,
InvalidPercentTriplet,
LowercasePercentHex,
EncodedSeparator,
EncodedControl,
EncodedUnreserved,
}
impl_static_error!(RequestPathError,
Self::Empty => "request path is empty",
Self::NotOriginForm => "request path is not in origin form",
Self::TooLong => "request path exceeds the length limit",
Self::InvalidByte => "request path contains a forbidden byte",
Self::DoubledSlash => "request path contains an empty segment",
Self::DotSegment => "request path contains a dot segment",
Self::InvalidPercentTriplet => "request path contains malformed percent encoding",
Self::LowercasePercentHex => "request path percent encoding is not uppercase",
Self::EncodedSeparator => "request path percent encoding hides a separator",
Self::EncodedControl => "request path percent encoding hides a control byte",
Self::EncodedUnreserved => "request path percent encodes an unreserved byte",
);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StructuredQueryError {
TooLong,
InvalidByte,
EmptyPair,
EmptyKey,
MultipleEquals,
InvalidPercentTriplet,
LowercasePercentHex,
EncodedControl,
EncodedUnreserved,
PlusForbidden,
}
impl_static_error!(StructuredQueryError,
Self::TooLong => "query exceeds the length limit",
Self::InvalidByte => "query contains a forbidden byte",
Self::EmptyPair => "query contains an empty pair",
Self::EmptyKey => "query key is empty",
Self::MultipleEquals => "query pair contains multiple separators",
Self::InvalidPercentTriplet => "query contains malformed percent encoding",
Self::LowercasePercentHex => "query percent encoding is not uppercase",
Self::EncodedControl => "query percent encoding hides a control or fragment byte",
Self::EncodedUnreserved => "query percent encodes an unreserved byte",
Self::PlusForbidden => "query uses form-style plus encoding",
);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RequestTargetError {
Path(RequestPathError),
Query(StructuredQueryError),
TooLong,
OutputTooSmall,
}
impl fmt::Display for RequestTargetError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Path(error) => write!(formatter, "invalid request path: {error}"),
Self::Query(error) => write!(formatter, "invalid request query: {error}"),
Self::TooLong => formatter.write_str("request target exceeds the length limit"),
Self::OutputTooSmall => formatter.write_str("request target output is too small"),
}
}
}
impl core::error::Error for RequestTargetError {}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct RequestPath<'a>(&'a str);
impl<'a> RequestPath<'a> {
pub fn new(value: &'a str) -> Result<Self, RequestPathError> {
validate_path(value)?;
Ok(Self(value))
}
#[must_use]
pub const fn as_str(self) -> &'a str {
self.0
}
}
impl fmt::Debug for RequestPath<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("RequestPath([redacted])")
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct CanonicalQuery<'a>(&'a str);
impl<'a> CanonicalQuery<'a> {
pub fn new(value: &'a str) -> Result<Self, StructuredQueryError> {
validate_query(value, false)?;
Ok(Self(value))
}
#[must_use]
pub const fn as_str(self) -> &'a str {
self.0
}
#[must_use]
pub const fn pairs(self) -> QueryPairs<'a> {
QueryPairs::new(self.0)
}
}
impl fmt::Debug for CanonicalQuery<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("CanonicalQuery([redacted])")
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct FormQuery<'a>(&'a str);
impl<'a> FormQuery<'a> {
pub fn new(value: &'a str) -> Result<Self, StructuredQueryError> {
validate_query(value, true)?;
Ok(Self(value))
}
#[must_use]
pub const fn as_str(self) -> &'a str {
self.0
}
#[must_use]
pub const fn pairs(self) -> QueryPairs<'a> {
QueryPairs::new(self.0)
}
}
impl fmt::Debug for FormQuery<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("FormQuery([redacted])")
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RequestQuery<'a> {
Absent,
Canonical(CanonicalQuery<'a>),
Form(FormQuery<'a>),
}
impl<'a> RequestQuery<'a> {
#[must_use]
pub const fn is_present(self) -> bool {
!matches!(self, Self::Absent)
}
#[must_use]
pub const fn as_str(self) -> Option<&'a str> {
match self {
Self::Absent => None,
Self::Canonical(query) => Some(query.as_str()),
Self::Form(query) => Some(query.as_str()),
}
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct QueryPair<'a> {
key: &'a str,
value: Option<&'a str>,
}
impl<'a> QueryPair<'a> {
#[must_use]
pub const fn key(self) -> &'a str {
self.key
}
#[must_use]
pub const fn value(self) -> Option<&'a str> {
self.value
}
}
impl fmt::Debug for QueryPair<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("QueryPair([redacted])")
}
}
#[derive(Clone)]
pub struct QueryPairs<'a> {
remaining: &'a str,
}
impl<'a> QueryPairs<'a> {
const fn new(value: &'a str) -> Self {
Self { remaining: value }
}
}
impl fmt::Debug for QueryPairs<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("QueryPairs([redacted])")
}
}
impl<'a> Iterator for QueryPairs<'a> {
type Item = QueryPair<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.remaining.is_empty() {
return None;
}
let (pair, remaining) = match self.remaining.split_once('&') {
Some(parts) => parts,
None => (self.remaining, ""),
};
self.remaining = remaining;
let (key, value) = match pair.split_once('=') {
Some((key, value)) => (key, Some(value)),
None => (pair, None),
};
Some(QueryPair { key, value })
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct RequestTarget<'a> {
value: &'a str,
path: RequestPath<'a>,
query: RequestQuery<'a>,
}
impl<'a> RequestTarget<'a> {
pub fn new(value: &'a str) -> Result<Self, RequestTargetError> {
if value.len() > MAX_REQUEST_TARGET_BYTES {
return Err(RequestTargetError::TooLong);
}
let (path, query) = match value.split_once('?') {
Some((path, query)) => (
RequestPath::new(path).map_err(RequestTargetError::Path)?,
RequestQuery::Canonical(
CanonicalQuery::new(query).map_err(RequestTargetError::Query)?,
),
),
None => (
RequestPath::new(value).map_err(RequestTargetError::Path)?,
RequestQuery::Absent,
),
};
Ok(Self { value, path, query })
}
pub fn assemble<'output>(
path: RequestPath<'_>,
query: RequestQuery<'_>,
output: &'output mut [u8],
) -> Result<RequestTarget<'output>, RequestTargetError> {
let query_len = query.as_borrowed_str().map_or(0, str::len);
let delimiter_len = usize::from(query.is_present());
let len = path
.as_str()
.len()
.checked_add(delimiter_len)
.and_then(|len| len.checked_add(query_len))
.ok_or(RequestTargetError::TooLong)?;
if len > MAX_REQUEST_TARGET_BYTES {
return Err(RequestTargetError::TooLong);
}
let target = output
.get_mut(..len)
.ok_or(RequestTargetError::OutputTooSmall)?;
let path_len = path.as_str().len();
let (path_output, suffix) = target.split_at_mut(path_len);
path_output.copy_from_slice(path.as_str().as_bytes());
if query.is_present() {
let (delimiter, query_output) = suffix.split_at_mut(1);
let delimiter = delimiter
.first_mut()
.ok_or(RequestTargetError::OutputTooSmall)?;
*delimiter = b'?';
if let Some(query) = query.as_borrowed_str() {
query_output.copy_from_slice(query.as_bytes());
}
}
let value = core::str::from_utf8(target).map_err(|_| RequestTargetError::OutputTooSmall)?;
let path_value = value
.get(..path_len)
.ok_or(RequestTargetError::OutputTooSmall)?;
let path = RequestPath(path_value);
let query = match query {
RequestQuery::Absent => RequestQuery::Absent,
RequestQuery::Canonical(_) => {
let query_start = path_len.checked_add(1).ok_or(RequestTargetError::TooLong)?;
let query_value = value
.get(query_start..)
.ok_or(RequestTargetError::OutputTooSmall)?;
RequestQuery::Canonical(CanonicalQuery(query_value))
}
RequestQuery::Form(_) => {
let query_start = path_len.checked_add(1).ok_or(RequestTargetError::TooLong)?;
let query_value = value
.get(query_start..)
.ok_or(RequestTargetError::OutputTooSmall)?;
RequestQuery::Form(FormQuery(query_value))
}
};
Ok(RequestTarget { value, path, query })
}
#[must_use]
pub const fn as_str(self) -> &'a str {
self.value
}
#[must_use]
pub const fn path(self) -> RequestPath<'a> {
self.path
}
#[must_use]
pub const fn query(self) -> RequestQuery<'a> {
self.query
}
#[must_use]
pub fn query_bytes(self) -> Option<&'a [u8]> {
self.query().as_borrowed_str().map(str::as_bytes)
}
#[must_use]
pub const fn len(self) -> usize {
self.value.len()
}
#[must_use]
pub const fn is_empty(self) -> bool {
false
}
}
impl fmt::Debug for RequestTarget<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("RequestTarget([redacted])")
}
}
impl<'a> RequestQuery<'a> {
const fn as_borrowed_str(self) -> Option<&'a str> {
match self {
Self::Absent => None,
Self::Canonical(query) => Some(query.as_str()),
Self::Form(query) => Some(query.as_str()),
}
}
}
#[cfg(test)]
mod tests;