diff --git a/benches/benches/atoi.rs b/benches/benches/itoa.rs similarity index 81% rename from benches/benches/atoi.rs rename to benches/benches/itoa.rs index d19dfaf0..b06280f2 100644 --- a/benches/benches/atoi.rs +++ b/benches/benches/itoa.rs @@ -2,13 +2,14 @@ mod candiate {#![allow(unused)] + #[inline(always)] pub fn to_string(n: usize) -> String { n.to_string() } #[inline(always)] - pub fn atoi_01(mut n: usize) -> String { - ohkami_lib::num::atoi(n) + pub fn itoa(mut n: usize) -> String { + ohkami_lib::num::itoa(n) } } @@ -24,5 +25,5 @@ macro_rules! benchmark { )*}; } benchmark! { to_string - atoi_01 + itoa } diff --git a/ohkami/src/header/append.rs b/ohkami/src/header/append.rs new file mode 100644 index 00000000..ab1e9426 --- /dev/null +++ b/ohkami/src/header/append.rs @@ -0,0 +1,36 @@ +use std::borrow::Cow; + + +pub struct Append(pub(crate) Cow<'static, str>); + +/// Passed to `{Request/Response}.headers.set().Name( 〜 )` and +/// append `value` to the header. +/// +/// Here appended values are combined by `,`. +/// +/// --- +/// *example.rs* +/// ```no_run +/// use ohkami::prelude::*; +/// use ohkami::header::append; +/// +/// #[derive(Clone)] +/// struct AppendServer(&'static str); +/// impl FangAction for AppendServer { +/// async fn back<'b>(&'b self, res: &'b mut Response) { +/// res.headers.set() +/// .Server(append(self.0)); +/// } +/// } +/// +/// #[tokio::main] +/// async fn main() { +/// Ohkami::with(AppendServer("ohkami"), +/// "/".GET(|| async {"Hello, append!"}) +/// ).howl("localhost:3000").await +/// } +/// ``` +#[inline] +pub fn append(value: impl Into>) -> Append { + Append(value.into()) +} diff --git a/ohkami/src/header/mod.rs b/ohkami/src/header/mod.rs new file mode 100644 index 00000000..747d7f16 --- /dev/null +++ b/ohkami/src/header/mod.rs @@ -0,0 +1,11 @@ +#![allow(non_snake_case)] + +mod append; +pub use append::append; +pub(crate) use append::Append; + +mod setcookie; +pub(crate) use setcookie::*; + +mod standard; +pub(crate) use standard::Standard; diff --git a/ohkami/src/header/setcookie.rs b/ohkami/src/header/setcookie.rs new file mode 100644 index 00000000..9a9a127d --- /dev/null +++ b/ohkami/src/header/setcookie.rs @@ -0,0 +1,231 @@ +use std::borrow::Cow; + + +#[derive(Debug, PartialEq)] +pub enum SameSitePolicy { + Strict, + Lax, + None, +} +impl SameSitePolicy { + const fn as_str(&self) -> &'static str { + match self { + Self::Strict => "Strict", + Self::Lax => "Lax", + Self::None => "None", + } + } + const fn from_bytes(bytes: &[u8]) -> Option { + match bytes { + b"Strict" => Some(Self::Strict), + b"Lax" => Some(Self::Lax), + b"None" => Some(Self::None), + _ => None + } + } +} + +#[derive(Debug, PartialEq)] +pub struct SetCookie<'c> { + pub(crate) Cookie: (&'c str, Cow<'c, str>), + pub(crate) Expires: Option>, + pub(crate) MaxAge: Option, + pub(crate) Domain: Option>, + pub(crate) Path: Option>, + pub(crate) Secure: Option, + pub(crate) HttpOnly: Option, + pub(crate) SameSite: Option, +} +impl<'c> SetCookie<'c> { + pub fn Cookie(&self) -> (&str, &str) { + let (name, value) = &self.Cookie; + (name, &value) + } + pub fn Expires(&self) -> Option<&str> { + self.Expires.as_deref() + } + pub const fn MaxAge(&self) -> Option { + self.MaxAge + } + pub fn Domain(&self) -> Option<&str> { + self.Domain.as_deref() + } + pub fn Path(&self) -> Option<&str> { + self.Path.as_deref() + } + pub const fn Secure(&self) -> Option { + self.Secure + } + pub const fn HttpOnly(&self) -> Option { + self.HttpOnly + } + /// `Some`: `"Lax" | "None" | "Strict"` + pub const fn SameSite(&self) -> Option<&'static str> { + match &self.SameSite { + None => None, + Some(policy) => Some(policy.as_str()) + } + } + + pub(crate) fn from_raw(str: &'c str) -> Result { + let mut r = byte_reader::Reader::new(str.as_bytes()); + + let mut this = { + let name = std::str::from_utf8(r.read_until(b"=")).map_err(|e| format!("Invalid Cookie name: {e}"))?; + r.consume("=").ok_or_else(|| format!("No `=` found in a `Set-Cookie` header value"))?; + let value = ohkami_lib::percent_decode_utf8({ + let mut bytes = r.read_until(b"; "); + let len = bytes.len(); + if len >= 2 && bytes[0] == b'"' && bytes[len-1] == b'"' { + bytes = &bytes[1..(len-1)] + } + bytes + }).map_err(|e| format!("Invalid Cookie value: {e}"))?; + + Self { + Cookie: (name, value), + Expires: None, + MaxAge: None, + Domain: None, + Path: None, + SameSite: None, + Secure: None, + HttpOnly: None, + } + }; + + while r.consume("; ").is_some() { + let directive = r.read_until(b"; "); + let mut r = byte_reader::Reader::new(directive); + match r.consume_oneof([ + "Expires", "Max-Age", "Domain", "Path", "SameSite", "Secure", "HttpOnly" + ]) { + Some(0) => { + r.consume("=").ok_or_else(|| format!("Invalid `Expires`: No `=` found"))?; + let value = std::str::from_utf8(r.read_until(b"; ")).map_err(|e| format!("Invalid `Expires`: {e}"))?; + this.Expires = Some(Cow::Borrowed(value)) + }, + Some(1) => { + r.consume("=").ok_or_else(|| format!("Invalid `Max-Age`: No `=` found"))?; + let value = r.read_until(b"; ").iter().fold(0, |secs, d| 10*secs + (*d - b'0') as u64); + this.MaxAge = Some(value) + } + Some(2) => { + r.consume("=").ok_or_else(|| format!("Invalid `Domain`: No `=` found"))?; + let value = std::str::from_utf8(r.read_until(b"; ")).map_err(|e| format!("Invalid `Domain`: {e}"))?; + this.Domain = Some(Cow::Borrowed(value)) + }, + Some(3) => { + r.consume("=").ok_or_else(|| format!("Invalid `Path`: No `=` found"))?; + let value = std::str::from_utf8(r.read_until(b"; ")).map_err(|e| format!("Invalid `Path`: {e}"))?; + this.Path = Some(Cow::Borrowed(value)) + } + Some(4) => { + r.consume("=").ok_or_else(|| format!("Invalid `SameSite`: No `=` found"))?; + this.SameSite = SameSitePolicy::from_bytes(r.read_until(b"; ")); + } + Some(5) => this.Secure = Some(true), + Some(6) => this.HttpOnly = Some(true), + _ => return Err((|| format!("Unkown directive: `{}`", r.remaining().escape_ascii()))()) + } + } + + Ok(this) + } +} + +pub struct SetCookieBuilder(SetCookie<'static>); +impl SetCookieBuilder { + #[inline] + pub(crate) fn new(cookie_name: &'static str, cookie_value: impl Into>) -> Self { + Self(SetCookie { + Cookie: (cookie_name, cookie_value.into()), + Expires: None, MaxAge: None, Domain: None, Path: None, Secure: None, HttpOnly: None, SameSite: None, + }) + } + pub(crate) fn build(self) -> String { + let mut bytes = Vec::new(); + + let (name, value) = self.0.Cookie; { + bytes.extend_from_slice(name.as_bytes()); + bytes.push(b'='); + bytes.extend_from_slice(ohkami_lib::percent_encode(&value).as_bytes()); + } + if let Some(Expires) = self.0.Expires { + bytes.extend_from_slice(b"; Expires="); + bytes.extend_from_slice(Expires.as_bytes()); + } + if let Some(MaxAge) = self.0.MaxAge { + bytes.extend_from_slice(b"; Max-Age="); + bytes.extend_from_slice(MaxAge.to_string().as_bytes()); + } + if let Some(Domain) = self.0.Domain { + bytes.extend_from_slice(b"; Domain="); + bytes.extend_from_slice(Domain.as_bytes()); + } + if let Some(Path) = self.0.Path { + bytes.extend_from_slice(b"; Path="); + bytes.extend_from_slice(Path.as_bytes()); + } + if let Some(true) = self.0.Secure { + bytes.extend_from_slice(b"; Secure"); + } + if let Some(true) = self.0.HttpOnly { + bytes.extend_from_slice(b"; HttpOnly"); + } + if let Some(SameSite) = self.0.SameSite { + bytes.extend_from_slice(b"; SameSite="); + bytes.extend_from_slice(SameSite.as_str().as_bytes()); + } + + unsafe {// SAFETY: All fields and punctuaters is UTF-8 + String::from_utf8_unchecked(bytes) + } + } + + #[inline] + pub fn Expires(mut self, Expires: impl Into>) -> Self { + self.0.Expires = Some(Expires.into()); + self + } + #[inline] + pub const fn MaxAge(mut self, MaxAge: u64) -> Self { + self.0.MaxAge = Some(MaxAge); + self + } + #[inline] + pub fn Domain(mut self, Domain: impl Into>) -> Self { + self.0.Domain = Some(Domain.into()); + self + } + #[inline] + pub fn Path(mut self, Path: impl Into>) -> Self { + self.0.Path = Some(Path.into()); + self + } + #[inline] + pub const fn Secure(mut self) -> Self { + self.0.Secure = Some(true); + self + } + #[inline] + pub const fn HttpOnly(mut self) -> Self { + self.0.HttpOnly = Some(true); + self + } + #[inline] + pub const fn SameSiteLax(mut self) -> Self { + self.0.SameSite = Some(SameSitePolicy::Lax); + self + } + #[inline] + pub const fn SameSiteNone(mut self) -> Self { + self.0.SameSite = Some(SameSitePolicy::None); + self + } + #[inline] + pub const fn SameSiteStrict(mut self) -> Self { + self.0.SameSite = Some(SameSitePolicy::Strict); + self + } +} diff --git a/ohkami/src/header/standard.rs b/ohkami/src/header/standard.rs new file mode 100644 index 00000000..c3a77fe6 --- /dev/null +++ b/ohkami/src/header/standard.rs @@ -0,0 +1,72 @@ +pub(crate) struct Standard { + index: [u8; N], + values: Vec, +} + +impl Standard { + const NULL: u8 = u8::MAX; + + #[inline] + pub(crate) fn new() -> Self { + Self { + index: [Self::NULL; N], + values: Vec::with_capacity(N / 4) + } + } + + #[inline(always)] + pub(crate) unsafe fn get(&self, index: usize) -> Option<&Value> { + match *self.index.get_unchecked(index) { + Self::NULL => None, + index => Some(self.values.get_unchecked(index as usize)) + } + } + #[inline(always)] + pub(crate) unsafe fn get_mut(&mut self, index: usize) -> Option<&mut Value> { + match *self.index.get_unchecked(index) { + Self::NULL => None, + index => Some(self.values.get_unchecked_mut(index as usize)) + } + } + + #[inline(always)] + pub(crate) unsafe fn delete(&mut self, index: usize) { + *self.index.get_unchecked_mut(index) = Self::NULL + } + + #[inline(always)] + pub(crate) unsafe fn set(&mut self, index: usize, value: Value) { + *self.index.get_unchecked_mut(index) = self.values.len() as u8; + self.values.push(value); + } + + pub(crate) fn iter(&self) -> impl Iterator { + self.index.iter() + .enumerate() + .filter(|(_, index)| **index != Self::NULL) + .map(|(h, index)| unsafe {( + h, self.values.get_unchecked(*index as usize) + )}) + } +} + +const _: () = { + impl PartialEq for Standard { + fn eq(&self, other: &Self) -> bool { + for i in 0..N { + if unsafe {self.get(i)} != unsafe {other.get(i)} { + return false + } + }; true + } + } + + impl Clone for Standard { + fn clone(&self) -> Self { + Self { + index: self.index.clone(), + values: self.values.clone(), + } + } + } +}; diff --git a/ohkami/src/lib.rs b/ohkami/src/lib.rs index 0c4c4d5c..a7609827 100644 --- a/ohkami/src/lib.rs +++ b/ohkami/src/lib.rs @@ -99,6 +99,8 @@ mod ohkami; #[cfg(any(feature="rt_tokio",feature="rt_async-std",feature="rt_worker"))] pub use ohkami::{Ohkami, Route}; +pub mod header; + pub mod builtin; pub mod typed; @@ -123,7 +125,7 @@ pub mod utils { #[macro_export] macro_rules! push_unchecked { ($buf:ident <- $bytes:expr) => { - unsafe { + { let (buf_len, bytes_len) = ($buf.len(), $bytes.len()); std::ptr::copy_nonoverlapping( $bytes.as_ptr(), @@ -187,274 +189,6 @@ pub mod utils { #[cfg(feature="rt_worker")] pub use ::ohkami_macros::{worker, bindings}; -pub mod header {#![allow(non_snake_case)] - pub(crate) mod private { - use std::borrow::Cow; - - pub struct Append(pub(crate) Cow<'static, str>); - - #[derive(Debug, PartialEq)] - pub enum SameSitePolicy { - Strict, - Lax, - None, - } - impl SameSitePolicy { - const fn as_str(&self) -> &'static str { - match self { - Self::Strict => "Strict", - Self::Lax => "Lax", - Self::None => "None", - } - } - const fn from_bytes(bytes: &[u8]) -> Option { - match bytes { - b"Strict" => Some(Self::Strict), - b"Lax" => Some(Self::Lax), - b"None" => Some(Self::None), - _ => None - } - } - } - - #[derive(Debug, PartialEq)] - pub struct SetCookie<'c> { - pub(crate) Cookie: (&'c str, Cow<'c, str>), - pub(crate) Expires: Option>, - pub(crate) MaxAge: Option, - pub(crate) Domain: Option>, - pub(crate) Path: Option>, - pub(crate) Secure: Option, - pub(crate) HttpOnly: Option, - pub(crate) SameSite: Option, - } - impl<'c> SetCookie<'c> { - pub fn Cookie(&self) -> (&str, &str) { - let (name, value) = &self.Cookie; - (name, &value) - } - pub fn Expires(&self) -> Option<&str> { - self.Expires.as_deref() - } - pub const fn MaxAge(&self) -> Option { - self.MaxAge - } - pub fn Domain(&self) -> Option<&str> { - self.Domain.as_deref() - } - pub fn Path(&self) -> Option<&str> { - self.Path.as_deref() - } - pub const fn Secure(&self) -> Option { - self.Secure - } - pub const fn HttpOnly(&self) -> Option { - self.HttpOnly - } - /// `Some`: `"Lax" | "None" | "Strict"` - pub const fn SameSite(&self) -> Option<&'static str> { - match &self.SameSite { - None => None, - Some(policy) => Some(policy.as_str()) - } - } - - pub(crate) fn from_raw(str: &'c str) -> Result { - let mut r = byte_reader::Reader::new(str.as_bytes()); - - let mut this = { - let name = std::str::from_utf8(r.read_until(b"=")).map_err(|e| format!("Invalid Cookie name: {e}"))?; - r.consume("=").ok_or_else(|| format!("No `=` found in a `Set-Cookie` header value"))?; - let value = ohkami_lib::percent_decode_utf8({ - let mut bytes = r.read_until(b"; "); - let len = bytes.len(); - if len >= 2 && bytes[0] == b'"' && bytes[len-1] == b'"' { - bytes = &bytes[1..(len-1)] - } - bytes - }).map_err(|e| format!("Invalid Cookie value: {e}"))?; - - Self { - Cookie: (name, value), - Expires: None, - MaxAge: None, - Domain: None, - Path: None, - SameSite: None, - Secure: None, - HttpOnly: None, - } - }; - - while r.consume("; ").is_some() { - let directive = r.read_until(b"; "); - let mut r = byte_reader::Reader::new(directive); - match r.consume_oneof([ - "Expires", "Max-Age", "Domain", "Path", "SameSite", "Secure", "HttpOnly" - ]) { - Some(0) => { - r.consume("=").ok_or_else(|| format!("Invalid `Expires`: No `=` found"))?; - let value = std::str::from_utf8(r.read_until(b"; ")).map_err(|e| format!("Invalid `Expires`: {e}"))?; - this.Expires = Some(Cow::Borrowed(value)) - }, - Some(1) => { - r.consume("=").ok_or_else(|| format!("Invalid `Max-Age`: No `=` found"))?; - let value = r.read_until(b"; ").iter().fold(0, |secs, d| 10*secs + (*d - b'0') as u64); - this.MaxAge = Some(value) - } - Some(2) => { - r.consume("=").ok_or_else(|| format!("Invalid `Domain`: No `=` found"))?; - let value = std::str::from_utf8(r.read_until(b"; ")).map_err(|e| format!("Invalid `Domain`: {e}"))?; - this.Domain = Some(Cow::Borrowed(value)) - }, - Some(3) => { - r.consume("=").ok_or_else(|| format!("Invalid `Path`: No `=` found"))?; - let value = std::str::from_utf8(r.read_until(b"; ")).map_err(|e| format!("Invalid `Path`: {e}"))?; - this.Path = Some(Cow::Borrowed(value)) - } - Some(4) => { - r.consume("=").ok_or_else(|| format!("Invalid `SameSite`: No `=` found"))?; - this.SameSite = SameSitePolicy::from_bytes(r.read_until(b"; ")); - } - Some(5) => this.Secure = Some(true), - Some(6) => this.HttpOnly = Some(true), - _ => return Err((|| format!("Unkown directive: `{}`", r.remaining().escape_ascii()))()) - } - } - - Ok(this) - } - } - - pub struct SetCookieBuilder(SetCookie<'static>); - impl SetCookieBuilder { - #[inline] - pub(crate) fn new(cookie_name: &'static str, cookie_value: impl Into>) -> Self { - Self(SetCookie { - Cookie: (cookie_name, cookie_value.into()), - Expires: None, MaxAge: None, Domain: None, Path: None, Secure: None, HttpOnly: None, SameSite: None, - }) - } - pub(crate) fn build(self) -> String { - let mut bytes = Vec::new(); - - let (name, value) = self.0.Cookie; { - bytes.extend_from_slice(name.as_bytes()); - bytes.push(b'='); - bytes.extend_from_slice(ohkami_lib::percent_encode(&value).as_bytes()); - } - if let Some(Expires) = self.0.Expires { - bytes.extend_from_slice(b"; Expires="); - bytes.extend_from_slice(Expires.as_bytes()); - } - if let Some(MaxAge) = self.0.MaxAge { - bytes.extend_from_slice(b"; Max-Age="); - bytes.extend_from_slice(MaxAge.to_string().as_bytes()); - } - if let Some(Domain) = self.0.Domain { - bytes.extend_from_slice(b"; Domain="); - bytes.extend_from_slice(Domain.as_bytes()); - } - if let Some(Path) = self.0.Path { - bytes.extend_from_slice(b"; Path="); - bytes.extend_from_slice(Path.as_bytes()); - } - if let Some(true) = self.0.Secure { - bytes.extend_from_slice(b"; Secure"); - } - if let Some(true) = self.0.HttpOnly { - bytes.extend_from_slice(b"; HttpOnly"); - } - if let Some(SameSite) = self.0.SameSite { - bytes.extend_from_slice(b"; SameSite="); - bytes.extend_from_slice(SameSite.as_str().as_bytes()); - } - - unsafe {// SAFETY: All fields and punctuaters is UTF-8 - String::from_utf8_unchecked(bytes) - } - } - - #[inline] - pub fn Expires(mut self, Expires: impl Into>) -> Self { - self.0.Expires = Some(Expires.into()); - self - } - #[inline] - pub const fn MaxAge(mut self, MaxAge: u64) -> Self { - self.0.MaxAge = Some(MaxAge); - self - } - #[inline] - pub fn Domain(mut self, Domain: impl Into>) -> Self { - self.0.Domain = Some(Domain.into()); - self - } - #[inline] - pub fn Path(mut self, Path: impl Into>) -> Self { - self.0.Path = Some(Path.into()); - self - } - #[inline] - pub const fn Secure(mut self) -> Self { - self.0.Secure = Some(true); - self - } - #[inline] - pub const fn HttpOnly(mut self) -> Self { - self.0.HttpOnly = Some(true); - self - } - #[inline] - pub const fn SameSiteLax(mut self) -> Self { - self.0.SameSite = Some(SameSitePolicy::Lax); - self - } - #[inline] - pub const fn SameSiteNone(mut self) -> Self { - self.0.SameSite = Some(SameSitePolicy::None); - self - } - #[inline] - pub const fn SameSiteStrict(mut self) -> Self { - self.0.SameSite = Some(SameSitePolicy::Strict); - self - } - } - } - - /// Passed to `{Request/Response}.headers.set().Name( 〜 )` and - /// append `value` to the header. - /// - /// Here appended values are combined by `,`. - /// - /// --- - /// *example.rs* - /// ```no_run - /// use ohkami::prelude::*; - /// use ohkami::header::append; - /// - /// #[derive(Clone)] - /// struct AppendServer(&'static str); - /// impl FangAction for AppendServer { - /// async fn back<'b>(&'b self, res: &'b mut Response) { - /// res.headers.set() - /// .Server(append(self.0)); - /// } - /// } - /// - /// #[tokio::main] - /// async fn main() { - /// Ohkami::with(AppendServer("ohkami"), - /// "/".GET(|| async {"Hello, append!"}) - /// ).howl("localhost:3000").await - /// } - /// ``` - pub fn append(value: impl Into>) -> private::Append { - private::Append(value.into()) - } -} - pub mod prelude { pub use crate::{Request, Response, IntoResponse, Method, Status}; pub use crate::utils::FangAction; diff --git a/ohkami/src/request/headers.rs b/ohkami/src/request/headers.rs index b7149604..0d86b9df 100644 --- a/ohkami/src/request/headers.rs +++ b/ohkami/src/request/headers.rs @@ -1,74 +1,18 @@ +use crate::header::{Standard, Append}; use std::borrow::Cow; -use crate::header::private::Append; use ohkami_lib::{CowSlice, Slice}; use rustc_hash::FxHashMap; pub struct Headers { - standard: Standard, + standard: Standard, custom: Option>>, } -struct Standard { - index: [u8; N_CLIENT_HEADERS], - values: Vec, -} impl Standard { - const NULL: u8 = u8::MAX; - - #[cfg(any(feature="rt_tokio",feature="rt_async-std",feature="rt_worker"))] - #[inline] - fn new() -> Self { - Self { - index: [Self::NULL; N_CLIENT_HEADERS], - values: Vec::with_capacity(N_CLIENT_HEADERS / 4) - } - } - - #[inline(always)] - fn get(&self, name: Header) -> Option<&CowSlice> { - unsafe {match *self.index.get_unchecked(name as usize) { - Self::NULL => None, - index => Some(self.values.get_unchecked(index as usize)) - }} - } - - #[inline(always)] - fn remove(&mut self, name: Header) { - unsafe {*self.index.get_unchecked_mut(name as usize) = Self::NULL} - } - - #[inline(always)] - fn insert(&mut self, name: Header, value: CowSlice) { - unsafe {*self.index.get_unchecked_mut(name as usize) = self.values.len() as u8} - self.values.push(value); - } - - #[inline(always)] - fn append(&mut self, name: Header, value: CowSlice) { - unsafe {match *self.index.get_unchecked(name as usize) { - Self::NULL => self.insert(name, value), - index => (|index| { - let target = self.values.get_unchecked_mut(index as usize); - target.extend_from_slice(b", "); - target.extend_from_slice(&value); - })(index) - }} - } - - fn iter(&self) -> impl Iterator { - self.index.iter() - .enumerate() - .filter(|(_, index)| **index != Self::NULL) - .map(|(h, index)| unsafe {( - std::mem::transmute::<_, Header>(h as u8).as_str(), - std::str::from_utf8(self.values.get_unchecked(*index as usize)).expect("non UTF-8 header value") - )}) - } -} - pub struct SetHeaders<'set>( &'set mut Headers ); impl Headers { + #[inline] pub fn set(&mut self) -> SetHeaders<'_> { SetHeaders(self) } @@ -79,6 +23,7 @@ pub trait HeaderAction<'set> { } const _: () = { // remove impl<'set> HeaderAction<'set> for Option<()> { + #[inline] fn perform(self, set: SetHeaders<'set>, key: Header) -> SetHeaders<'set> { set.0.remove(key); set @@ -87,6 +32,7 @@ pub trait HeaderAction<'set> { // append impl<'set> HeaderAction<'set> for Append { + #[inline] fn perform(self, set: SetHeaders<'set>, key: Header) -> SetHeaders<'set> { set.0.append(key, self.0.into()); set @@ -116,8 +62,7 @@ pub trait HeaderAction<'set> { pub trait CustomHeadersAction<'set> { fn perform(self, set: SetHeaders<'set>, key: &'static str) -> SetHeaders<'set>; -} -const _: () = { +} const _: () = { // remove impl<'set> CustomHeadersAction<'set> for Option<()> { fn perform(self, set: SetHeaders<'set>, key: &'static str) -> SetHeaders<'set> { @@ -131,19 +76,7 @@ const _: () = { // append impl<'set> CustomHeadersAction<'set> for Append { fn perform(self, set: SetHeaders<'set>, key: &'static str) -> SetHeaders<'set> { - let key = Slice::from_bytes(key.as_bytes()); - let custom = set.0.get_or_init_custom_mut(); - - match custom.get_mut(&key) { - Some(v) => unsafe { - v.extend_from_slice(b", "); - v.extend_from_slice(self.0.as_bytes()); - } - None => { - custom.insert(key, CowSlice::from(self.0)); - } - } - + set.0.append_custom(Slice::from_bytes(key.as_bytes()), self.0.into()); set } } @@ -151,7 +84,7 @@ const _: () = { // insert impl<'set> CustomHeadersAction<'set> for &'static str { fn perform(self, set: SetHeaders<'set>, key: &'static str) -> SetHeaders<'set> { - set.0.get_or_init_custom_mut().insert( + set.0.insert_custom( Slice::from_bytes(key.as_bytes()), CowSlice::Ref(Slice::from_bytes(self.as_bytes())) ); @@ -160,7 +93,7 @@ const _: () = { } impl<'set> CustomHeadersAction<'set> for String { fn perform(self, set: SetHeaders<'set>, key: &'static str) -> SetHeaders<'set> { - set.0.get_or_init_custom_mut().insert( + set.0.insert_custom( Slice::from_bytes(key.as_bytes()), CowSlice::Own(self.into_bytes().into_boxed_slice()) ); @@ -169,7 +102,7 @@ const _: () = { } impl<'set> CustomHeadersAction<'set> for Cow<'static, str> { fn perform(self, set: SetHeaders<'set>, key: &'static str) -> SetHeaders<'set> { - set.0.get_or_init_custom_mut().insert( + set.0.insert_custom( Slice::from_bytes(key.as_bytes()), CowSlice::from(self) ); @@ -329,17 +262,13 @@ impl Headers { }) ).into_iter().flatten() } -} - -impl Headers { - #[inline(always)] - fn get_or_init_custom_mut(&mut self) -> &mut FxHashMap { - self.custom.is_none().then(|| self.custom = Some(Box::new(FxHashMap::default()))); - unsafe {self.custom.as_mut().unwrap_unchecked()} - } pub(crate) fn iter(&self) -> impl Iterator { self.standard.iter() + .map(|(i, v)| ( + unsafe {std::mem::transmute::<_, Header>(i as u8).as_str()}, + std::str::from_utf8(v).expect("Non UTF-8 header value") + )) .chain(self.custom.as_ref() .into_iter() .flat_map(|hm| hm.iter().map(|(k, v)| ( @@ -353,7 +282,7 @@ impl Headers { impl Headers { #[inline(always)] pub(crate) fn insert(&mut self, name: Header, value: CowSlice) { - self.standard.insert(name, value) + unsafe {self.standard.set(name as usize, value)} } #[cfg(feature="DEBUG")] #[inline(always)] pub fn _insert(&mut self, name: Header, value: CowSlice) { @@ -361,30 +290,29 @@ impl Headers { } pub(crate) fn remove(&mut self, name: Header) { - self.standard.remove(name) + unsafe {self.standard.delete(name as usize)} } #[inline] pub(crate) fn get(&self, name: Header) -> Option<&str> { - match self.standard.get(name) { + unsafe {match self.standard.get(name as usize) { Some(cs) => Some(std::str::from_utf8(&cs).expect("non UTF-8 header value")), None => None - } + }} } #[inline(always)] pub(crate) fn append(&mut self, name: Header, value: CowSlice) { - self.standard.append(name, value) + unsafe {match self.standard.get_mut(name as usize) { + None => self.standard.set(name as usize, value), + Some(v) => { + v.extend_from_slice(b", "); + v.extend_from_slice(&value); + } + }} } } -#[cfg(any(feature="rt_tokio",feature="rt_async-std",feature="rt_worker"))] impl Headers { - #[allow(unused)] - #[inline] pub(crate) fn get_raw(&self, name: Header) -> Option<&CowSlice> { - self.standard.get(name) - } - - #[allow(unused)] #[inline] pub(crate) fn insert_custom(&mut self, name: Slice, value: CowSlice) { match &mut self.custom { Some(c) => {c.insert(name, value);} @@ -398,17 +326,20 @@ impl Headers { self.insert_custom(name, value) } - #[allow(unused)] #[inline] pub(crate) fn append_custom(&mut self, name: Slice, value: CowSlice) { - let custom = self.get_or_init_custom_mut(); + if self.custom.is_none() { + self.custom = Some(Box::new(FxHashMap::default())) + } - match custom.get_mut(&name) { + let c = unsafe {self.custom.as_mut().unwrap_unchecked()}; + + match c.get_mut(&name) { Some(v) => unsafe { - v.extend_from_slice(b","); + v.extend_from_slice(b", "); v.extend_from_slice(value.as_bytes()); } None => { - custom.insert(name, value); + c.insert(name, value); } } } @@ -428,6 +359,10 @@ impl Headers { Self::init() } + #[inline] pub(crate) fn get_raw(&self, name: Header) -> Option<&CowSlice> { + unsafe {self.standard.get(name as usize)} + } + #[allow(unused)] #[cfg(test)] pub(crate) fn from_iters( iter: impl IntoIterator, @@ -478,16 +413,4 @@ const _: () = { self.standard == other.standard } } - - impl PartialEq for Standard { - fn eq(&self, other: &Self) -> bool { - fn sort_collect<'s>(iter: impl Iterator) -> Vec<(&'s str, &'s str)> { - let mut collect = iter.collect::>(); - collect.sort_by_key(|(k, _)| *k); - collect - } - - sort_collect(self.iter()) == sort_collect(other.iter()) - } - } }; diff --git a/ohkami/src/response/_test_headers.rs b/ohkami/src/response/_test_headers.rs index c86b356f..f9f392d0 100644 --- a/ohkami/src/response/_test_headers.rs +++ b/ohkami/src/response/_test_headers.rs @@ -1,6 +1,6 @@ #![cfg(any(feature="rt_tokio",feature="rt_async-std"))] -use crate::header::{append, private::{SameSitePolicy, SetCookie}}; +use crate::header::{append, SameSitePolicy, SetCookie}; use super::ResponseHeaders; diff --git a/ohkami/src/response/headers.rs b/ohkami/src/response/headers.rs index c53e4851..689749a0 100644 --- a/ohkami/src/response/headers.rs +++ b/ohkami/src/response/headers.rs @@ -1,72 +1,19 @@ -use crate::header::private::{Append, SetCookie, SetCookieBuilder}; +use crate::header::{Standard, Append, SetCookie, SetCookieBuilder}; use std::borrow::Cow; use rustc_hash::FxHashMap; #[derive(Clone)] pub struct Headers { - standard: Standard, + standard: Standard>, custom: Option>>>, setcookie: Option>>>, pub(crate) size: usize, } -#[derive(PartialEq, Clone)] -struct Standard { - index: [u8; N_SERVER_HEADERS], - values: Vec>, -} impl Standard { - const NULL: u8 = u8::MAX; - - #[inline] - fn new() -> Self { - Self { - index: [Self::NULL; N_SERVER_HEADERS], - values: Vec::with_capacity(N_SERVER_HEADERS / 4) - } - } - - #[inline(always)] - fn get(&self, name: Header) -> Option<&Cow<'static, str>> { - unsafe {match *self.index.get_unchecked(name as usize) { - Self::NULL => None, - index => Some(self.values.get_unchecked(index as usize)) - }} - } - #[inline(always)] - fn get_mut(&mut self, name: Header) -> Option<&mut Cow<'static, str>> { - unsafe {match *self.index.get_unchecked(name as usize) { - Self::NULL => None, - index => Some(self.values.get_unchecked_mut(index as usize)) - }} - } - - #[inline(always)] - fn delete(&mut self, name: Header) { - unsafe {*self.index.get_unchecked_mut(name as usize) = Self::NULL} - } - - #[inline(always)] - fn push(&mut self, name: Header, value: Cow<'static, str>) { - unsafe {*self.index.get_unchecked_mut(name as usize) = self.values.len() as u8} - self.values.push(value); - } - - fn iter(&self) -> impl Iterator { - self.index.iter() - .enumerate() - .filter(|(_, index)| **index != Self::NULL) - .map(|(h, index)| unsafe {( - std::mem::transmute::<_, Header>(h as u8).as_str(), - &**self.values.get_unchecked(*index as usize) - )}) - } -} - pub struct SetHeaders<'set>( &'set mut Headers -); -impl Headers { +); impl Headers { #[inline] pub fn set(&mut self) -> SetHeaders<'_> { SetHeaders(self) } @@ -114,8 +61,7 @@ pub trait HeaderAction<'action> { pub trait CustomHeadersAction<'action> { fn perform(self, set: SetHeaders<'action>, key: &'static str) -> SetHeaders<'action>; -} -const _: () = { +} const _: () = { /* remove */ impl<'set> CustomHeadersAction<'set> for Option<()> { #[inline] @@ -128,100 +74,27 @@ const _: () = { /* append */ impl<'set> CustomHeadersAction<'set> for Append { fn perform(self, set: SetHeaders<'set>, key: &'static str) -> SetHeaders<'set> { - let self_len = self.0.len(); - - let custom = { - if set.0.custom.is_none() { - set.0.custom = Some(Box::new(FxHashMap::default())); - } - unsafe {set.0.custom.as_mut().unwrap_unchecked()} - }; - - set.0.size += match custom.get_mut(&key) { - Some(value) => { - match value { - Cow::Owned(string) => { - string.push_str(", "); - string.push_str(&self.0); - } - Cow::Borrowed(s) => { - let mut s = s.to_string(); - s.push_str(", "); - s.push_str(&self.0); - *value = Cow::Owned(s); - } - } - ", ".len() + self_len - } - None => { - custom.insert(key, self.0); - key.len() + ": ".len() + self_len + "\r\n".len() - } - }; - + set.0.append_custom(key, self.0); set } } /* insert */ - // specialize for `&'static str`: - // NOT perform `let` binding of `self.len()`, using inlined `self.len()` instead. impl<'set> CustomHeadersAction<'set> for &'static str { #[inline(always)] fn perform(self, set: SetHeaders<'set>, key: &'static str) -> SetHeaders<'set> { - match &mut set.0.custom { - None => { - set.0.custom = Some(Box::new(FxHashMap::from_iter([(key, Cow::Borrowed(self))]))); - set.0.size += key.len() + ": ".len() + self.len() + "\r\n".len() - } - Some(custom) => { - if let Some(old) = custom.insert(key, Cow::Borrowed(self)) { - set.0.size -= old.len(); - set.0.size += self.len(); - } else { - set.0.size += key.len() + ": ".len() + self.len() + "\r\n".len() - } - } - } + set.0.insert_custom(key, Cow::Borrowed(self)); set } } impl<'set> CustomHeadersAction<'set> for String { #[inline(always)] fn perform(self, set: SetHeaders<'set>, key: &'static str) -> SetHeaders<'set> { - let self_len = self.len(); - match &mut set.0.custom { - None => { - set.0.custom = Some(Box::new(FxHashMap::from_iter([(key, Cow::Owned(self))]))); - set.0.size += key.len() + ": ".len() + self_len + "\r\n".len() - } - Some(custom) => { - if let Some(old) = custom.insert(key, Cow::Owned(self)) { - set.0.size -= old.len(); - set.0.size += self_len; - } else { - set.0.size += key.len() + ": ".len() + self_len + "\r\n".len() - } - } - } + set.0.insert_custom(key, Cow::Owned(self)); set } } impl<'set> CustomHeadersAction<'set> for Cow<'static, str> { fn perform(self, set: SetHeaders<'set>, key: &'static str) -> SetHeaders<'set> { - let self_len = self.len(); - match &mut set.0.custom { - None => { - set.0.custom = Some(Box::new(FxHashMap::from_iter([(key, self)]))); - set.0.size += key.len() + ": ".len() + self_len + "\r\n".len() - } - Some(custom) => { - if let Some(old) = custom.insert(key, self) { - set.0.size -= old.len(); - set.0.size += self_len; - } else { - set.0.size += key.len() + ": ".len() + self_len + "\r\n".len() - } - } - } + set.0.insert_custom(key, self); set } } @@ -343,9 +216,9 @@ macro_rules! Header { Upgrade: b"Upgrade", Vary: b"Vary", Via: b"Via", - WWWAuthenticate: b"WWW-Authenticate", XContentTypeOptions: b"X-Content-Type-Options", XFrameOptions: b"X-Frame-Options", + WWWAuthenticate: b"WWW-Authenticate", } const _: () = { @@ -408,10 +281,10 @@ impl Headers { #[inline(always)] pub(crate) fn insert(&mut self, name: Header, value: Cow<'static, str>) { let (name_len, value_len) = (name.len(), value.len()); - match self.standard.get_mut(name) { + match unsafe {self.standard.get_mut(name as usize)} { None => { self.size += name_len + ": ".len() + value_len + "\r\n".len(); - self.standard.push(name, value) + unsafe {self.standard.set(name as usize, value)} } Some(old) => { self.size -= old.len(); self.size += value_len; @@ -419,14 +292,31 @@ impl Headers { } } } + #[inline] + pub(crate) fn insert_custom(&mut self, name: &'static str, value: Cow<'static, str>) { + let self_len = value.len(); + match &mut self.custom { + None => { + self.custom = Some(Box::new(FxHashMap::from_iter([(name, value)]))); + self.size += name.len() + ": ".len() + self_len + "\r\n".len() + } + Some(custom) => { + if let Some(old) = custom.insert(name, value) { + self.size -= old.len(); self.size += self_len; + } else { + self.size += name.len() + ": ".len() + self_len + "\r\n".len() + } + } + } + } #[inline] pub(crate) fn remove(&mut self, name: Header) { let name_len = name.len(); - if let Some(v) = self.standard.get(name) { + if let Some(v) = unsafe {self.standard.get(name as usize)} { self.size -= name_len + ": ".len() + v.len() + "\r\n".len() } - self.standard.delete(name) + unsafe {self.standard.delete(name as usize)} } pub(crate) fn remove_custom(&mut self, name: &'static str) { if let Some(c) = self.custom.as_mut() { @@ -438,7 +328,7 @@ impl Headers { #[inline(always)] pub(crate) fn get(&self, name: Header) -> Option<&str> { - self.standard.get(name).map(Cow::as_ref) + unsafe {self.standard.get(name as usize)}.map(Cow::as_ref) } #[inline] pub(crate) fn get_custom(&self, name: &'static str) -> Option<&str> { @@ -449,7 +339,7 @@ impl Headers { pub(crate) fn append(&mut self, name: Header, value: Cow<'static, str>) { let value_len = value.len(); - let target = self.standard.get_mut(name); + let target = unsafe {self.standard.get_mut(name as usize)}; self.size += match target { Some(v) => { @@ -469,7 +359,39 @@ impl Headers { ", ".len() + value_len } None => { - self.standard.push(name, value); + unsafe {self.standard.set(name as usize, value)} + name.len() + ": ".len() + value_len + "\r\n".len() + } + }; + } + pub(crate) fn append_custom(&mut self, name: &'static str, value: Cow<'static, str>) { + let value_len = value.len(); + + let custom = { + if self.custom.is_none() { + self.custom = Some(Box::new(FxHashMap::default())); + } + unsafe {self.custom.as_mut().unwrap_unchecked()} + }; + + self.size += match custom.get_mut(name) { + Some(v) => { + match v { + Cow::Owned(string) => { + string.push_str(", "); + string.push_str(&value); + } + Cow::Borrowed(s) => { + let mut s = s.to_string(); + s.push_str(", "); + s.push_str(&value); + *v = Cow::Owned(s); + } + } + ", ".len() + value_len + } + None => { + custom.insert(name, value); name.len() + ": ".len() + value_len + "\r\n".len() } }; @@ -492,10 +414,14 @@ impl Headers { pub(crate) fn iter_standard(&self) -> impl Iterator { self.standard.iter() + .map(|(i, v)| ( + unsafe {std::mem::transmute::<_, Header>(i as u8)}.as_str(), + &**v + )) } pub(crate) fn iter(&self) -> impl Iterator { - self.standard.iter() + self.iter_standard() .chain(self.custom.as_ref() .into_iter() .flat_map(|hm| hm.iter().map(|(k, v)| (*k, &**v))) @@ -514,8 +440,8 @@ impl Headers { /// SAFETY: `buf` has remaining capacity of at least `self.size` pub(crate) unsafe fn write_unchecked_to(&self, buf: &mut Vec) { for n in 0..N_SERVER_HEADERS { - let h = std::mem::transmute(n as u8); - if let Some(v) = self.standard.get(h) { + let h = std::mem::transmute::<_, Header>(n as u8); + if let Some(v) = self.standard.get(h as usize) { crate::push_unchecked!(buf <- h.as_bytes()); crate::push_unchecked!(buf <- b": "); crate::push_unchecked!(buf <- v.as_bytes()); diff --git a/ohkami/src/response/mod.rs b/ohkami/src/response/mod.rs index dbb51bc7..ac4e231f 100644 --- a/ohkami/src/response/mod.rs +++ b/ohkami/src/response/mod.rs @@ -138,7 +138,7 @@ impl Response { } Content::Payload(bytes) => self.headers.set() - .ContentLength(ohkami_lib::num::atoi(bytes.len())), + .ContentLength(ohkami_lib::num::itoa(bytes.len())), #[cfg(feature="sse")] Content::Stream(_) => self.headers.set() @@ -155,27 +155,40 @@ impl Response { match self.content { Content::None => { - let mut buf = Vec::::with_capacity(self.status.line().len() + self.headers.size); - crate::push_unchecked!(buf <- self.status.line()); - unsafe {self.headers.write_unchecked_to(&mut buf)} + let mut buf = Vec::::with_capacity( + self.status.line().len() + + self.headers.size + ); unsafe { + crate::push_unchecked!(buf <- self.status.line()); + self.headers.write_unchecked_to(&mut buf); + } conn.write_all(&buf).await.expect("Failed to send response"); } Content::Payload(bytes) => { - let mut buf = Vec::::with_capacity(self.status.line().len() + self.headers.size + bytes.len()); - crate::push_unchecked!(buf <- self.status.line()); - unsafe {self.headers.write_unchecked_to(&mut buf)} - crate::push_unchecked!(buf <- bytes); + let mut buf = Vec::::with_capacity( + self.status.line().len() + + self.headers.size + + bytes.len() + ); unsafe { + crate::push_unchecked!(buf <- self.status.line()); + self.headers.write_unchecked_to(&mut buf); + crate::push_unchecked!(buf <- bytes); + } conn.write_all(&buf).await.expect("Failed to send response"); } #[cfg(feature="sse")] Content::Stream(mut stream) => { - let mut buf = Vec::::with_capacity(self.status.line().len() + self.headers.size); - crate::push_unchecked!(buf <- self.status.line()); - unsafe {self.headers.write_unchecked_to(&mut buf)} + let mut buf = Vec::::with_capacity( + self.status.line().len() + + self.headers.size + ); unsafe { + crate::push_unchecked!(buf <- self.status.line()); + self.headers.write_unchecked_to(&mut buf); + } conn.write_all(&buf).await.expect("Failed to send response"); while let Some(chunk) = stream.next().await { diff --git a/ohkami_lib/src/num.rs b/ohkami_lib/src/num.rs index d0d0d4a2..b96ddae3 100644 --- a/ohkami_lib/src/num.rs +++ b/ohkami_lib/src/num.rs @@ -36,7 +36,7 @@ pub fn hexized_bytes(n: usize) -> [u8; std::mem::size_of::() * 2] { #[inline] -pub fn atoi(mut n: usize) -> String { +pub fn itoa(mut n: usize) -> String { let log10 = match usize::checked_ilog10(n) { Some(log10) => log10 as usize, None => return String::from("0") @@ -56,7 +56,7 @@ pub fn atoi(mut n: usize) -> String { } #[cfg(test)] -#[test] fn test_atoi() { +#[test] fn test_itoa() { for (n, expected) in [ (0, "0"), (1, "1"), @@ -69,6 +69,6 @@ pub fn atoi(mut n: usize) -> String { (999, "999"), (1000, "1000"), ] { - assert_eq!(atoi(n), expected) + assert_eq!(itoa(n), expected) } }