use core::cmp::min;
use core::str::FromStr;
use crate::std::string::String;
use rama_core::error::BoxErrorExt as _;
use rama_core::error::{BoxError, ErrorContext};
use rama_core::extensions::Extension;
use rama_utils::macros::str::eq_ignore_ascii_case;
use rama_utils::str::smol_str::SmolStr;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Extension)]
#[extension(tags(net))]
pub struct Protocol(ProtocolKind);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
enum ProtocolKind {
Http,
Https,
Ws,
Wss,
Socks5,
Socks5h,
File,
Custom(SmolStr),
}
impl Protocol {
pub const HTTP_SCHEME: &str = "http";
pub const HTTP_DEFAULT_PORT: u16 = 80;
pub const HTTP: Self = Self(ProtocolKind::Http);
pub const HTTPS_SCHEME: &str = "https";
pub const HTTPS_DEFAULT_PORT: u16 = 443;
pub const HTTPS: Self = Self(ProtocolKind::Https);
pub const WS_SCHEME: &str = "ws";
pub const WS_DEFAULT_PORT: u16 = Self::HTTP_DEFAULT_PORT;
pub const WS: Self = Self(ProtocolKind::Ws);
pub const WSS_SCHEME: &str = "wss";
pub const WSS_DEFAULT_PORT: u16 = Self::HTTPS_DEFAULT_PORT;
pub const WSS: Self = Self(ProtocolKind::Wss);
pub const SOCKS5_SCHEME: &str = "socks5";
pub const SOCKS5_DEFAULT_PORT: u16 = 1080;
pub const SOCKS5: Self = Self(ProtocolKind::Socks5);
pub const SOCKS5H_SCHEME: &str = "socks5h";
pub const SOCKS5H_DEFAULT_PORT: u16 = Self::SOCKS5_DEFAULT_PORT;
pub const SOCKS5H: Self = Self(ProtocolKind::Socks5h);
pub const FILE_SCHEME: &str = "file";
pub const FILE: Self = Self(ProtocolKind::File);
#[must_use]
#[expect(
clippy::panic,
reason = "static-str invariant: panic at compile time when the static is not a valid protocol"
)]
pub const fn from_static(s: &'static str) -> Self {
Self(if eq_ignore_ascii_case!(s, Self::HTTPS_SCHEME) {
ProtocolKind::Https
} else if eq_ignore_ascii_case!(s, Self::HTTP_SCHEME) {
ProtocolKind::Http
} else if eq_ignore_ascii_case!(s, Self::SOCKS5_SCHEME) {
ProtocolKind::Socks5
} else if eq_ignore_ascii_case!(s, Self::SOCKS5H_SCHEME) {
ProtocolKind::Socks5h
} else if eq_ignore_ascii_case!(s, Self::WS_SCHEME) {
ProtocolKind::Ws
} else if eq_ignore_ascii_case!(s, Self::WSS_SCHEME) {
ProtocolKind::Wss
} else if eq_ignore_ascii_case!(s, Self::FILE_SCHEME) {
ProtocolKind::File
} else if validate_scheme_str(s) {
ProtocolKind::Custom(SmolStr::new_static(s))
} else {
panic!("invalid static protocol str");
})
}
#[must_use]
pub fn is_http(&self) -> bool {
match &self.0 {
ProtocolKind::Http | ProtocolKind::Https => true,
ProtocolKind::Ws
| ProtocolKind::Wss
| ProtocolKind::Socks5
| ProtocolKind::Socks5h
| ProtocolKind::File
| ProtocolKind::Custom(_) => false,
}
}
#[must_use]
pub fn is_ws(&self) -> bool {
match &self.0 {
ProtocolKind::Ws | ProtocolKind::Wss => true,
ProtocolKind::Http
| ProtocolKind::Https
| ProtocolKind::Socks5
| ProtocolKind::Socks5h
| ProtocolKind::File
| ProtocolKind::Custom(_) => false,
}
}
#[must_use]
pub fn is_socks5(&self) -> bool {
match &self.0 {
ProtocolKind::Socks5 | ProtocolKind::Socks5h => true,
ProtocolKind::Http
| ProtocolKind::Https
| ProtocolKind::Ws
| ProtocolKind::Wss
| ProtocolKind::File
| ProtocolKind::Custom(_) => false,
}
}
#[must_use]
pub fn is_secure(&self) -> bool {
match &self.0 {
ProtocolKind::Https | ProtocolKind::Wss => true,
ProtocolKind::Ws
| ProtocolKind::Http
| ProtocolKind::Socks5
| ProtocolKind::Socks5h
| ProtocolKind::File
| ProtocolKind::Custom(_) => false,
}
}
#[must_use]
pub fn default_port(&self) -> Option<u16> {
match &self.0 {
ProtocolKind::Https => Some(Self::HTTPS_DEFAULT_PORT),
ProtocolKind::Wss => Some(Self::WSS_DEFAULT_PORT),
ProtocolKind::Http => Some(Self::HTTP_DEFAULT_PORT),
ProtocolKind::Ws => Some(Self::WS_DEFAULT_PORT),
ProtocolKind::Socks5 => Some(Self::SOCKS5_DEFAULT_PORT),
ProtocolKind::Socks5h => Some(Self::SOCKS5H_DEFAULT_PORT),
ProtocolKind::File | ProtocolKind::Custom(_) => None,
}
}
#[must_use]
pub fn as_str(&self) -> &str {
match &self.0 {
ProtocolKind::Http => Self::HTTP_SCHEME,
ProtocolKind::Https => Self::HTTPS_SCHEME,
ProtocolKind::Ws => Self::WS_SCHEME,
ProtocolKind::Wss => Self::WSS_SCHEME,
ProtocolKind::Socks5 => Self::SOCKS5_SCHEME,
ProtocolKind::Socks5h => Self::SOCKS5H_SCHEME,
ProtocolKind::File => Self::FILE_SCHEME,
ProtocolKind::Custom(s) => s.as_ref(),
}
}
}
rama_utils::macros::error::static_str_error! {
#[doc = "invalid protocol string"]
pub struct InvalidProtocolStr;
}
fn try_to_convert_str_to_non_custom_protocol(
s: &str,
) -> Result<Option<Protocol>, InvalidProtocolStr> {
Ok(Some(Protocol(
if eq_ignore_ascii_case!(s, Protocol::HTTPS_SCHEME) {
ProtocolKind::Https
} else if eq_ignore_ascii_case!(s, Protocol::HTTP_SCHEME) {
ProtocolKind::Http
} else if eq_ignore_ascii_case!(s, Protocol::SOCKS5_SCHEME) {
ProtocolKind::Socks5
} else if eq_ignore_ascii_case!(s, Protocol::SOCKS5H_SCHEME) {
ProtocolKind::Socks5h
} else if eq_ignore_ascii_case!(s, Protocol::WS_SCHEME) {
ProtocolKind::Ws
} else if eq_ignore_ascii_case!(s, Protocol::WSS_SCHEME) {
ProtocolKind::Wss
} else if eq_ignore_ascii_case!(s, Protocol::FILE_SCHEME) {
ProtocolKind::File
} else if validate_scheme_str(s) {
return Ok(None);
} else {
return Err(InvalidProtocolStr);
},
)))
}
impl TryFrom<&str> for Protocol {
type Error = InvalidProtocolStr;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Ok(try_to_convert_str_to_non_custom_protocol(s)?
.unwrap_or_else(|| Self(ProtocolKind::Custom(SmolStr::new(s)))))
}
}
impl TryFrom<String> for Protocol {
type Error = InvalidProtocolStr;
fn try_from(s: String) -> Result<Self, Self::Error> {
Ok(try_to_convert_str_to_non_custom_protocol(&s)?
.unwrap_or(Self(ProtocolKind::Custom(SmolStr::new(s)))))
}
}
impl TryFrom<&String> for Protocol {
type Error = InvalidProtocolStr;
fn try_from(s: &String) -> Result<Self, Self::Error> {
Ok(try_to_convert_str_to_non_custom_protocol(s)?
.unwrap_or_else(|| Self(ProtocolKind::Custom(SmolStr::new(s)))))
}
}
impl FromStr for Protocol {
type Err = InvalidProtocolStr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.try_into()
}
}
impl PartialEq<str> for Protocol {
fn eq(&self, other: &str) -> bool {
match &self.0 {
ProtocolKind::Https => other.eq_ignore_ascii_case(Self::HTTPS_SCHEME),
ProtocolKind::Http => other.eq_ignore_ascii_case(Self::HTTP_SCHEME) || other.is_empty(),
ProtocolKind::Socks5 => other.eq_ignore_ascii_case(Self::SOCKS5_SCHEME),
ProtocolKind::Socks5h => other.eq_ignore_ascii_case(Self::SOCKS5H_SCHEME),
ProtocolKind::Ws => other.eq_ignore_ascii_case(Self::WS_SCHEME),
ProtocolKind::Wss => other.eq_ignore_ascii_case(Self::WSS_SCHEME),
ProtocolKind::File => other.eq_ignore_ascii_case(Self::FILE_SCHEME),
ProtocolKind::Custom(s) => other.eq_ignore_ascii_case(s),
}
}
}
impl PartialEq<String> for Protocol {
fn eq(&self, other: &String) -> bool {
self == other.as_str()
}
}
impl PartialEq<&str> for Protocol {
fn eq(&self, other: &&str) -> bool {
self == *other
}
}
impl PartialEq<Protocol> for str {
fn eq(&self, other: &Protocol) -> bool {
other == self
}
}
impl PartialEq<Protocol> for String {
fn eq(&self, other: &Protocol) -> bool {
other == self.as_str()
}
}
impl PartialEq<Protocol> for &str {
#[inline(always)]
fn eq(&self, other: &Protocol) -> bool {
other == *self
}
}
impl core::fmt::Display for Protocol {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
self.as_str().fmt(f)
}
}
pub(crate) fn try_to_extract_protocol_from_uri_scheme(
s: &[u8],
) -> Result<(Option<Protocol>, usize), BoxError> {
if s.is_empty() {
return Err(BoxError::from_static_str("empty uri contains no scheme"));
}
for i in 0..min(s.len(), 512) {
let b = s[i];
if b == b':' {
if s.len() < i + 3 {
break;
}
if &s[i + 1..i + 3] != b"//" {
break;
}
let str =
core::str::from_utf8(&s[..i]).context("interpret scheme bytes as utf-8 str")?;
let protocol = str
.try_into()
.context("parse scheme utf-8 str as protocol")?;
return Ok((Some(protocol), i + 3));
}
}
Ok((None, 0))
}
#[inline]
const fn validate_scheme_str(s: &str) -> bool {
validate_scheme_slice(s.as_bytes())
}
const fn validate_scheme_slice(s: &[u8]) -> bool {
if s.is_empty() || s.len() > MAX_SCHEME_LEN {
return false;
}
let mut i = 0;
while i < s.len() {
if SCHEME_CHARS[s[i] as usize] == 0 {
return false;
}
i += 1;
}
true
}
const MAX_SCHEME_LEN: usize = 64;
#[rustfmt::skip]
const SCHEME_CHARS: [u8; 256] = [
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, b'+', 0, b'-', b'.', 0, b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', 0, 0, 0, 0, 0, 0, 0, b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', b'I', b'J', b'K', b'L', b'M', b'N', b'O', b'P', b'Q', b'R', b'S', b'T', b'U', b'V', b'W', b'X', b'Y', b'Z', 0, 0, 0, 0, 0, 0, b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h', b'i', b'j', b'k', b'l', b'm', b'n', b'o', b'p', b'q', b'r', b's', b't', b'u', b'v', b'w', b'x', b'y', b'z', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_str() {
assert_eq!("http".parse(), Ok(Protocol::HTTP));
assert_eq!("https".parse(), Ok(Protocol::HTTPS));
assert_eq!("ws".parse(), Ok(Protocol::WS));
assert_eq!("wss".parse(), Ok(Protocol::WSS));
assert_eq!("socks5".parse(), Ok(Protocol::SOCKS5));
assert_eq!("socks5h".parse(), Ok(Protocol::SOCKS5H));
assert_eq!("custom".parse(), Ok(Protocol::from_static("custom")));
}
#[test]
fn empty_scheme_rejected() {
"".parse::<Protocol>().unwrap_err();
Protocol::try_from("").unwrap_err();
}
#[test]
fn try_from_rejects_non_ascii_scheme() {
Protocol::try_from("müncheme").unwrap_err();
Protocol::try_from("ab cd").unwrap_err();
Protocol::try_from("ab\0").unwrap_err();
Protocol::try_from("git+ssh").unwrap();
Protocol::try_from("coap+tcp").unwrap();
}
#[test]
fn regression_custom_scheme_over_smolstr_inline_cap_does_not_panic() {
let long = "hhhhhhahhhhhhhhhhhhhhhhhh"; assert_eq!(long.len(), 25);
let proto: Protocol = long.try_into().unwrap();
assert_eq!(proto.as_str(), long);
let uri: crate::uri::Uri = format!("{long}:/aq").parse().unwrap();
assert_eq!(uri.scheme().unwrap().as_str(), long);
}
#[test]
fn test_scheme_is_secure() {
assert!(!Protocol::HTTP.is_secure());
assert!(Protocol::HTTPS.is_secure());
assert!(!Protocol::SOCKS5.is_secure());
assert!(!Protocol::SOCKS5H.is_secure());
assert!(!Protocol::WS.is_secure());
assert!(Protocol::WSS.is_secure());
assert!(!Protocol::from_static("custom").is_secure());
}
#[test]
fn test_try_to_extract_protocol_from_uri_scheme() {
for (s, expected) in [
("", None),
("http://example.com", Some((Some(Protocol::HTTP), 7))),
("https://example.com", Some((Some(Protocol::HTTPS), 8))),
("ws://example.com", Some((Some(Protocol::WS), 5))),
("wss://example.com", Some((Some(Protocol::WSS), 6))),
("socks5://example.com", Some((Some(Protocol::SOCKS5), 9))),
("socks5h://example.com", Some((Some(Protocol::SOCKS5H), 10))),
(
"custom://example.com",
Some((Some(Protocol::from_static("custom")), 9)),
),
(" http://example.com", None),
("example.com", Some((None, 0))),
("127.0.0.1", Some((None, 0))),
("127.0.0.1:8080", Some((None, 0))),
(
"longlonglongwaytoolongforsomethingusefulorvaliddontyouthinkmydearreader://example.com",
None,
),
] {
let result = try_to_extract_protocol_from_uri_scheme(s.as_bytes());
match expected {
Some(t) => match result {
Err(err) => panic!("unexpected err: {err} (case: {s}"),
Ok(p) => assert_eq!(t, p, "case: {s}"),
},
None => assert!(result.is_err(), "case: {s}, result: {result:?}"),
}
}
}
}