1use core::cmp::min;
2use core::str::FromStr;
3
4use crate::std::string::String;
5
6use rama_core::error::BoxErrorExt as _;
7use rama_core::error::{BoxError, ErrorContext};
8use rama_core::extensions::Extension;
9use rama_utils::macros::str::eq_ignore_ascii_case;
10use rama_utils::str::smol_str::SmolStr;
11
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Extension)]
13#[extension(tags(net))]
14pub struct Protocol(ProtocolKind);
22
23#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
24#[non_exhaustive]
25enum ProtocolKind {
26 Http,
28 Https,
30 Ws,
35 Wss,
40 Socks5,
44 Socks5h,
51 File,
58 Data,
65 Custom(SmolStr),
67}
68
69impl Protocol {
70 pub const HTTP_SCHEME: &str = "http";
72 pub const HTTP_DEFAULT_PORT: u16 = 80;
74 pub const HTTP_ALT_PORT: u16 = 8080;
76 pub const HTTP_PROXY_DEFAULT_PORT: u16 = 1080;
82 pub const HTTP: Self = Self(ProtocolKind::Http);
84
85 pub const HTTPS_SCHEME: &str = "https";
87 pub const HTTPS_DEFAULT_PORT: u16 = 443;
89 pub const HTTPS_ALT_PORT: u16 = 8443;
91 pub const HTTPS: Self = Self(ProtocolKind::Https);
93
94 pub const WS_SCHEME: &str = "ws";
96 pub const WS_DEFAULT_PORT: u16 = Self::HTTP_DEFAULT_PORT;
98 pub const WS: Self = Self(ProtocolKind::Ws);
100
101 pub const WSS_SCHEME: &str = "wss";
103 pub const WSS_DEFAULT_PORT: u16 = Self::HTTPS_DEFAULT_PORT;
105 pub const WSS: Self = Self(ProtocolKind::Wss);
107
108 pub const SOCKS5_SCHEME: &str = "socks5";
110 pub const SOCKS5_DEFAULT_PORT: u16 = 1080;
112 pub const SOCKS5: Self = Self(ProtocolKind::Socks5);
114
115 pub const SOCKS5H_SCHEME: &str = "socks5h";
117 pub const SOCKS5H_DEFAULT_PORT: u16 = Self::SOCKS5_DEFAULT_PORT;
119 pub const SOCKS5H: Self = Self(ProtocolKind::Socks5h);
121
122 pub const FILE_SCHEME: &str = "file";
124 pub const FILE: Self = Self(ProtocolKind::File);
129
130 pub const DATA_SCHEME: &str = "data";
132 pub const DATA: Self = Self(ProtocolKind::Data);
136
137 #[must_use]
149 #[expect(
150 clippy::panic,
151 reason = "static-str invariant: panic at compile time when the static is not a valid protocol"
152 )]
153 pub const fn from_static(s: &'static str) -> Self {
154 Self(if eq_ignore_ascii_case!(s, Self::HTTPS_SCHEME) {
158 ProtocolKind::Https
159 } else if eq_ignore_ascii_case!(s, Self::HTTP_SCHEME) {
160 ProtocolKind::Http
161 } else if eq_ignore_ascii_case!(s, Self::SOCKS5_SCHEME) {
162 ProtocolKind::Socks5
163 } else if eq_ignore_ascii_case!(s, Self::SOCKS5H_SCHEME) {
164 ProtocolKind::Socks5h
165 } else if eq_ignore_ascii_case!(s, Self::WS_SCHEME) {
166 ProtocolKind::Ws
167 } else if eq_ignore_ascii_case!(s, Self::WSS_SCHEME) {
168 ProtocolKind::Wss
169 } else if eq_ignore_ascii_case!(s, Self::FILE_SCHEME) {
170 ProtocolKind::File
171 } else if eq_ignore_ascii_case!(s, Self::DATA_SCHEME) {
172 ProtocolKind::Data
173 } else if validate_scheme_str(s) {
174 ProtocolKind::Custom(SmolStr::new_static(s))
175 } else {
176 panic!("invalid static protocol str");
177 })
178 }
179
180 #[must_use]
182 pub fn is_http(&self) -> bool {
183 match &self.0 {
184 ProtocolKind::Http | ProtocolKind::Https => true,
185 ProtocolKind::Ws
186 | ProtocolKind::Wss
187 | ProtocolKind::Socks5
188 | ProtocolKind::Socks5h
189 | ProtocolKind::File
190 | ProtocolKind::Data
191 | ProtocolKind::Custom(_) => false,
192 }
193 }
194
195 #[must_use]
197 pub fn is_ws(&self) -> bool {
198 match &self.0 {
199 ProtocolKind::Ws | ProtocolKind::Wss => true,
200 ProtocolKind::Http
201 | ProtocolKind::Https
202 | ProtocolKind::Socks5
203 | ProtocolKind::Socks5h
204 | ProtocolKind::File
205 | ProtocolKind::Data
206 | ProtocolKind::Custom(_) => false,
207 }
208 }
209
210 #[must_use]
212 pub fn is_socks5(&self) -> bool {
213 match &self.0 {
214 ProtocolKind::Socks5 | ProtocolKind::Socks5h => true,
215 ProtocolKind::Http
216 | ProtocolKind::Https
217 | ProtocolKind::Ws
218 | ProtocolKind::Wss
219 | ProtocolKind::File
220 | ProtocolKind::Data
221 | ProtocolKind::Custom(_) => false,
222 }
223 }
224
225 #[must_use]
227 pub fn is_secure(&self) -> bool {
228 match &self.0 {
229 ProtocolKind::Https | ProtocolKind::Wss => true,
230 ProtocolKind::Ws
231 | ProtocolKind::Http
232 | ProtocolKind::Socks5
233 | ProtocolKind::Socks5h
234 | ProtocolKind::File
235 | ProtocolKind::Data
236 | ProtocolKind::Custom(_) => false,
237 }
238 }
239
240 #[must_use]
252 pub fn default_port(&self) -> Option<u16> {
253 match &self.0 {
254 ProtocolKind::Https => Some(Self::HTTPS_DEFAULT_PORT),
255 ProtocolKind::Wss => Some(Self::WSS_DEFAULT_PORT),
256 ProtocolKind::Http => Some(Self::HTTP_DEFAULT_PORT),
257 ProtocolKind::Ws => Some(Self::WS_DEFAULT_PORT),
258 ProtocolKind::Socks5 => Some(Self::SOCKS5_DEFAULT_PORT),
259 ProtocolKind::Socks5h => Some(Self::SOCKS5H_DEFAULT_PORT),
260 ProtocolKind::File | ProtocolKind::Data | ProtocolKind::Custom(_) => None,
262 }
263 }
264
265 #[must_use]
272 pub fn proxy_default_port(&self) -> Option<u16> {
273 match &self.0 {
274 ProtocolKind::Http => Some(Self::HTTP_PROXY_DEFAULT_PORT),
275 ProtocolKind::Https => Some(Self::HTTPS_DEFAULT_PORT),
276 ProtocolKind::Socks5 => Some(Self::SOCKS5_DEFAULT_PORT),
277 ProtocolKind::Socks5h => Some(Self::SOCKS5H_DEFAULT_PORT),
278 ProtocolKind::Ws
279 | ProtocolKind::Wss
280 | ProtocolKind::File
281 | ProtocolKind::Data
282 | ProtocolKind::Custom(_) => None,
283 }
284 }
285
286 #[must_use]
288 pub fn as_str(&self) -> &str {
289 match &self.0 {
290 ProtocolKind::Http => Self::HTTP_SCHEME,
291 ProtocolKind::Https => Self::HTTPS_SCHEME,
292 ProtocolKind::Ws => Self::WS_SCHEME,
293 ProtocolKind::Wss => Self::WSS_SCHEME,
294 ProtocolKind::Socks5 => Self::SOCKS5_SCHEME,
295 ProtocolKind::Socks5h => Self::SOCKS5H_SCHEME,
296 ProtocolKind::File => Self::FILE_SCHEME,
297 ProtocolKind::Data => Self::DATA_SCHEME,
298 ProtocolKind::Custom(s) => s.as_ref(),
299 }
300 }
301
302 #[must_use]
308 pub fn canonicalize(self) -> Self {
309 match self.0 {
310 ProtocolKind::Custom(scheme)
311 if scheme.bytes().any(|byte| byte.is_ascii_uppercase()) =>
312 {
313 Self(ProtocolKind::Custom(SmolStr::new(
314 scheme.to_ascii_lowercase(),
315 )))
316 }
317 _ => self,
318 }
319 }
320}
321
322rama_utils::macros::error::static_str_error! {
323 #[doc = "invalid protocol string"]
324 pub struct InvalidProtocolStr;
325}
326
327fn try_to_convert_str_to_non_custom_protocol(
328 s: &str,
329) -> Result<Option<Protocol>, InvalidProtocolStr> {
330 Ok(Some(Protocol(
331 if eq_ignore_ascii_case!(s, Protocol::HTTPS_SCHEME) {
332 ProtocolKind::Https
333 } else if eq_ignore_ascii_case!(s, Protocol::HTTP_SCHEME) {
334 ProtocolKind::Http
335 } else if eq_ignore_ascii_case!(s, Protocol::SOCKS5_SCHEME) {
336 ProtocolKind::Socks5
337 } else if eq_ignore_ascii_case!(s, Protocol::SOCKS5H_SCHEME) {
338 ProtocolKind::Socks5h
339 } else if eq_ignore_ascii_case!(s, Protocol::WS_SCHEME) {
340 ProtocolKind::Ws
341 } else if eq_ignore_ascii_case!(s, Protocol::WSS_SCHEME) {
342 ProtocolKind::Wss
343 } else if eq_ignore_ascii_case!(s, Protocol::FILE_SCHEME) {
344 ProtocolKind::File
345 } else if eq_ignore_ascii_case!(s, Protocol::DATA_SCHEME) {
346 ProtocolKind::Data
347 } else if validate_scheme_str(s) {
348 return Ok(None);
349 } else {
350 return Err(InvalidProtocolStr);
351 },
352 )))
353}
354
355impl TryFrom<&str> for Protocol {
356 type Error = InvalidProtocolStr;
357
358 fn try_from(s: &str) -> Result<Self, Self::Error> {
359 Ok(try_to_convert_str_to_non_custom_protocol(s)?
364 .unwrap_or_else(|| Self(ProtocolKind::Custom(SmolStr::new(s)))))
365 }
366}
367
368impl TryFrom<String> for Protocol {
369 type Error = InvalidProtocolStr;
370
371 fn try_from(s: String) -> Result<Self, Self::Error> {
372 Ok(try_to_convert_str_to_non_custom_protocol(&s)?
373 .unwrap_or(Self(ProtocolKind::Custom(SmolStr::new(s)))))
374 }
375}
376
377impl TryFrom<&String> for Protocol {
378 type Error = InvalidProtocolStr;
379
380 fn try_from(s: &String) -> Result<Self, Self::Error> {
381 Ok(try_to_convert_str_to_non_custom_protocol(s)?
382 .unwrap_or_else(|| Self(ProtocolKind::Custom(SmolStr::new(s)))))
383 }
384}
385
386impl FromStr for Protocol {
387 type Err = InvalidProtocolStr;
388
389 fn from_str(s: &str) -> Result<Self, Self::Err> {
390 s.try_into()
391 }
392}
393
394impl PartialEq<str> for Protocol {
395 fn eq(&self, other: &str) -> bool {
396 match &self.0 {
397 ProtocolKind::Https => other.eq_ignore_ascii_case(Self::HTTPS_SCHEME),
398 ProtocolKind::Http => other.eq_ignore_ascii_case(Self::HTTP_SCHEME) || other.is_empty(),
399 ProtocolKind::Socks5 => other.eq_ignore_ascii_case(Self::SOCKS5_SCHEME),
400 ProtocolKind::Socks5h => other.eq_ignore_ascii_case(Self::SOCKS5H_SCHEME),
401 ProtocolKind::Ws => other.eq_ignore_ascii_case(Self::WS_SCHEME),
402 ProtocolKind::Wss => other.eq_ignore_ascii_case(Self::WSS_SCHEME),
403 ProtocolKind::File => other.eq_ignore_ascii_case(Self::FILE_SCHEME),
404 ProtocolKind::Data => other.eq_ignore_ascii_case(Self::DATA_SCHEME),
405 ProtocolKind::Custom(s) => other.eq_ignore_ascii_case(s),
406 }
407 }
408}
409
410impl PartialEq<String> for Protocol {
411 fn eq(&self, other: &String) -> bool {
412 self == other.as_str()
413 }
414}
415
416impl PartialEq<&str> for Protocol {
417 fn eq(&self, other: &&str) -> bool {
418 self == *other
419 }
420}
421
422impl PartialEq<Protocol> for str {
423 fn eq(&self, other: &Protocol) -> bool {
424 other == self
425 }
426}
427
428impl PartialEq<Protocol> for String {
429 fn eq(&self, other: &Protocol) -> bool {
430 other == self.as_str()
431 }
432}
433
434impl PartialEq<Protocol> for &str {
435 #[inline(always)]
436 fn eq(&self, other: &Protocol) -> bool {
437 other == *self
438 }
439}
440
441impl core::fmt::Display for Protocol {
442 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
443 self.as_str().fmt(f)
444 }
445}
446
447pub(crate) fn try_to_extract_protocol_from_uri_scheme(
448 s: &[u8],
449) -> Result<(Option<Protocol>, usize), BoxError> {
450 if s.is_empty() {
451 return Err(BoxError::from_static_str("empty uri contains no scheme"));
452 }
453
454 for i in 0..min(s.len(), 512) {
455 let b = s[i];
456
457 if b == b':' {
458 if s.len() < i + 3 {
460 break;
461 }
462
463 if &s[i + 1..i + 3] != b"//" {
465 break;
466 }
467
468 let str =
469 core::str::from_utf8(&s[..i]).context("interpret scheme bytes as utf-8 str")?;
470 let protocol = str
471 .try_into()
472 .context("parse scheme utf-8 str as protocol")?;
473 return Ok((Some(protocol), i + 3));
474 }
475 }
476
477 Ok((None, 0))
478}
479
480#[inline]
481const fn validate_scheme_str(s: &str) -> bool {
482 validate_scheme_slice(s.as_bytes())
483}
484
485const fn validate_scheme_slice(s: &[u8]) -> bool {
486 if s.is_empty() || s.len() > MAX_SCHEME_LEN {
487 return false;
488 }
489
490 let mut i = 0;
491 while i < s.len() {
492 if SCHEME_CHARS[s[i] as usize] == 0 {
493 return false;
494 }
495 i += 1;
496 }
497 true
498}
499
500pub(crate) const MAX_SCHEME_LEN: usize = 64;
503
504#[rustfmt::skip]
513const SCHEME_CHARS: [u8; 256] = [
514 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 ];
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546
547 #[test]
548 fn test_from_str() {
549 assert_eq!("http".parse(), Ok(Protocol::HTTP));
550 assert_eq!("https".parse(), Ok(Protocol::HTTPS));
551 assert_eq!("ws".parse(), Ok(Protocol::WS));
552 assert_eq!("wss".parse(), Ok(Protocol::WSS));
553 assert_eq!("socks5".parse(), Ok(Protocol::SOCKS5));
554 assert_eq!("socks5h".parse(), Ok(Protocol::SOCKS5H));
555 assert_eq!("file".parse(), Ok(Protocol::FILE));
556 assert_eq!("data".parse(), Ok(Protocol::DATA));
557 assert_eq!("custom".parse(), Ok(Protocol::from_static("custom")));
558 }
559
560 #[test]
561 fn canonicalize_lowercases_custom_schemes() {
562 assert_eq!(
563 Protocol::from_static("CuStOm").canonicalize().as_str(),
564 "custom"
565 );
566 assert_eq!(Protocol::HTTPS.canonicalize(), Protocol::HTTPS);
567 }
568
569 #[test]
570 fn test_non_network_schemes() {
571 for (protocol, scheme) in [
572 (Protocol::FILE, Protocol::FILE_SCHEME),
573 (Protocol::DATA, Protocol::DATA_SCHEME),
574 ] {
575 assert_eq!(Protocol::from_static(scheme), protocol);
577 assert_eq!(scheme.parse(), Ok(protocol.clone()));
578 assert_eq!(scheme.to_uppercase().parse(), Ok(protocol.clone()));
579 assert_eq!(protocol.as_str(), scheme);
580 assert_eq!(protocol.default_port(), None);
581 assert!(!protocol.is_http());
582 assert!(!protocol.is_ws());
583 assert!(!protocol.is_socks5());
584 assert!(!protocol.is_secure());
585 }
586 }
587
588 #[test]
589 fn proxy_default_ports_are_transport_specific() {
590 for (protocol, expected) in [
591 (Protocol::HTTP, Some(Protocol::HTTP_PROXY_DEFAULT_PORT)),
592 (Protocol::HTTPS, Some(Protocol::HTTPS_DEFAULT_PORT)),
593 (Protocol::SOCKS5, Some(Protocol::SOCKS5_DEFAULT_PORT)),
594 (Protocol::SOCKS5H, Some(Protocol::SOCKS5H_DEFAULT_PORT)),
595 (Protocol::WS, None),
596 (Protocol::WSS, None),
597 (Protocol::FILE, None),
598 (Protocol::DATA, None),
599 (Protocol::from_static("custom"), None),
600 ] {
601 assert_eq!(protocol.proxy_default_port(), expected, "{protocol}");
602 }
603 }
604
605 #[test]
606 fn empty_scheme_rejected() {
607 "".parse::<Protocol>().unwrap_err();
611 Protocol::try_from("").unwrap_err();
612 }
613
614 #[test]
615 fn try_from_rejects_non_ascii_scheme() {
616 Protocol::try_from("müncheme").unwrap_err();
621 Protocol::try_from("ab cd").unwrap_err();
622 Protocol::try_from("ab\0").unwrap_err();
623 Protocol::try_from("git+ssh").unwrap();
625 Protocol::try_from("coap+tcp").unwrap();
626 }
627
628 #[test]
629 fn regression_custom_scheme_over_smolstr_inline_cap_does_not_panic() {
630 let long = "hhhhhhahhhhhhhhhhhhhhhhhh"; assert_eq!(long.len(), 25);
637 let proto: Protocol = long.try_into().unwrap();
638 assert_eq!(proto.as_str(), long);
639
640 let uri: crate::uri::Uri = format!("{long}:/aq").parse().unwrap();
642 assert_eq!(uri.scheme().unwrap().as_str(), long);
643 }
644
645 #[test]
646 fn test_scheme_is_secure() {
647 assert!(!Protocol::HTTP.is_secure());
648 assert!(Protocol::HTTPS.is_secure());
649 assert!(!Protocol::SOCKS5.is_secure());
650 assert!(!Protocol::SOCKS5H.is_secure());
651 assert!(!Protocol::WS.is_secure());
652 assert!(Protocol::WSS.is_secure());
653 assert!(!Protocol::FILE.is_secure());
654 assert!(!Protocol::DATA.is_secure());
655 assert!(!Protocol::from_static("custom").is_secure());
656 }
657
658 #[test]
659 fn test_try_to_extract_protocol_from_uri_scheme() {
660 for (s, expected) in [
661 ("", None),
662 ("http://example.com", Some((Some(Protocol::HTTP), 7))),
663 ("https://example.com", Some((Some(Protocol::HTTPS), 8))),
664 ("ws://example.com", Some((Some(Protocol::WS), 5))),
665 ("wss://example.com", Some((Some(Protocol::WSS), 6))),
666 ("socks5://example.com", Some((Some(Protocol::SOCKS5), 9))),
667 ("socks5h://example.com", Some((Some(Protocol::SOCKS5H), 10))),
668 (
669 "custom://example.com",
670 Some((Some(Protocol::from_static("custom")), 9)),
671 ),
672 (" http://example.com", None),
673 ("example.com", Some((None, 0))),
674 ("127.0.0.1", Some((None, 0))),
675 ("127.0.0.1:8080", Some((None, 0))),
676 (
677 "longlonglongwaytoolongforsomethingusefulorvaliddontyouthinkmydearreader://example.com",
678 None,
679 ),
680 ] {
681 let result = try_to_extract_protocol_from_uri_scheme(s.as_bytes());
682 match expected {
683 Some(t) => match result {
684 Err(err) => panic!("unexpected err: {err} (case: {s}"),
685 Ok(p) => assert_eq!(t, p, "case: {s}"),
686 },
687 None => assert!(result.is_err(), "case: {s}, result: {result:?}"),
688 }
689 }
690 }
691}