#![deny(
unreachable_pub,
missing_debug_implementations,
missing_docs,
clippy::pedantic
)]
#![allow(clippy::missing_panics_doc)]
pub mod extensions;
pub mod parse;
pub mod prelude;
use prelude::*;
#[doc(inline)]
pub use extensions::{
PresentArguments, PresentArgumentsIter, PresentExtensions, PresentExtensionsIter,
};
#[doc(inline)]
pub use parse::{list_header, sanitize_request, CriticalRequestComponents, ValueQualitySet};
#[macro_export]
macro_rules! field_str {
($self:ident.$field:ident) => {{
#[allow(unused_must_use)]
{
&$self.$field;
}
stringify!($field)
}};
}
#[macro_export]
macro_rules! fmt_field {
($f: expr, $self:ident.$field:ident) => {
$f.field($crate::field_str!($self.$field), &$self.$field);
};
($f: expr, $self:ident.$field:ident, $value:expr) => {
$f.field($crate::field_str!($self.$field), $value);
};
}
#[macro_export]
macro_rules! fmt_fields {
($f: expr, $($(#[$meta:meta])?($self:ident.$field:ident $(, $value:expr)?)),+ $(,)?) => {
$(
$(#[$meta])?
$crate::fmt_field!($f, $self.$field $(, $value)?);
)+
};
}
#[macro_export]
macro_rules! ident_str {
($item:ident $(, $($name:ident)+, $($generics:tt)+)?) => {{
impl$(<$($generics)+>)? $item$(<$($name)+>)? {}
stringify!($item)
}};
}
pub mod chars {
pub const TAB: u8 = b'\t';
pub const LF: u8 = b'\n';
pub const CR: u8 = b'\r';
pub const SPACE: u8 = b' ';
pub const BANG: u8 = b'!';
pub const QUOTATION: u8 = b'"';
pub const DOLLAR: u8 = b'$';
pub const AMPERSAND: u8 = b'&';
pub const PERIOD: u8 = b'.';
pub const FORWARD_SLASH: u8 = b'/';
pub const COLON: u8 = b':';
pub const R_TAG: u8 = b'>';
pub const PIPE: u8 = b'|';
pub const L_SQ_BRACKET: u8 = b'[';
pub const ESCAPE: u8 = b'\\';
pub const R_SQ_BRACKET: u8 = b']';
}
#[macro_export]
macro_rules! build_bytes {
() => (
$crate::prelude::prelude::Bytes::new()
);
($($bytes:expr),+ $(,)?) => {{
let mut b = $crate::prelude::BytesMut::with_capacity($($bytes.len() +)* 0);
$(b.extend($bytes.iter());)*
b.freeze()
}};
}
pub struct CleanDebug<'a, T: ?Sized + Display>(&'a T);
impl<'a, T: ?Sized + Display> CleanDebug<'a, T> {
#[inline]
pub fn new(value: &'a T) -> Self {
Self(value)
}
}
impl<'a, T: ?Sized + Display> Debug for CleanDebug<'a, T> {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self.0, f)
}
}
impl<'a, T: ?Sized + Display> Display for CleanDebug<'a, T> {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(self.0, f)
}
}
pub trait AsCleanDebug {
fn as_clean(&self) -> CleanDebug<Self>
where
Self: Display,
{
CleanDebug::new(self)
}
}
impl<T: Display> AsCleanDebug for T {}
#[derive(Debug)]
#[must_use]
pub struct WriteableBytes {
bytes: BytesMut,
len: usize,
}
impl WriteableBytes {
#[inline]
pub fn new() -> Self {
Self {
bytes: BytesMut::new(),
len: 0,
}
}
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
let mut bytes = BytesMut::with_capacity(capacity);
unsafe { bytes.set_len(bytes.capacity()) };
Self { bytes, len: 0 }
}
#[inline]
#[must_use]
pub fn into_inner(mut self) -> BytesMut {
unsafe { self.bytes.set_len(self.len) };
self.bytes
}
}
impl Default for WriteableBytes {
fn default() -> Self {
Self::new()
}
}
impl From<BytesMut> for WriteableBytes {
fn from(mut bytes: BytesMut) -> Self {
let len = bytes.len();
unsafe { bytes.set_len(bytes.capacity()) };
Self { bytes, len }
}
}
impl Write for WriteableBytes {
#[inline]
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if self.len + buf.len() > self.bytes.capacity() {
self.bytes.reserve(buf.len() + 512);
unsafe { self.bytes.set_len(self.bytes.capacity()) };
}
self.bytes[self.len..self.len + buf.len()].copy_from_slice(buf);
self.len += buf.len();
Ok(buf.len())
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
pub fn make_path(
base_path: impl AsRef<Path>,
dir: impl AsRef<Path>,
file: impl AsRef<Path>,
extension: Option<&str>,
) -> PathBuf {
let mut path = PathBuf::with_capacity(
base_path.as_ref().as_os_str().len()
+ dir.as_ref().as_os_str().len()
+ 2
+ file.as_ref().as_os_str().len()
+ extension.map_or(0, |e| e.len() + 1),
);
path.push(base_path);
path.push(dir);
path.push(file);
if let Some(extension) = extension {
path.set_extension(extension);
}
path
}
#[must_use]
pub fn hardcoded_error_body(code: http::StatusCode, message: Option<&[u8]>) -> Bytes {
let mut body = BytesMut::with_capacity(200 + message.map_or(0, <[u8]>::len));
let reason = code.canonical_reason();
body.extend(b"<!DOCTYPE html><html><head><title>");
body.extend(code.as_str().as_bytes());
body.extend(b" ");
if let Some(reason) = reason {
body.extend(reason.as_bytes());
}
body.extend(b"</title></head><body><center><h1>".iter());
body.extend(code.as_str().as_bytes());
body.extend(b" ");
if let Some(reason) = reason {
body.extend(reason.as_bytes());
}
body.extend(b"</h1><hr>An unexpected error occurred. <a href='/'>Return home</a>?".iter());
if let Some(message) = message {
body.extend(b"<p>");
body.extend(message);
body.extend(b"</p>");
}
body.extend(b"</center></body></html>".iter());
body.freeze()
}
#[inline]
pub fn empty_clone_response<T>(response: &Response<T>) -> Response<()> {
let mut builder = Response::builder()
.version(response.version())
.status(response.status());
*builder.headers_mut().unwrap() = response.headers().clone();
builder.body(()).unwrap()
}
#[inline]
pub fn empty_clone_request<T>(request: &Request<T>) -> Request<()> {
let mut builder = Request::builder()
.method(request.method())
.version(request.version())
.uri(request.uri().clone());
*builder.headers_mut().unwrap() = request.headers().clone();
builder.body(()).unwrap()
}
#[inline]
pub fn split_response<T>(response: Response<T>) -> (Response<()>, T) {
let mut body = None;
let response = response.map(|t| body = Some(t));
(response, body.unwrap())
}
#[inline]
pub fn remove_all_headers<K: header::IntoHeaderName>(headers: &mut HeaderMap, name: K) {
if let header::Entry::Occupied(entry) = headers.entry(name) {
entry.remove_entry_mult();
}
}
pub fn header_eq(headers: &HeaderMap, name: impl header::AsHeaderName, value: &str) -> bool {
let header_value = headers
.get(name)
.map(HeaderValue::to_str)
.and_then(Result::ok);
header_value.map_or(false, |s| s.eq_ignore_ascii_case(value))
}
#[macro_export]
macro_rules! starts_with_any {
($e:expr, $($match:expr),* $(,)?) => {
$($e.starts_with($match) || )* false
};
}
#[must_use]
pub fn valid_method(bytes: &[u8]) -> bool {
starts_with_any!(
bytes,
b"GET",
b"HEAD",
b"POST",
b"PUT",
b"DELETE",
b"TRACE",
b"OPTIONS",
b"CONNECT",
b"PATCH",
b"COPY",
b"LOCK",
b"MKCOL",
b"MOVE",
b"PROPFIND",
b"PROPPATCH",
b"UNLOCK",
)
}
#[must_use]
pub fn valid_version(bytes: &[u8]) -> bool {
starts_with_any!(
bytes,
b"HTTP/0.9",
b"HTTP/1.0",
b"HTTP/1.1",
b"HTTP/2",
b"HTTP/3"
)
}
#[inline]
pub fn get_body_length_request<T>(request: &Request<T>) -> usize {
use std::str::FromStr;
if method_has_request_body(request.method()) {
request
.headers()
.get("content-length")
.map(HeaderValue::to_str)
.and_then(Result::ok)
.map(usize::from_str)
.and_then(Result::ok)
.unwrap_or(0)
} else {
0
}
}
#[inline]
pub fn set_content_length(headers: &mut HeaderMap, len: usize) {
headers.insert(
"content-length",
HeaderValue::from_str(len.to_string().as_str()).unwrap(),
);
}
pub fn get_body_length_response<T>(response: &Response<T>, method: Option<&Method>) -> usize {
use std::str::FromStr;
if method.map_or(true, method_has_response_body) {
response
.headers()
.get("content-length")
.map(HeaderValue::to_str)
.and_then(Result::ok)
.map(usize::from_str)
.and_then(Result::ok)
.unwrap_or(0)
} else {
0
}
}
#[inline]
#[must_use]
pub fn method_has_request_body(method: &Method) -> bool {
matches!(*method, Method::POST | Method::PUT | Method::DELETE)
}
#[inline]
#[must_use]
pub fn method_has_response_body(method: &Method) -> bool {
matches!(
*method,
Method::GET
| Method::POST
| Method::DELETE
| Method::CONNECT
| Method::OPTIONS
| Method::PATCH
)
}
pub fn ref_to_mut<T>(reference: &T) -> *mut T {
reference as *const T as *mut T
}
#[repr(transparent)]
#[must_use]
pub struct SuperUnsafePointer<T> {
pointer: *mut T,
}
impl<T> SuperUnsafePointer<T> {
pub fn new_mut(reference: &mut T) -> Self {
Self { pointer: reference }
}
pub unsafe fn new(reference: &T) -> Self {
Self {
pointer: ref_to_mut(reference),
}
}
#[must_use]
pub unsafe fn get(&self) -> &T {
&*self.pointer
}
pub unsafe fn get_mut(&mut self) -> &mut T {
&mut *self.pointer
}
}
unsafe impl<T: Send> Send for SuperUnsafePointer<T> {}
unsafe impl<T: Sync> Sync for SuperUnsafePointer<T> {}
impl<T> Debug for SuperUnsafePointer<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct(ident_str!(SuperUnsafePointer, T, T));
fmt_field!(s, self.pointer, &"[unsafe pointer]".as_clean());
s.finish()
}
}
impl<T> Clone for SuperUnsafePointer<T> {
fn clone(&self) -> Self {
unsafe { Self::new(self.get()) }
}
}
impl<T> PartialEq for SuperUnsafePointer<T> {
fn eq(&self, other: &Self) -> bool {
self.pointer == other.pointer
}
}
impl<T> Eq for SuperUnsafePointer<T> {}
#[must_use]
#[inline]
pub fn is_valid_header_value_byte(byte: u8) -> bool {
(32..127).contains(&byte) || byte == b'\t'
}
#[must_use]
#[inline]
pub fn percent_decode(s: &str) -> Cow<'_, str> {
percent_encoding::percent_decode_str(s)
.decode_utf8()
.unwrap_or(Cow::Borrowed(s))
}
pub fn join<S: AsRef<str>, I: Iterator<Item = S> + Clone>(
iter: I,
separator: impl AsRef<str>,
) -> String {
let length = iter
.clone()
.map(|s| s.as_ref().len() + separator.as_ref().len())
.sum::<usize>()
.saturating_sub(separator.as_ref().len());
let mut string = String::with_capacity(length);
for (pos, s) in iter.enumerate() {
if pos != 0 {
string += separator.as_ref();
}
string += s.as_ref();
}
string
}
#[derive(Debug, Clone, Copy)]
enum InQuotes {
No,
Single,
Double,
}
impl InQuotes {
fn quoted(self) -> bool {
matches!(self, Self::Single | Self::Double)
}
}
#[derive(Debug, Clone)]
#[must_use = "consume the iterator"]
pub struct QuotedStrSplitIter<'a> {
iter: std::str::Chars<'a>,
quotes: InQuotes,
current: String,
escaped: usize,
}
impl<'a> Iterator for QuotedStrSplitIter<'a> {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
loop {
#[allow(clippy::single_match_else)] let c = match self.iter.next() {
Some(c) => c,
None => {
if !self.current.is_empty() {
return Some(std::mem::take(&mut self.current));
}
return None;
}
};
if c == '\\' {
self.escaped += 1;
match self.escaped {
1 => continue,
2 => {
self.current.push('\\');
self.escaped = 0;
continue;
}
_ => {}
}
}
if self.escaped != 1 {
match c {
' ' if !self.quotes.quoted() => {
if self.current.is_empty() {
continue;
}
return Some(std::mem::replace(
&mut self.current,
String::with_capacity(16),
));
}
'"' => match self.quotes {
InQuotes::No => {
self.quotes = InQuotes::Double;
continue;
}
InQuotes::Double => {
self.quotes = InQuotes::No;
continue;
}
InQuotes::Single => {}
},
'\'' => match self.quotes {
InQuotes::No => {
self.quotes = InQuotes::Single;
continue;
}
InQuotes::Single => {
self.quotes = InQuotes::No;
continue;
}
InQuotes::Double => {}
},
_ => {}
}
}
if c != '\\' {
self.escaped = 0;
}
self.current.push(c);
}
}
}
pub fn quoted_str_split(s: &str) -> QuotedStrSplitIter {
QuotedStrSplitIter {
iter: s.chars(),
quotes: InQuotes::No,
current: String::with_capacity(16),
escaped: 0,
}
}
pub fn encode_quoted_str(src: &str, dest: &mut String) {
dest.reserve(src.len() + 2 + 4); dest.push('"');
for c in src.chars() {
match c {
'"' => dest.push_str("\\\""),
'\\' => dest.push_str("\\\\"),
_ => dest.push(c),
}
}
dest.push('"');
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_bytes() {
struct Spaces {
count: usize,
}
impl Spaces {
fn iter(&mut self) -> &mut Self {
self
}
fn len(&self) -> usize {
self.count
}
}
impl Iterator for Spaces {
type Item = u8;
fn next(&mut self) -> Option<Self::Item> {
if self.count == 0 {
return None;
}
self.count -= 1;
Some(chars::SPACE)
}
}
let mut spaces = Spaces { count: 1 };
let world = "world!".to_string().into_bytes();
let bytes = build_bytes!(b"Hello", spaces, &world);
assert_eq!(bytes, "Hello world!".as_bytes());
}
#[test]
fn starts_with() {
let example1 = "POST /api/username HTTP/3";
let example2 = "S_,Qasz>8!+}24_R?Z?j";
assert!(starts_with_any!(example1, "POST", "PUT", "DELETE", "PATCH"));
assert!(!starts_with_any!(
example2, "POST", "PUT", "DELETE", "PATCH", "S_,qasz"
));
}
#[test]
fn clean_debug() {
let message = "All\tOK.";
assert_eq!(format!("{:?}", message.as_clean()), message);
assert_ne!(format!("{:?}", message), message);
assert_eq!(format!("{:?}", message), r#""All\tOK.""#);
}
#[test]
fn writeable_bytes() {
use io::Write;
let mut bytes = WriteableBytes::new();
bytes.write_all(b"oh hi").unwrap();
bytes.write_all(&[chars::SPACE; 8]).unwrap();
bytes.write_all(b"bye").unwrap();
assert_eq!(bytes.into_inner().freeze(), "oh hi bye".as_bytes());
}
#[test]
fn body_length() {
let request1 = Request::options("/")
.body(Bytes::from_static(b"Hello!"))
.unwrap();
let request2 = Request::get("/api/update-status")
.header("content-length", HeaderValue::from_static("42"))
.body(Bytes::from_static(
b"{ name: \"Icelk\", status: \"Testing Kvarn\" } spurious data...",
))
.unwrap();
let request3 = Request::put("/api/status?name=icelk")
.header("content-length", HeaderValue::from_static("13"))
.body(Bytes::from_static(b"Testing Kvarn"))
.unwrap();
assert_eq!(get_body_length_request(&request1), 0);
assert_eq!(get_body_length_request(&request2), 0);
assert_eq!(get_body_length_request(&request3), 13);
let data =
Bytes::from_static(b"I refuses to brew coffee because I am, permanently, a teapot.");
let data_len = data.len();
let response1 = Response::builder()
.status(418)
.header(
"content-length",
HeaderValue::from_str(data_len.to_string().as_str()).unwrap(),
)
.body(data.clone())
.unwrap();
assert_eq!(
get_body_length_response(&response1, Some(&Method::GET)),
data_len
);
assert_eq!(get_body_length_response(&response1, None), data_len);
assert_eq!(get_body_length_response(&response1, Some(&Method::PUT)), 0);
let mut response2 = empty_clone_response(&response1).map(|()| data);
response2.headers_mut().insert(
"Content-Length",
HeaderValue::from_static("invalid content-length"),
);
assert_eq!(get_body_length_response(&response2, Some(&Method::GET)), 0);
}
#[test]
fn header_case_insensitive_equality() {
let mut headers = HeaderMap::default();
headers.append("referrer-policy", HeaderValue::from_static("no-referrer"));
headers.append("content-encoding", HeaderValue::from_static("gzip"));
assert!(header_eq(&headers, "referrer-policy", "NO-REFERRER"));
assert!(header_eq(&headers, "REFERRER-POLICY", "no-refeRrer"));
assert!(!header_eq(&headers, "REFERRER-POLICY", "NO_REFERRER"));
assert!(header_eq(&headers, "content-encoding", "gzip"));
assert!(header_eq(&headers, "Content-Encoding", "gzip"));
assert!(!header_eq(&headers, "Content_Encoding", "gzip"));
}
#[test]
fn path1() {
let path = make_path("public", "errors", "404", Some("html"));
assert_eq!(path, Path::new("public/errors/404.html"));
}
#[test]
fn path2() {
let path = make_path("public", "errors/static", "404", None);
assert_eq!(path, Path::new("public/errors/static/404"));
}
fn get_headers() -> HeaderMap {
let mut headers = HeaderMap::new();
headers.append("user-agent", HeaderValue::from_static("curl/7.64.1"));
headers.append(
"user-agent",
HeaderValue::from_static("Kvarn/0.2.0 (macOS)"),
);
headers.append("accept", HeaderValue::from_static("text/plain"));
headers
}
#[test]
fn header_management1() {
let mut headers = get_headers();
remove_all_headers(&mut headers, "user-agent");
assert_eq!(headers.get("user-agent"), None);
assert!(headers.get("accept").is_some());
}
#[test]
fn header_management2() {
let start = std::time::Instant::now();
let mut headers = get_headers();
headers.insert("user-agent", HeaderValue::from_str("tinyquest").unwrap());
let processing_time = start.elapsed().as_micros().to_string();
headers.insert(
"x-processing-time",
HeaderValue::from_str(&processing_time).unwrap(),
);
assert_eq!(
headers.get("user-agent"),
Some(&HeaderValue::from_static("tinyquest"))
);
assert_eq!(
headers.get("x-processing-time").unwrap().to_str().unwrap(),
&processing_time
);
assert!(headers.get("accept").is_some());
}
#[test]
fn header_management3() {
let mut headers = get_headers();
headers.insert("user-agent", HeaderValue::from_static("tinyquest"));
assert_eq!(
headers.get("user-agent"),
Some(&HeaderValue::from_static("tinyquest"))
);
assert!(headers.get("accept").is_some());
}
#[test]
fn quoted_str_split_1() {
let s = r#"this" should be" quoted. Is\ it?"#;
assert_eq!(
quoted_str_split(s).collect::<Vec<_>>(),
["this should be", "quoted.", "Is it?"]
);
}
#[test]
fn quoted_str_split_2() {
let s = r#" yay! this\\ works\ ! "#;
assert_eq!(
quoted_str_split(s).collect::<Vec<_>>(),
["yay!", "this\\", "works !"]
);
}
#[test]
fn quoted_str_split_3() {
let s = r#" how' bou't this?' no end to this quote "#;
assert_eq!(
quoted_str_split(s).collect::<Vec<_>>(),
["how bout", "this? no end to this quote "]
);
}
#[test]
fn quoted_str_split_4() {
let s = r#"Just normal quotes:\ \" and \'."#;
assert_eq!(
quoted_str_split(s).collect::<Vec<_>>(),
["Just", "normal", "quotes: \"", "and", "'."]
);
}
#[test]
fn quoted_str_split_5() {
let s = r#"This 'is a quote " inside a quote. "#;
assert_eq!(
quoted_str_split(s).collect::<Vec<_>>(),
["This", "is a quote \" inside a quote. "]
);
}
#[test]
fn quoted_str_split_6() {
let s = r#"h"i \\\\\" the"re"#;
assert_eq!(quoted_str_split(s).collect::<Vec<_>>(), [r#"hi \\" there"#]);
}
#[test]
fn quoted_str_encode_decode_1() {
let src = r#"program arg1 'arg "'two\ st\\\"il"l goes "on. 'third-arg "#;
let mut dest = String::new();
encode_quoted_str(src, &mut dest);
let mut iter = quoted_str_split(&dest);
assert_eq!(iter.next().unwrap(), src);
assert_eq!(iter.next(), None);
}
}