use arrayvec::ArrayString;
use std::{
borrow::{Borrow, Cow},
cmp::Ordering,
fmt,
hash::{Hash, Hasher},
ops::Deref,
};
#[derive(Clone)]
pub struct Scheme(Cow<'static, str>);
impl Scheme {
pub const HTTP: Self = Self(Cow::Borrowed("http"));
pub const HTTPS: Self = Self(Cow::Borrowed("https"));
pub fn as_str(&self) -> &str {
&self.0
}
fn into_string(self) -> String {
self.0.into_owned()
}
}
impl Scheme {
fn known(value: &str) -> Option<Self> {
if value.eq_ignore_ascii_case("http") {
Some(Self::HTTP)
} else if value.eq_ignore_ascii_case("https") {
Some(Self::HTTPS)
} else {
None
}
}
}
impl From<&str> for Scheme {
fn from(value: &str) -> Self {
Self::known(value).unwrap_or_else(|| Self(Cow::Owned(value.to_ascii_lowercase())))
}
}
impl From<String> for Scheme {
fn from(mut value: String) -> Self {
Self::known(&value).unwrap_or_else(|| {
value.make_ascii_lowercase();
Self(Cow::Owned(value))
})
}
}
#[inline]
fn lowercase(value: &mut str) {
if has_uppercase(value.as_bytes()) {
value.make_ascii_lowercase();
}
}
#[inline]
fn has_uppercase(bytes: &[u8]) -> bool {
const ONES: u64 = u64::MAX / 255;
let word = |word: u64| {
let ascii = word & (0x7f * ONES);
(ascii + (0x80 - b'A') as u64 * ONES)
& !(ascii + (0x80 - b'Z' - 1) as u64 * ONES)
& !word
& (0x80 * ONES)
!= 0
};
let load = |at: usize| u64::from_le_bytes(bytes[at..at + 8].try_into().unwrap());
if bytes.len() < 8 {
return bytes.iter().any(u8::is_ascii_uppercase);
}
let mut at = 0;
while at + 8 < bytes.len() {
if word(load(at)) {
return true;
}
at += 8;
}
word(load(bytes.len() - 8))
}
#[inline]
fn preserve(_: &mut str) {}
#[derive(Clone)]
enum Storage {
Inline(ArrayString<24>),
Heap(String),
}
macro_rules! stored {
($($(#[$meta:meta])* $name:ident => $normalize:ident),*) => {$(
$(#[$meta])*
#[derive(Clone)]
pub struct $name(Storage);
impl $name {
pub fn as_str(&self) -> &str {
match &self.0 {
Storage::Inline(value) => value.as_str(),
Storage::Heap(value) => value,
}
}
fn into_string(self) -> String {
match self.0 {
Storage::Inline(value) => value.as_str().to_owned(),
Storage::Heap(value) => value,
}
}
}
impl From<&str> for $name {
#[inline]
fn from(value: &str) -> Self {
match ArrayString::from(value) {
Ok(mut value) => {
$normalize(&mut value);
Self(Storage::Inline(value))
}
Err(_) => {
let mut value = value.to_owned();
$normalize(&mut value);
Self(Storage::Heap(value))
}
}
}
}
impl From<String> for $name {
#[inline]
fn from(mut value: String) -> Self {
match ArrayString::from(&value) {
Ok(mut inline) => {
$normalize(&mut inline);
Self(Storage::Inline(inline))
}
Err(_) => {
$normalize(&mut value);
Self(Storage::Heap(value))
}
}
}
}
)*};
}
stored! {
Authority => lowercase,
Path => preserve
}
macro_rules! text {
($($name:ident),*) => {$(
impl From<&String> for $name {
fn from(value: &String) -> Self {
Self::from(value.as_str())
}
}
impl From<Box<str>> for $name {
fn from(value: Box<str>) -> Self {
Self::from(String::from(value))
}
}
impl From<Cow<'_, str>> for $name {
fn from(value: Cow<'_, str>) -> Self {
match value {
Cow::Borrowed(value) => Self::from(value),
Cow::Owned(value) => Self::from(value),
}
}
}
impl From<$name> for String {
fn from(value: $name) -> Self {
value.into_string()
}
}
impl Deref for $name {
type Target = str;
fn deref(&self) -> &str {
self.as_str()
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl Borrow<str> for $name {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for $name {
fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
output.write_str(self.as_str())
}
}
impl fmt::Debug for $name {
fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
output.debug_tuple(stringify!($name)).field(&self.as_str()).finish()
}
}
impl PartialEq for $name {
fn eq(&self, other: &Self) -> bool {
self.as_str() == other.as_str()
}
}
impl Eq for $name {}
impl Hash for $name {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_str().hash(state)
}
}
impl PartialOrd for $name {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for $name {
fn cmp(&self, other: &Self) -> Ordering {
self.as_str().cmp(other.as_str())
}
}
text!(@eq $name, str);
text!(@eq $name, &str);
text!(@eq $name, String);
)*};
(@eq $name:ident, $other:ty) => {
impl PartialEq<$other> for $name {
fn eq(&self, other: &$other) -> bool {
self.as_str() == &other[..]
}
}
impl PartialEq<$name> for $other {
fn eq(&self, other: &$name) -> bool {
&self[..] == other.as_str()
}
}
};
}
text!(Scheme, Authority, Path);
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{BTreeSet, HashMap};
#[test]
fn schemes_use_constants() {
assert!(matches!(Scheme::from("http").0, Cow::Borrowed("http")));
assert!(matches!(
Scheme::from(String::from("https")).0,
Cow::Borrowed("https")
));
assert!(matches!(Scheme::from("HTTP").0, Cow::Borrowed("http")));
assert!(matches!(
Scheme::from(String::from("HttpS")).0,
Cow::Borrowed("https")
));
assert_eq!(Scheme::from("WS"), "ws");
assert_eq!(Scheme::from(String::from("Coap+TCP")), "coap+tcp");
assert_eq!(Scheme::from(&String::from("WSS")), "wss");
assert_eq!(Scheme::from(Cow::Borrowed("Https")), Scheme::HTTPS);
assert_eq!(Scheme::from(Box::<str>::from("FTP")), "ftp");
assert_ne!(Scheme::from("HTTPS"), "HTTPS");
assert_eq!(String::from(Scheme::HTTPS), "https");
}
#[test]
fn short_text_is_inline_however_supplied() {
let inline = |path: &Path| matches!(path.0, Storage::Inline(_));
assert!(inline(&Path::from("/")));
assert!(inline(&Path::from(String::from("/"))));
assert!(inline(&Path::from("x".repeat(24))));
assert!(!inline(&Path::from("x".repeat(25))));
assert!(inline(&Path::from("é".repeat(12).as_str())));
assert!(!inline(&Path::from("é".repeat(13).as_str())));
assert_eq!(String::from(Path::from("x".repeat(25))), "x".repeat(25));
}
#[test]
fn uppercase_detection_matches_bytes() {
for length in 0..=24 {
for at in 0..length {
for byte in 0..=255u8 {
let mut bytes = vec![b'a'; length];
bytes[at] = byte;
assert_eq!(
has_uppercase(&bytes),
byte.is_ascii_uppercase(),
"{length} {at} {byte}"
);
}
}
}
for neighbour in [0x00, 0x40, 0x5b, 0x7f, 0x80, 0xc0, 0xff] {
for byte in 0..=255u8 {
let bytes = [
neighbour, byte, neighbour, neighbour, neighbour, neighbour, neighbour,
neighbour,
];
assert_eq!(
has_uppercase(&bytes),
bytes.iter().any(u8::is_ascii_uppercase),
"{neighbour} {byte}"
);
}
}
}
#[test]
fn authorities_are_lowercase_and_paths_are_preserved() {
let long = "Long.Example.COM:".to_owned() + &"8".repeat(10);
assert_eq!(Authority::from("Example.COM:80"), "example.com:80");
assert_eq!(
Authority::from(String::from("[::ABCD]:443")),
"[::abcd]:443"
);
assert_eq!(Authority::from(long.as_str()), long.to_ascii_lowercase());
assert_eq!(Authority::from(long.clone()), long.to_ascii_lowercase());
assert_eq!(Authority::from("É.example"), "É.example");
assert_eq!(Authority::from(Cow::Borrowed("A:1")), "a:1");
assert_eq!(Authority::from(Box::<str>::from("B:2")), "b:2");
assert_eq!(Path::from("/A?B=C"), "/A?B=C");
assert_eq!(Path::from("/A".repeat(20)), "/A".repeat(20));
}
#[test]
fn text_behaves_as_str() {
let long = "x".repeat(40);
let owned = String::from("/a");
assert_eq!(Path::from(&owned), Path::from(Cow::Borrowed("/a")));
assert_eq!(
Authority::from(long.clone()),
Authority::from(long.as_str())
);
assert_eq!(Path::from(Box::<str>::from("/a")), "/a");
let path = Path::from("/a");
assert!("/a" == path && owned == path && path == owned);
assert_eq!(format!("{:?}", Path::from("/a")), "Path(\"/a\")");
assert_eq!(format!("{:?}", Scheme::HTTP), "Scheme(\"http\")");
let sorted: BTreeSet<_> = ["/b", "/a"].into_iter().map(Path::from).collect();
assert_eq!(sorted.first().unwrap(), "/a");
let schemes = HashMap::from([(Scheme::HTTP, 80)]);
assert_eq!(schemes.get("http"), Some(&80));
}
}