1use std::{
2 borrow::{Borrow, Cow},
3 fmt::{self, Display},
4 hash::{Hash, Hasher},
5 ops::Deref,
6 str::FromStr,
7};
8
9use bytes::{Bytes, BytesMut};
10use serde::{Deserialize, Serialize};
11use snafu::{OptionExt, ResultExt, Snafu};
12
13#[derive(Clone, Debug)]
20struct BytesStr(Bytes);
21
22impl Deref for BytesStr {
23 type Target = str;
24
25 #[inline]
26 fn deref(&self) -> &str {
27 unsafe { std::str::from_utf8_unchecked(&self.0) }
29 }
30}
31
32impl PartialEq for BytesStr {
33 #[inline]
34 fn eq(&self, other: &Self) -> bool {
35 self.deref() == other.deref()
36 }
37}
38
39impl Eq for BytesStr {}
40
41impl Hash for BytesStr {
42 #[inline]
43 fn hash<H: Hasher>(&self, state: &mut H) {
44 <str as Hash>::hash(Borrow::<str>::borrow(self), state)
45 }
46}
47
48impl Borrow<str> for BytesStr {
49 #[inline]
50 fn borrow(&self) -> &str {
51 self.deref()
52 }
53}
54
55impl AsRef<str> for BytesStr {
56 #[inline]
57 fn as_ref(&self) -> &str {
58 self.deref()
59 }
60}
61
62impl BytesStr {
63 #[inline]
64 fn modify(&mut self, modify: impl FnOnce(&mut String)) {
65 let mut string = self.as_ref().to_owned();
66 modify(&mut string);
67 self.0 = Bytes::from(string.into_bytes());
68 }
69}
70
71#[derive(Clone, Debug)]
72enum CowBytes<'a> {
73 Borrowed(&'a [u8]),
74 Owned(Bytes),
75}
76
77impl AsRef<[u8]> for CowBytes<'_> {
78 #[inline]
79 fn as_ref(&self) -> &[u8] {
80 match self {
81 Self::Borrowed(bytes) => bytes,
82 Self::Owned(bytes) => bytes,
83 }
84 }
85}
86
87#[derive(Clone, Debug)]
88enum CowBytesStr<'a> {
89 Borrowed(&'a str),
90 Owned(BytesStr),
91}
92
93impl CowBytesStr<'_> {
94 #[inline]
95 fn modify(&mut self, modify: impl FnOnce(&mut String)) {
96 match self {
97 Self::Borrowed(value) => {
98 let mut owned = BytesStr(Bytes::from(value.to_owned()));
99 owned.modify(modify);
100 *self = Self::Owned(owned);
101 }
102 Self::Owned(value) => value.modify(modify),
103 }
104 }
105
106 #[inline]
107 fn into_owned(self) -> CowBytesStr<'static> {
108 match self {
109 Self::Borrowed(value) => CowBytesStr::Owned(BytesStr(Bytes::from(value.to_owned()))),
110 Self::Owned(value) => CowBytesStr::Owned(value),
111 }
112 }
113
114 #[inline]
115 fn into_bytes(self) -> Bytes {
116 match self {
117 Self::Borrowed(value) => Bytes::from(value.to_owned()),
118 Self::Owned(value) => value.0,
119 }
120 }
121}
122
123impl AsRef<str> for CowBytesStr<'_> {
124 #[inline]
125 fn as_ref(&self) -> &str {
126 match self {
127 Self::Borrowed(value) => value,
128 Self::Owned(value) => value.as_ref(),
129 }
130 }
131}
132
133#[derive(Clone, Debug)]
134struct DnsName<S>(S);
135
136impl<S: AsRef<str>> AsRef<str> for DnsName<S> {
137 #[inline]
138 fn as_ref(&self) -> &str {
139 self.0.as_ref()
140 }
141}
142
143impl<S: AsRef<[u8]>> DnsName<S> {
144 const MAX_LABEL_LENGTH: usize = 63;
145 const MAX_LENGTH: usize = 253;
146
147 fn validate(input: S) -> Result<S, InvalidName> {
157 enum State {
158 Start,
159 Next,
160 NumericOnly { len: usize },
161 Subsequent { len: usize },
162 Hyphen { len: usize },
163 Wildcard,
164 }
165
166 use State::*;
167
168 let bytes = input.as_ref();
169
170 if bytes.len() > Self::MAX_LENGTH {
171 return Err(InvalidName::TooLong {});
172 }
173
174 let mut state = Start;
175 let mut idx = 0;
176 while idx < bytes.len() {
177 let ch = bytes[idx];
178 state = match (state, ch) {
179 (Start, b'*') => Wildcard,
180 (Wildcard, b'.') => Next,
181 (Start | Next | Hyphen { .. }, b'.') => {
182 return Err(InvalidName::EmptyLabel {});
183 }
184 (Subsequent { .. }, b'.') => Next,
185 (NumericOnly { .. }, b'.') => return Err(InvalidName::EmptyLabel {}),
186 (Subsequent { len } | NumericOnly { len } | Hyphen { len }, _)
187 if len >= Self::MAX_LABEL_LENGTH =>
188 {
189 return Err(InvalidName::LabelTooLong {});
190 }
191 (Start | Next, b'0'..=b'9') => NumericOnly { len: 1 },
192 (NumericOnly { len }, b'0'..=b'9') => NumericOnly { len: len + 1 },
193 (Start | Next, b'a'..=b'z' | b'A'..=b'Z' | b'_') => Subsequent { len: 1 },
194 (Subsequent { len } | NumericOnly { len } | Hyphen { len }, b'-') => {
195 Hyphen { len: len + 1 }
196 }
197 (
198 Subsequent { len } | NumericOnly { len } | Hyphen { len },
199 b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'0'..=b'9',
200 ) => Subsequent { len: len + 1 },
201 _ => return Err(InvalidName::InvalidCharacter {}),
202 };
203 idx += 1;
204 }
205
206 if matches!(state, Start | Hyphen { .. } | NumericOnly { .. }) {
207 return Err(InvalidName::EmptyLabel {});
208 }
209
210 Ok(input)
211 }
212}
213
214impl<'a> TryFrom<CowBytes<'a>> for DnsName<CowBytesStr<'a>> {
215 type Error = InvalidName;
216
217 #[inline]
218 fn try_from(value: CowBytes<'a>) -> Result<Self, Self::Error> {
219 let value = DnsName::<CowBytes>::validate(value)?;
220 Ok(DnsName(match value {
221 CowBytes::Borrowed(bytes) => {
222 CowBytesStr::Borrowed(unsafe { std::str::from_utf8_unchecked(bytes) })
225 }
226 CowBytes::Owned(bytes) => CowBytesStr::Owned(BytesStr(bytes)),
227 }))
228 }
229}
230
231impl<'a> DnsName<CowBytesStr<'a>> {
232 #[inline]
233 fn try_from_static(value: &'static [u8]) -> Result<Self, InvalidName> {
234 DnsName::try_from(CowBytes::Owned(Bytes::from_static(value)))
235 }
236}
237
238#[derive(Debug, Snafu)]
243pub enum InvalidName {
244 #[snafu(display("name too long (max {} characters)", Name::MAX_LENGTH))]
245 TooLong {},
246 #[snafu(display("label too long (max {} characters)", Name::MAX_LABEL_LENGTH))]
247 LabelTooLong {},
248 #[snafu(display("name contains empty or numeric / hyphen only label"))]
249 EmptyLabel {},
250 #[snafu(display("name contains invalid characters"))]
251 InvalidCharacter {},
252 #[snafu(display("name is missing required suffix {suffix}"))]
253 MissingSuffix { suffix: String },
254}
255
256#[derive(Clone, Debug)]
266pub struct Name<'a>(DnsName<CowBytesStr<'a>>);
267
268impl Name<'_> {
269 pub const MAX_LABEL_LENGTH: usize = DnsName::<CowBytes<'static>>::MAX_LABEL_LENGTH;
270 pub const MAX_LENGTH: usize = DnsName::<CowBytes<'static>>::MAX_LENGTH;
271
272 #[inline]
274 pub fn as_str(&self) -> &str {
275 self.0.as_ref()
276 }
277
278 #[inline]
280 pub fn as_full(&self) -> &str {
281 self.as_str()
282 }
283
284 #[inline]
286 pub fn to_owned(&self) -> Name<'static> {
287 Name(DnsName(self.0.0.clone().into_owned()))
288 }
289
290 #[inline]
292 pub fn into_owned(self) -> Name<'static> {
293 Name(DnsName(self.0.0.into_owned()))
294 }
295
296 #[inline]
301 pub fn into_bytes(self) -> Bytes {
302 self.0.0.into_bytes()
303 }
304
305 #[inline]
310 pub fn to_wildcard(self) -> Name<'static> {
311 if self.is_wildcard() {
312 return self.into_owned();
313 }
314 if let Some((_head, tail)) = self.as_str().split_once('.') {
315 let wild = format!("*.{tail}");
316 return wild.parse().expect("wildcard of valid name must be valid");
317 }
318 self.into_owned()
320 }
321
322 #[inline]
324 pub fn is_wildcard(&self) -> bool {
325 self.as_str().starts_with('*')
326 }
327
328 #[inline]
334 pub fn matches(&self, name: &Name) -> bool {
335 if !self.is_wildcard() {
336 return self == name;
337 }
338
339 let self_tails = &self.as_str()[2..]; name.as_str()
341 .split_once('.')
342 .is_some_and(|(.., tails)| tails == self_tails)
343 }
344
345 #[inline]
346 pub fn try_from_static(bytes: &'static [u8]) -> Result<Name<'static>, InvalidName> {
347 Ok(Name::from(
348 DnsName::<CowBytesStr<'static>>::try_from_static(bytes)?,
349 ))
350 }
351}
352
353impl Deref for Name<'_> {
356 type Target = str;
357
358 #[inline]
359 fn deref(&self) -> &str {
360 self.as_str()
361 }
362}
363
364impl Hash for Name<'_> {
365 #[inline]
366 fn hash<H: Hasher>(&self, state: &mut H) {
367 <str as Hash>::hash(Borrow::<str>::borrow(self), state)
368 }
369}
370
371impl Borrow<str> for Name<'_> {
372 #[inline]
373 fn borrow(&self) -> &str {
374 self.as_str()
375 }
376}
377
378impl PartialEq for Name<'_> {
379 #[inline]
380 fn eq(&self, other: &Self) -> bool {
381 self.as_str() == other.as_str()
382 }
383}
384
385impl Eq for Name<'_> {}
386
387impl Display for Name<'_> {
388 #[inline]
389 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
390 f.write_str(self.as_str())
391 }
392}
393
394impl Serialize for Name<'_> {
395 #[inline]
396 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
397 where
398 S: serde::Serializer,
399 {
400 serializer.serialize_str(self.as_str())
401 }
402}
403
404impl<'de> Deserialize<'de> for Name<'static> {
405 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
406 where
407 D: serde::Deserializer<'de>,
408 {
409 let s: String = String::deserialize(deserializer)?;
410 Name::try_from(s).map_err(serde::de::Error::custom)
411 }
412}
413
414impl<'a> From<DnsName<CowBytesStr<'a>>> for Name<'a> {
415 #[inline]
416 fn from(mut value: DnsName<CowBytesStr<'a>>) -> Self {
417 if value.as_ref().bytes().any(|byte| byte.is_ascii_uppercase()) {
418 value.0.modify(|string| string.make_ascii_lowercase());
419 }
420 Name(value)
421 }
422}
423
424impl<'a> TryFrom<&'a str> for Name<'a> {
428 type Error = InvalidName;
429
430 #[inline]
431 fn try_from(s: &'a str) -> Result<Self, Self::Error> {
432 Name::try_from(s.as_bytes())
433 }
434}
435
436impl<'a> TryFrom<&'a [u8]> for Name<'a> {
438 type Error = InvalidName;
439
440 #[inline]
441 fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> {
442 DnsName::try_from(CowBytes::Borrowed(bytes)).map(Name::from)
443 }
444}
445
446impl<'a, const N: usize> TryFrom<&'a [u8; N]> for Name<'a> {
447 type Error = InvalidName;
448
449 #[inline]
450 fn try_from(bytes: &'a [u8; N]) -> Result<Self, Self::Error> {
451 Name::try_from(&bytes[..])
452 }
453}
454
455impl FromStr for Name<'static> {
459 type Err = InvalidName;
460
461 #[inline]
462 fn from_str(s: &str) -> Result<Self, Self::Err> {
463 Name::try_from(s).map(Name::into_owned)
464 }
465}
466
467impl TryFrom<String> for Name<'_> {
468 type Error = InvalidName;
469
470 #[inline]
471 fn try_from(s: String) -> Result<Self, Self::Error> {
472 Name::try_from(s.into_bytes())
473 }
474}
475
476impl TryFrom<Vec<u8>> for Name<'_> {
477 type Error = InvalidName;
478
479 #[inline]
480 fn try_from(v: Vec<u8>) -> Result<Self, Self::Error> {
481 Name::try_from(Bytes::from(v))
482 }
483}
484
485impl TryFrom<Bytes> for Name<'_> {
486 type Error = InvalidName;
487
488 #[inline]
489 fn try_from(bytes: Bytes) -> Result<Self, Self::Error> {
490 DnsName::try_from(CowBytes::Owned(bytes)).map(Name::from)
491 }
492}
493
494impl<'a> TryFrom<Cow<'a, str>> for Name<'a> {
497 type Error = InvalidName;
498
499 #[inline]
500 fn try_from(cow: Cow<'a, str>) -> Result<Self, Self::Error> {
501 match cow {
502 Cow::Borrowed(s) => Name::try_from(s),
503 Cow::Owned(s) => Name::try_from(s),
504 }
505 }
506}
507
508impl<'a> TryFrom<Cow<'a, [u8]>> for Name<'a> {
509 type Error = InvalidName;
510
511 #[inline]
512 fn try_from(cow: Cow<'a, [u8]>) -> Result<Self, Self::Error> {
513 match cow {
514 Cow::Borrowed(bytes) => Name::try_from(bytes),
515 Cow::Owned(bytes) => Name::try_from(bytes),
516 }
517 }
518}
519
520#[derive(Debug, Snafu)]
525pub enum InvalidDhttpName {
526 #[snafu(transparent)]
527 InvalidName { source: InvalidName },
528}
529
530#[derive(Debug, Snafu)]
531#[snafu(module)]
532pub enum ExpandAuthorityError {
533 #[snafu(transparent)]
534 InvalidName { source: InvalidDhttpName },
535 #[snafu(display("cannot expand bare dhttp shorthand without a base name"))]
536 MissingBaseName,
537 #[snafu(display("failed to parse expanded authority `{authority}`"))]
538 ParseAuthority {
539 authority: String,
540 source: http::uri::InvalidUri,
541 },
542}
543
544#[derive(Debug, Snafu)]
545#[snafu(module)]
546pub enum ExpandUriError {
547 #[snafu(display("failed to expand dhttp shorthand in uri authority"))]
548 Authority { source: ExpandAuthorityError },
549 #[snafu(display("failed to reconstruct uri with expanded dhttp name"))]
550 ReconstructUri { source: http::uri::InvalidUriParts },
551}
552
553#[derive(Clone, Debug)]
562pub struct DhttpName<'a>(Name<'a>);
563
564impl DhttpName<'_> {
565 pub const SUFFIX: &'static str = ".dhttp.net";
566
567 #[inline]
569 pub fn validate(input: &[u8]) -> Result<(), InvalidDhttpName> {
570 if !input.ends_with(Self::SUFFIX.as_bytes()) {
571 return Err(InvalidName::MissingSuffix {
572 suffix: Self::SUFFIX.to_string(),
573 }
574 .into());
575 }
576 match DnsName::<&[u8]>::validate(input) {
577 Ok(_) => Ok(()),
578 Err(source) => Err(source.into()),
579 }
580 }
581
582 #[inline]
583 pub fn try_from_static(input: &'static [u8]) -> Result<DhttpName<'static>, InvalidDhttpName> {
584 DhttpName::try_from(Bytes::from_static(input))
585 }
586
587 #[inline]
589 pub fn into_name(self) -> Name<'static> {
590 self.0.into_owned()
591 }
592
593 #[inline]
600 pub fn as_partial(&self) -> &str {
601 debug_assert!(self.0.as_str().ends_with(Self::SUFFIX));
602 &self.0.as_str()[..self.0.as_str().len() - Self::SUFFIX.len()]
603 }
604
605 #[inline]
607 pub fn as_full(&self) -> &str {
608 self.0.as_str()
609 }
610
611 #[inline]
613 pub fn as_name(&self) -> &Name<'_> {
614 &self.0
615 }
616
617 #[inline]
619 pub fn borrow(&self) -> DhttpName<'_> {
620 DhttpName(Name(DnsName(CowBytesStr::Borrowed(self.0.as_str()))))
621 }
622
623 #[inline]
625 pub fn to_wildcard(self) -> DhttpName<'static> {
626 DhttpName(self.0.to_wildcard())
627 }
628
629 #[inline]
635 pub fn expand_uri(&self, uri: http::Uri) -> Result<http::Uri, ExpandUriError> {
636 Self::expand_uri_with_base(Some(self), uri)
637 }
638
639 pub fn expand_authority_with_base(
645 base: Option<&DhttpName<'_>>,
646 authority: http::uri::Authority,
647 ) -> Result<http::uri::Authority, ExpandAuthorityError> {
648 let raw = authority.as_str();
649 let host = authority.host();
650
651 let replacement = if host == "~" {
652 base.context(expand_authority_error::MissingBaseNameSnafu)?
653 .to_owned()
654 } else if let Some(partial) = host.strip_suffix('~') {
655 DhttpName::try_from(partial)?.into_owned()
656 } else if host.len() >= Self::SUFFIX.len()
657 && host[host.len() - Self::SUFFIX.len()..].eq_ignore_ascii_case(Self::SUFFIX)
658 {
659 let name = match Name::try_from(host) {
660 Ok(name) => name,
661 Err(source) => {
662 return Err(ExpandAuthorityError::InvalidName {
663 source: InvalidDhttpName::InvalidName { source },
664 });
665 }
666 };
667 DhttpName::try_from(name)?.into_owned()
668 } else {
669 return Ok(authority);
670 };
671
672 if raw == host {
673 let authority = replacement.as_full().to_owned();
674 return http::uri::Authority::from_maybe_shared(replacement.into_name().into_bytes())
675 .context(expand_authority_error::ParseAuthoritySnafu { authority });
676 }
677
678 let user_info_len = raw
679 .split_once('@')
680 .map(|(user_info, ..)| user_info.len() + 1)
681 .unwrap_or_default();
682 let host_len = host.len();
683 let authority = format!(
684 "{user_info}{host}{port}",
685 user_info = &raw[..user_info_len],
686 host = replacement.as_full(),
687 port = &raw[user_info_len + host_len..],
688 );
689 authority
690 .parse()
691 .context(expand_authority_error::ParseAuthoritySnafu {
692 authority: &authority,
693 })
694 }
695
696 pub fn expand_uri_with_base(
702 base: Option<&DhttpName<'_>>,
703 uri: http::Uri,
704 ) -> Result<http::Uri, ExpandUriError> {
705 let mut parts = uri.into_parts();
706
707 if let Some(authority) = parts.authority {
708 parts.authority = Some(
709 Self::expand_authority_with_base(base, authority)
710 .context(expand_uri_error::AuthoritySnafu)?,
711 );
712 }
713
714 http::Uri::from_parts(parts).context(expand_uri_error::ReconstructUriSnafu)
715 }
716}
717
718impl<'a> Deref for DhttpName<'a> {
721 type Target = Name<'a>;
722
723 #[inline]
724 fn deref(&self) -> &Name<'a> {
725 &self.0
726 }
727}
728
729impl Display for DhttpName<'_> {
735 #[inline]
736 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
737 f.write_str(self.as_partial())
738 }
739}
740
741impl From<DhttpName<'static>> for Name<'static> {
742 #[inline]
743 fn from(dn: DhttpName<'static>) -> Self {
744 dn.0
745 }
746}
747
748impl PartialEq for DhttpName<'_> {
749 #[inline]
750 fn eq(&self, other: &Self) -> bool {
751 self.0 == other.0
752 }
753}
754
755impl Eq for DhttpName<'_> {}
756
757impl Hash for DhttpName<'_> {
758 #[inline]
759 fn hash<H: Hasher>(&self, state: &mut H) {
760 self.0.hash(state)
761 }
762}
763
764impl Serialize for DhttpName<'_> {
765 #[inline]
766 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
767 where
768 S: serde::Serializer,
769 {
770 serializer.serialize_str(self.as_partial())
771 }
772}
773
774impl<'de> Deserialize<'de> for DhttpName<'static> {
775 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
776 where
777 D: serde::Deserializer<'de>,
778 {
779 let s: String = String::deserialize(deserializer)?;
780 DhttpName::try_from(s).map_err(serde::de::Error::custom)
781 }
782}
783
784impl FromStr for DhttpName<'static> {
785 type Err = InvalidDhttpName;
786
787 #[inline]
788 fn from_str(s: &str) -> Result<Self, Self::Err> {
789 DhttpName::try_from(s).map(DhttpName::into_owned)
790 }
791}
792
793impl<'a> TryFrom<&'a str> for DhttpName<'a> {
794 type Error = InvalidDhttpName;
795
796 #[inline]
797 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
798 DhttpName::try_from(value.as_bytes())
799 }
800}
801
802impl<'a> TryFrom<String> for DhttpName<'a> {
803 type Error = InvalidDhttpName;
804
805 #[inline]
806 fn try_from(value: String) -> Result<Self, Self::Error> {
807 DhttpName::try_from(value.into_bytes())
808 }
809}
810
811impl<'a> TryFrom<&'a [u8]> for DhttpName<'a> {
812 type Error = InvalidDhttpName;
813
814 #[inline]
815 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
816 DhttpName::try_from(CowBytes::Borrowed(value))
817 }
818}
819
820impl<'a, const N: usize> TryFrom<&'a [u8; N]> for DhttpName<'a> {
821 type Error = InvalidDhttpName;
822
823 #[inline]
824 fn try_from(value: &'a [u8; N]) -> Result<Self, Self::Error> {
825 DhttpName::try_from(&value[..])
826 }
827}
828
829impl<'a> TryFrom<Bytes> for DhttpName<'a> {
830 type Error = InvalidDhttpName;
831
832 #[inline]
833 fn try_from(value: Bytes) -> Result<Self, Self::Error> {
834 DhttpName::try_from(CowBytes::Owned(value))
835 }
836}
837
838impl<'a> TryFrom<Vec<u8>> for DhttpName<'a> {
839 type Error = InvalidDhttpName;
840
841 #[inline]
842 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
843 DhttpName::try_from(Bytes::from(value))
844 }
845}
846
847impl<'a> TryFrom<Name<'a>> for DhttpName<'a> {
848 type Error = InvalidDhttpName;
849
850 #[inline]
851 fn try_from(value: Name<'a>) -> Result<Self, Self::Error> {
852 if !value.as_str().ends_with(Self::SUFFIX) {
853 return Err(InvalidName::MissingSuffix {
854 suffix: DhttpName::SUFFIX.to_string(),
855 }
856 .into());
857 }
858 Ok(DhttpName(value))
859 }
860}
861
862impl<'a> TryFrom<CowBytes<'a>> for DhttpName<'a> {
863 type Error = InvalidDhttpName;
864
865 #[inline]
866 fn try_from(input: CowBytes<'a>) -> Result<Self, Self::Error> {
867 if input.as_ref().ends_with(Self::SUFFIX.as_bytes()) {
868 return match input {
869 CowBytes::Borrowed(input) => match Name::try_from(input) {
870 Ok(name) => Ok(DhttpName(name)),
871 Err(source) => Err(source.into()),
872 },
873 CowBytes::Owned(input) => match Name::try_from(input) {
874 Ok(name) => Ok(DhttpName(name)),
875 Err(source) => Err(source.into()),
876 },
877 };
878 }
879
880 let mut input = match input {
881 CowBytes::Borrowed(input) => BytesMut::from(input),
882 CowBytes::Owned(input) => BytesMut::from(input),
883 };
884 if input.ends_with(b"~") {
885 input.truncate(input.len() - 1);
886 }
887 input.extend_from_slice(Self::SUFFIX.as_bytes());
888 match Name::try_from(input.freeze()) {
889 Ok(name) => Ok(DhttpName(name)),
890 Err(source) => Err(source.into()),
891 }
892 }
893}
894
895impl<'a> TryFrom<Cow<'a, str>> for DhttpName<'a> {
896 type Error = InvalidDhttpName;
897
898 #[inline]
899 fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> {
900 match value {
901 Cow::Borrowed(value) => DhttpName::try_from(value),
902 Cow::Owned(value) => DhttpName::try_from(value),
903 }
904 }
905}
906
907impl<'a> TryFrom<Cow<'a, [u8]>> for DhttpName<'a> {
908 type Error = InvalidDhttpName;
909
910 #[inline]
911 fn try_from(value: Cow<'a, [u8]>) -> Result<Self, Self::Error> {
912 match value {
913 Cow::Borrowed(value) => DhttpName::try_from(value),
914 Cow::Owned(value) => DhttpName::try_from(value),
915 }
916 }
917}
918
919impl DhttpName<'_> {
920 #[inline]
922 pub fn to_owned(&self) -> DhttpName<'static> {
923 DhttpName(self.0.to_owned())
924 }
925
926 #[inline]
928 pub fn into_owned(self) -> DhttpName<'static> {
929 DhttpName(self.0.into_owned())
930 }
931}
932
933#[cfg(test)]
934mod tests {
935 use super::*;
936 use std::borrow::Cow;
937
938 #[test]
939 fn name_try_from_static_lowercase() {
940 let n = Name::try_from_static(b"example.com").unwrap();
941 assert_eq!(n.as_str(), "example.com");
942 }
943
944 #[test]
945 fn name_try_from_static_mixed_case() {
946 let n = Name::try_from_static(b"Example.COM").unwrap();
947 assert_eq!(n.as_str(), "example.com");
948 }
949
950 #[test]
951 fn name_try_from_static_wildcard() {
952 let n = Name::try_from_static(b"*.example.com").unwrap();
953 assert!(n.is_wildcard());
954 assert_eq!(n.as_str(), "*.example.com");
955 }
956
957 #[test]
958 fn name_try_from_static_invalid() {
959 let err = Name::try_from_static(b"!!!").unwrap_err();
960 assert!(matches!(err, InvalidName::InvalidCharacter {}));
961 }
962
963 #[test]
964 fn name_try_from_static_bytes_reuses_static_bytes_path() {
965 let name = Name::try_from_static(b"Example.COM").unwrap();
966
967 assert_eq!(name.as_str(), "example.com");
968 }
969
970 #[test]
971 fn name_from_str_trait() {
972 let n: Name = "example.com".parse().unwrap();
973 assert_eq!(n.as_str(), "example.com");
974 }
975
976 #[test]
977 fn name_from_str_trait_rejects_invalid() {
978 let result: Result<Name, _> = "INVALID!!!".parse();
979 assert!(result.is_err());
980 }
981
982 #[test]
983 fn name_try_from_str_valid() {
984 let n: Name = "example.com".parse().unwrap();
985 assert_eq!(n.as_str(), "example.com");
986 }
987
988 #[test]
989 fn name_try_from_str_too_long() {
990 let long = "a".repeat(254);
991 let err: Result<Name, _> = long.parse();
992 assert!(matches!(err.unwrap_err(), InvalidName::TooLong {}));
993 }
994
995 #[test]
996 fn name_try_from_str_empty() {
997 let err: Result<Name, _> = "".parse();
998 assert!(matches!(err.unwrap_err(), InvalidName::EmptyLabel {}));
999 }
1000
1001 #[test]
1002 fn name_try_from_str_invalid_char() {
1003 let err: Result<Name, _> = "hello!".parse();
1004 assert!(matches!(err.unwrap_err(), InvalidName::InvalidCharacter {}));
1005 }
1006
1007 #[test]
1008 fn name_try_from_str_label_too_long() {
1009 let long_label = format!("{}.com", "a".repeat(64));
1010 let err: Result<Name, _> = long_label.parse();
1011 assert!(matches!(err.unwrap_err(), InvalidName::LabelTooLong {}));
1012 }
1013
1014 #[test]
1015 fn name_wildcard() {
1016 let n: Name = "*.example.com".parse().unwrap();
1017 assert!(n.is_wildcard());
1018
1019 let m: Name = "foo.example.com".parse().unwrap();
1020 assert!(n.matches(&m));
1021 assert!(n.matches(&n));
1022 }
1023
1024 #[test]
1025 fn name_no_wildcard_match() {
1026 let n: Name = "a.example.com".parse().unwrap();
1027 let m: Name = "b.example.com".parse().unwrap();
1028 assert!(!n.matches(&m));
1029 }
1030
1031 #[test]
1032 fn name_exact_match() {
1033 let n: Name = "foo.example.com".parse().unwrap();
1034 let m: Name = "foo.example.com".parse().unwrap();
1035 assert!(n.matches(&m));
1036 }
1037
1038 #[test]
1039 fn name_hash_borrow_consistency() {
1040 use std::collections::HashSet;
1041 let n: Name = "example.com".parse().unwrap();
1042 let mut set = HashSet::new();
1043 set.insert(n.clone());
1044 assert!(set.contains("example.com"));
1045 }
1046
1047 #[test]
1048 fn name_clone_owned() {
1049 let n: Name = "example.com".parse().unwrap();
1050 let c = n.clone();
1051 assert_eq!(n, c);
1052 }
1053
1054 #[test]
1055 fn name_to_wildcard_name() {
1056 let n: Name = "foo.example.com".parse().unwrap();
1057 let w = n.to_wildcard();
1058 assert!(w.is_wildcard());
1059 assert_eq!(w.as_str(), "*.example.com");
1060 }
1061
1062 #[test]
1063 fn name_wildcard_already() {
1064 let n: Name = "*.example.com".parse().unwrap();
1065 let w = n.to_wildcard();
1066 assert_eq!(w.as_str(), "*.example.com");
1067 }
1068
1069 #[test]
1070 fn name_serialize_deserialize() {
1071 let n: Name = "example.com".parse().unwrap();
1072 let json = serde_json::to_string(&n).unwrap();
1073 assert_eq!(json, r#""example.com""#);
1074 let d: Name<'static> = serde_json::from_str(&json).unwrap();
1075 assert_eq!(n, d);
1076 }
1077
1078 #[test]
1079 fn name_display() {
1080 let n: Name = "Example.COM".parse().unwrap();
1081 assert_eq!(format!("{n}"), "example.com");
1082 }
1083
1084 #[test]
1087 fn name_try_from_ref_str_lowercase() {
1088 let n = Name::try_from("example.com").unwrap();
1089 assert_eq!(n.as_str(), "example.com");
1090 }
1091
1092 #[test]
1093 fn name_try_from_ref_str_mixed_case() {
1094 let n = Name::try_from("Example.COM").unwrap();
1095 assert_eq!(n.as_str(), "example.com");
1096 }
1097
1098 #[test]
1099 fn name_try_from_ref_str_wildcard() {
1100 let n = Name::try_from("*.example.com").unwrap();
1101 assert!(n.is_wildcard());
1102 assert_eq!(n.as_str(), "*.example.com");
1103 }
1104
1105 #[test]
1106 fn name_try_from_ref_str_invalid() {
1107 let err = Name::try_from("!!!").unwrap_err();
1108 assert!(matches!(err, InvalidName::InvalidCharacter {}));
1109 }
1110
1111 #[test]
1112 fn name_try_from_ref_str_borrowed_variant() {
1113 let input = "example.com";
1114 let n = Name::try_from(input).unwrap();
1115 assert_eq!(n.as_str(), "example.com");
1116 }
1117
1118 #[test]
1121 fn name_try_from_ref_bytes_lowercase() {
1122 let input: &[u8] = b"example.com";
1123 let n = Name::try_from(input).unwrap();
1124 assert_eq!(n.as_str(), "example.com");
1125 }
1126
1127 #[test]
1128 fn name_try_from_ref_bytes_mixed_case() {
1129 let input: &[u8] = b"Example.COM";
1130 let n = Name::try_from(input).unwrap();
1131 assert_eq!(n.as_str(), "example.com");
1132 }
1133
1134 #[test]
1135 fn name_try_from_ref_bytes_wildcard() {
1136 let input: &[u8] = b"*.example.com";
1137 let n = Name::try_from(input).unwrap();
1138 assert!(n.is_wildcard());
1139 assert_eq!(n.as_str(), "*.example.com");
1140 }
1141
1142 #[test]
1143 fn name_try_from_ref_bytes_invalid() {
1144 let input: &[u8] = b"!!!";
1145 let err = Name::try_from(input).unwrap_err();
1146 assert!(matches!(err, InvalidName::InvalidCharacter {}));
1147 }
1148
1149 #[test]
1152 fn name_try_from_string_mixed_case() {
1153 let s = String::from("Hello.World");
1154 let n = Name::try_from(s).unwrap();
1155 assert_eq!(n.as_str(), "hello.world");
1156 }
1157
1158 #[test]
1159 fn name_try_from_string_invalid() {
1160 let s = String::from("!!!");
1161 let err = Name::try_from(s).unwrap_err();
1162 assert!(matches!(err, InvalidName::InvalidCharacter {}));
1163 }
1164
1165 #[test]
1166 fn name_try_from_string_empty() {
1167 let s = String::new();
1168 let err = Name::try_from(s).unwrap_err();
1169 assert!(matches!(err, InvalidName::EmptyLabel {}));
1170 }
1171
1172 #[test]
1175 fn name_try_from_vec_u8_lowercase() {
1176 let n = Name::try_from(b"example.com".to_vec()).unwrap();
1177 assert_eq!(n.as_str(), "example.com");
1178 }
1179
1180 #[test]
1181 fn name_try_from_vec_u8_mixed_case() {
1182 let n = Name::try_from(b"Hello.World".to_vec()).unwrap();
1183 assert_eq!(n.as_str(), "hello.world");
1184 }
1185
1186 #[test]
1187 fn name_try_from_vec_u8_invalid() {
1188 let err = Name::try_from(b"!!!".to_vec()).unwrap_err();
1189 assert!(matches!(err, InvalidName::InvalidCharacter {}));
1190 }
1191
1192 #[test]
1195 fn name_try_from_cow_borrowed_lowercase() {
1196 let cow: Cow<'_, str> = Cow::Borrowed("example.com");
1197 let n = Name::try_from(cow).unwrap();
1198 assert_eq!(n.as_str(), "example.com");
1199 }
1200
1201 #[test]
1202 fn name_try_from_cow_borrowed_mixed_case() {
1203 let cow: Cow<'_, str> = Cow::Borrowed("Example.COM");
1204 let n = Name::try_from(cow).unwrap();
1205 assert_eq!(n.as_str(), "example.com");
1206 }
1207
1208 #[test]
1209 fn name_try_from_cow_owned_lowercase() {
1210 let cow: Cow<'_, str> = Cow::Owned("example.com".to_string());
1211 let n = Name::try_from(cow).unwrap();
1212 assert_eq!(n.as_str(), "example.com");
1213 }
1214
1215 #[test]
1216 fn name_try_from_cow_owned_mixed_case() {
1217 let cow: Cow<'_, str> = Cow::Owned("Example.COM".to_string());
1218 let n = Name::try_from(cow).unwrap();
1219 assert_eq!(n.as_str(), "example.com");
1220 }
1221
1222 #[test]
1223 fn name_try_from_cow_invalid() {
1224 let cow: Cow<'_, str> = Cow::Borrowed("!!!");
1225 let err = Name::try_from(cow).unwrap_err();
1226 assert!(matches!(err, InvalidName::InvalidCharacter {}));
1227 }
1228
1229 #[test]
1230 fn name_try_from_cow_bytes_borrowed_and_owned() {
1231 let borrowed = Cow::<[u8]>::Borrowed(b"Example.COM");
1232 let owned: Cow<'_, [u8]> = Cow::Owned(b"Reimu.Pilot".to_vec());
1233
1234 let borrowed_name = Name::try_from(borrowed).unwrap();
1235 let owned_name = Name::try_from(owned).unwrap();
1236
1237 assert_eq!(borrowed_name.as_str(), "example.com");
1238 assert_eq!(owned_name.as_str(), "reimu.pilot");
1239 }
1240
1241 #[test]
1244 fn dhttp_name_suffix_is_dhttp_net() {
1245 assert_eq!(DhttpName::SUFFIX, ".dhttp.net");
1246 }
1247
1248 #[test]
1249 fn dhttp_name_parse_full() {
1250 let dn = "hello.dhttp.net".parse::<DhttpName>().unwrap();
1251 assert_eq!(dn.as_full(), "hello.dhttp.net");
1252 assert_eq!(dn.as_partial(), "hello");
1253 }
1254
1255 #[test]
1256 fn dhttp_name_parse_partial_multi_label() {
1257 let dn = "reimu.pilot".parse::<DhttpName>().unwrap();
1258 assert_eq!(dn.as_full(), "reimu.pilot.dhttp.net");
1259 assert_eq!(dn.as_partial(), "reimu.pilot");
1260 }
1261
1262 #[test]
1263 fn dhttp_name_parse_partial_single_label_rejected() {
1264 let name = "hello".parse::<DhttpName>().unwrap();
1265
1266 assert_eq!(name.as_full(), "hello.dhttp.net");
1267 }
1268
1269 #[test]
1270 fn dhttp_name_serialize() {
1271 let dn = "reimu.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1272 let json = serde_json::to_string(&dn).unwrap();
1273 assert_eq!(json, "\"reimu.pilot\"");
1274 }
1275
1276 #[test]
1277 fn dhttp_name_deserialize_from_partial() {
1278 let dn: DhttpName<'static> = serde_json::from_str("\"reimu.pilot\"").unwrap();
1279 assert_eq!(dn.as_full(), "reimu.pilot.dhttp.net");
1280 }
1281
1282 #[test]
1283 fn dhttp_name_deserialize_from_full() {
1284 let dn: DhttpName<'static> = serde_json::from_str("\"reimu.pilot.dhttp.net\"").unwrap();
1285 assert_eq!(dn.as_full(), "reimu.pilot.dhttp.net");
1286 }
1287
1288 #[test]
1289 fn dhttp_name_deserialize_rejects_invalid() {
1290 let result: Result<DhttpName<'static>, _> = serde_json::from_str("\"!!!\"");
1291 assert!(result.is_err());
1292 }
1293
1294 #[test]
1295 fn dhttp_name_hash_consistent_with_name() {
1296 use std::hash::{DefaultHasher, Hasher};
1297 let dn = "reimu.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1298 let n = Name::try_from_static(b"reimu.pilot.dhttp.net").unwrap();
1299 let hash_dn = {
1300 let mut h = DefaultHasher::new();
1301 dn.hash(&mut h);
1302 h.finish()
1303 };
1304 let hash_n = {
1305 let mut h = DefaultHasher::new();
1306 n.hash(&mut h);
1307 h.finish()
1308 };
1309 assert_eq!(hash_dn, hash_n);
1310 }
1311
1312 #[test]
1313 fn dhttp_name_eq() {
1314 let a = "reimu.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1315 let b = "reimu.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1316 let c = "other.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1317 assert_eq!(a, b);
1318 assert_ne!(a, c);
1319 }
1320
1321 #[test]
1322 fn dhttp_name_to_owned_and_clone() {
1323 let dn = "reimu.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1324 let owned = dn.to_owned();
1325 assert_eq!(owned.as_full(), "reimu.pilot.dhttp.net");
1326 let cloned = owned.clone();
1327 assert_eq!(cloned.as_full(), "reimu.pilot.dhttp.net");
1328 }
1329
1330 #[test]
1331 fn dhttp_name_into_owned() {
1332 let dn = "reimu.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1333 let owned = dn.into_owned();
1334 assert_eq!(owned.as_full(), "reimu.pilot.dhttp.net");
1335 }
1336
1337 #[test]
1338 fn dhttp_name_to_wildcard_replaces_first_label() {
1339 let dn = "reimu.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1340
1341 let wildcard = dn.to_wildcard();
1342
1343 assert_eq!(wildcard.as_full(), "*.pilot.dhttp.net");
1344 }
1345
1346 #[test]
1347 fn dhttp_name_from_str_trait() {
1348 let dn: DhttpName = "reimu.pilot.dhttp.net".parse().unwrap();
1349 assert_eq!(dn.as_full(), "reimu.pilot.dhttp.net");
1350 }
1351
1352 #[test]
1353 fn dhttp_name_from_str_trait_rejects_invalid() {
1354 let result: Result<DhttpName, _> = "!!!".parse();
1355 assert!(result.is_err());
1356 }
1357
1358 #[test]
1359 fn dhttp_name_legacy_borrow_method() {
1360 let dn = "reimu.pilot".parse::<DhttpName>().unwrap();
1361 let borrowed = dn.borrow();
1362 assert_eq!(borrowed.as_full(), dn.as_full());
1363 }
1364
1365 #[test]
1366 fn dhttp_name_legacy_validate() {
1367 DhttpName::validate(b"reimu.pilot.dhttp.net").unwrap();
1368 assert!(DhttpName::validate(b"reimu.pilot").is_err());
1369 }
1370
1371 #[test]
1372 fn dhttp_name_try_from_str_expands_partial_name() {
1373 let name = DhttpName::try_from("reimu.pilot").unwrap();
1374 assert_eq!(name.as_full(), "reimu.pilot.dhttp.net");
1375 }
1376
1377 #[test]
1378 fn dhttp_name_try_from_string_expands_tilde_name() {
1379 let name = DhttpName::try_from(String::from("reimu.pilot~")).unwrap();
1380 assert_eq!(name.as_full(), "reimu.pilot.dhttp.net");
1381 }
1382
1383 #[test]
1384 fn dhttp_name_try_from_bytes_and_cow_bytes_append_suffix() {
1385 let from_bytes = DhttpName::try_from(Bytes::from_static(b"Reimu.Pilot")).unwrap();
1386 let from_cow: DhttpName<'_> =
1387 DhttpName::try_from(Cow::<[u8]>::Borrowed(b"Device")).unwrap();
1388
1389 assert_eq!(from_bytes.as_full(), "reimu.pilot.dhttp.net");
1390 assert_eq!(from_cow.as_full(), "device.dhttp.net");
1391 }
1392
1393 #[test]
1394 fn dhttp_name_try_from_static_bytes_appends_suffix() {
1395 let name = DhttpName::try_from_static(b"Device").unwrap();
1396
1397 assert_eq!(name.as_full(), "device.dhttp.net");
1398 }
1399
1400 #[test]
1401 fn dhttp_name_try_from_name_accepts_full_name_without_reparsing_string() {
1402 let name = Name::try_from("reimu.pilot.dhttp.net").unwrap();
1403
1404 let dhttp_name = DhttpName::try_from(name).unwrap();
1405
1406 assert_eq!(dhttp_name.as_full(), "reimu.pilot.dhttp.net");
1407 }
1408
1409 #[test]
1410 fn dhttp_name_try_from_name_rejects_missing_suffix() {
1411 let name = Name::try_from("example.com").unwrap();
1412
1413 let error = DhttpName::try_from(name).unwrap_err();
1414
1415 assert!(matches!(
1416 error,
1417 InvalidDhttpName::InvalidName {
1418 source: InvalidName::MissingSuffix { .. }
1419 }
1420 ));
1421 }
1422
1423 #[test]
1424 fn expand_uri_replaces_bare_tilde_with_self_name() {
1425 let name = "reimu.pilot".parse::<DhttpName>().unwrap();
1426 let uri = "https://~/api?q=1".parse().unwrap();
1427
1428 let expanded = name.expand_uri(uri).unwrap();
1429
1430 assert_eq!(
1431 expanded.to_string(),
1432 "https://reimu.pilot.dhttp.net/api?q=1"
1433 );
1434 }
1435
1436 #[test]
1437 fn expand_uri_expands_tilde_suffix_and_preserves_userinfo_port() {
1438 let name = "self.host".parse::<DhttpName>().unwrap();
1439 let uri = "https://alice@reimu.pilot~:443/api".parse().unwrap();
1440
1441 let expanded = name.expand_uri(uri).unwrap();
1442
1443 assert_eq!(
1444 expanded.to_string(),
1445 "https://alice@reimu.pilot.dhttp.net:443/api"
1446 );
1447 }
1448
1449 #[test]
1450 fn expand_authority_expands_tilde_suffix_and_preserves_userinfo_port() {
1451 let name = "self.host".parse::<DhttpName>().unwrap();
1452 let authority = "alice@reimu.pilot~:443".parse().unwrap();
1453
1454 let expanded = DhttpName::expand_authority_with_base(Some(&name), authority).unwrap();
1455
1456 assert_eq!(expanded.as_str(), "alice@reimu.pilot.dhttp.net:443");
1457 }
1458
1459 #[test]
1460 fn expand_authority_canonicalizes_mixed_case_host_only_dhttp_name() {
1461 let authority = "Reimu.Pilot.Dhttp.Net".parse().unwrap();
1462
1463 let expanded = DhttpName::expand_authority_with_base(None, authority).unwrap();
1464
1465 assert_eq!(expanded.as_str(), "reimu.pilot.dhttp.net");
1466 }
1467
1468 #[test]
1469 fn expand_authority_canonicalizes_mixed_case_decorated_dhttp_name() {
1470 let authority = "alice@Reimu.Pilot.Dhttp.Net:443".parse().unwrap();
1471
1472 let expanded = DhttpName::expand_authority_with_base(None, authority).unwrap();
1473
1474 assert_eq!(expanded.as_str(), "alice@reimu.pilot.dhttp.net:443");
1475 }
1476
1477 #[test]
1478 fn expand_authority_host_only_partial_uses_canonical_name() {
1479 let authority = "Reimu.Pilot~".parse().unwrap();
1480
1481 let expanded = DhttpName::expand_authority_with_base(None, authority).unwrap();
1482
1483 assert_eq!(expanded.as_str(), "reimu.pilot.dhttp.net");
1484 }
1485
1486 #[test]
1487 fn expand_authority_with_base_requires_base_name_for_bare_tilde() {
1488 let authority = "~".parse().unwrap();
1489
1490 let error = DhttpName::expand_authority_with_base(None, authority).unwrap_err();
1491
1492 assert!(matches!(error, ExpandAuthorityError::MissingBaseName));
1493 }
1494
1495 #[test]
1496 fn expand_uri_leaves_plain_host_unchanged() {
1497 let name = "self.host".parse::<DhttpName>().unwrap();
1498 let uri: http::Uri = "https://example.com/api".parse().unwrap();
1499
1500 let expanded = name.expand_uri(uri.clone()).unwrap();
1501
1502 assert_eq!(expanded, uri);
1503 }
1504
1505 #[test]
1506 fn expand_uri_rejects_invalid_expanded_name() {
1507 let name = "self.host".parse::<DhttpName>().unwrap();
1508 let uri = "https://123~/api".parse().unwrap();
1509
1510 let error = name.expand_uri(uri).unwrap_err();
1511
1512 assert!(matches!(
1513 error,
1514 ExpandUriError::Authority {
1515 source: ExpandAuthorityError::InvalidName { .. }
1516 }
1517 ));
1518 }
1519
1520 #[test]
1521 fn expand_uri_with_base_expands_partial_without_base_name() {
1522 let uri = "https://reimu.pilot~/api".parse().unwrap();
1523
1524 let expanded = DhttpName::expand_uri_with_base(None, uri).unwrap();
1525
1526 assert_eq!(expanded.to_string(), "https://reimu.pilot.dhttp.net/api");
1527 }
1528
1529 #[test]
1530 fn expand_uri_with_base_requires_base_name_for_bare_tilde() {
1531 let uri = "https://~/api".parse().unwrap();
1532
1533 let error = DhttpName::expand_uri_with_base(None, uri).unwrap_err();
1534
1535 assert!(matches!(
1536 error,
1537 ExpandUriError::Authority {
1538 source: ExpandAuthorityError::MissingBaseName
1539 }
1540 ));
1541 }
1542}