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)]
72pub enum CowBytes<'a> {
73 Borrowed(&'a [u8]),
74 Owned(Bytes),
75}
76
77impl<'a> From<&'a str> for CowBytes<'a> {
78 #[inline]
79 fn from(value: &'a str) -> Self {
80 Self::Borrowed(value.as_bytes())
81 }
82}
83
84impl<'a> From<&'a [u8]> for CowBytes<'a> {
85 #[inline]
86 fn from(value: &'a [u8]) -> Self {
87 Self::Borrowed(value)
88 }
89}
90
91impl<'a, const N: usize> From<&'a [u8; N]> for CowBytes<'a> {
92 #[inline]
93 fn from(value: &'a [u8; N]) -> Self {
94 Self::Borrowed(&value[..])
95 }
96}
97
98impl From<String> for CowBytes<'static> {
99 #[inline]
100 fn from(value: String) -> Self {
101 Self::Owned(Bytes::from(value.into_bytes()))
102 }
103}
104
105impl From<Vec<u8>> for CowBytes<'static> {
106 #[inline]
107 fn from(value: Vec<u8>) -> Self {
108 Self::Owned(Bytes::from(value))
109 }
110}
111
112impl From<Bytes> for CowBytes<'static> {
113 #[inline]
114 fn from(value: Bytes) -> Self {
115 Self::Owned(value)
116 }
117}
118
119impl<'a> From<Cow<'a, str>> for CowBytes<'a> {
120 #[inline]
121 fn from(value: Cow<'a, str>) -> Self {
122 match value {
123 Cow::Borrowed(value) => Self::Borrowed(value.as_bytes()),
124 Cow::Owned(value) => Self::Owned(Bytes::from(value.into_bytes())),
125 }
126 }
127}
128
129impl<'a> From<Cow<'a, [u8]>> for CowBytes<'a> {
130 #[inline]
131 fn from(value: Cow<'a, [u8]>) -> Self {
132 match value {
133 Cow::Borrowed(value) => Self::Borrowed(value),
134 Cow::Owned(value) => Self::Owned(Bytes::from(value)),
135 }
136 }
137}
138
139impl AsRef<[u8]> for CowBytes<'_> {
140 #[inline]
141 fn as_ref(&self) -> &[u8] {
142 match self {
143 Self::Borrowed(bytes) => bytes,
144 Self::Owned(bytes) => bytes,
145 }
146 }
147}
148
149fn expand_dhttp_shorthand<'a>(input: CowBytes<'a>) -> CowBytes<'a> {
150 let bytes = input.as_ref();
151 if !bytes.contains(&b'~') {
152 return input;
153 }
154
155 let suffix = DhttpName::SUFFIX.as_bytes();
156 let extra = bytes.iter().filter(|byte| **byte == b'~').count() * (suffix.len() - 1);
157 let mut expanded = BytesMut::with_capacity(bytes.len() + extra);
158 for byte in bytes {
159 if *byte == b'~' {
160 expanded.extend_from_slice(suffix);
161 } else {
162 expanded.extend_from_slice(&[*byte]);
163 }
164 }
165 CowBytes::Owned(expanded.freeze())
166}
167
168#[derive(Clone, Debug)]
169enum CowBytesStr<'a> {
170 Borrowed(&'a str),
171 Owned(BytesStr),
172}
173
174impl CowBytesStr<'_> {
175 #[inline]
176 fn modify(&mut self, modify: impl FnOnce(&mut String)) {
177 match self {
178 Self::Borrowed(value) => {
179 let mut owned = BytesStr(Bytes::from(value.to_owned()));
180 owned.modify(modify);
181 *self = Self::Owned(owned);
182 }
183 Self::Owned(value) => value.modify(modify),
184 }
185 }
186
187 #[inline]
188 fn into_owned(self) -> CowBytesStr<'static> {
189 match self {
190 Self::Borrowed(value) => CowBytesStr::Owned(BytesStr(Bytes::from(value.to_owned()))),
191 Self::Owned(value) => CowBytesStr::Owned(value),
192 }
193 }
194
195 #[inline]
196 fn into_bytes(self) -> Bytes {
197 match self {
198 Self::Borrowed(value) => Bytes::from(value.to_owned()),
199 Self::Owned(value) => value.0,
200 }
201 }
202}
203
204impl AsRef<str> for CowBytesStr<'_> {
205 #[inline]
206 fn as_ref(&self) -> &str {
207 match self {
208 Self::Borrowed(value) => value,
209 Self::Owned(value) => value.as_ref(),
210 }
211 }
212}
213
214#[derive(Clone, Debug)]
215struct DnsName<S>(S);
216
217impl<S: AsRef<str>> AsRef<str> for DnsName<S> {
218 #[inline]
219 fn as_ref(&self) -> &str {
220 self.0.as_ref()
221 }
222}
223
224impl<S: AsRef<[u8]>> DnsName<S> {
225 const MAX_LABEL_LENGTH: usize = 63;
226 const MAX_LENGTH: usize = 253;
227
228 fn validate(input: S) -> Result<S, InvalidName> {
238 enum State {
239 Start,
240 Next,
241 NumericOnly { len: usize },
242 Subsequent { len: usize },
243 Hyphen { len: usize },
244 Wildcard,
245 }
246
247 use State::*;
248
249 let bytes = input.as_ref();
250
251 if bytes.len() > Self::MAX_LENGTH {
252 return Err(InvalidName::TooLong {});
253 }
254
255 let mut state = Start;
256 let mut idx = 0;
257 while idx < bytes.len() {
258 let ch = bytes[idx];
259 state = match (state, ch) {
260 (Start, b'*') => Wildcard,
261 (Wildcard, b'.') => Next,
262 (Start | Next | Hyphen { .. }, b'.') => {
263 return Err(InvalidName::EmptyLabel {});
264 }
265 (Subsequent { .. }, b'.') => Next,
266 (NumericOnly { .. }, b'.') => return Err(InvalidName::EmptyLabel {}),
267 (Subsequent { len } | NumericOnly { len } | Hyphen { len }, _)
268 if len >= Self::MAX_LABEL_LENGTH =>
269 {
270 return Err(InvalidName::LabelTooLong {});
271 }
272 (Start | Next, b'0'..=b'9') => NumericOnly { len: 1 },
273 (NumericOnly { len }, b'0'..=b'9') => NumericOnly { len: len + 1 },
274 (Start | Next, b'a'..=b'z' | b'A'..=b'Z' | b'_') => Subsequent { len: 1 },
275 (Subsequent { len } | NumericOnly { len } | Hyphen { len }, b'-') => {
276 Hyphen { len: len + 1 }
277 }
278 (
279 Subsequent { len } | NumericOnly { len } | Hyphen { len },
280 b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'0'..=b'9',
281 ) => Subsequent { len: len + 1 },
282 _ => return Err(InvalidName::InvalidCharacter {}),
283 };
284 idx += 1;
285 }
286
287 if matches!(state, Start | Hyphen { .. } | NumericOnly { .. }) {
288 return Err(InvalidName::EmptyLabel {});
289 }
290
291 Ok(input)
292 }
293}
294
295impl<'a> TryFrom<CowBytes<'a>> for DnsName<CowBytesStr<'a>> {
296 type Error = InvalidName;
297
298 #[inline]
299 fn try_from(value: CowBytes<'a>) -> Result<Self, Self::Error> {
300 let value = DnsName::<CowBytes>::validate(value)?;
301 Ok(DnsName(match value {
302 CowBytes::Borrowed(bytes) => {
303 CowBytesStr::Borrowed(unsafe { std::str::from_utf8_unchecked(bytes) })
306 }
307 CowBytes::Owned(bytes) => CowBytesStr::Owned(BytesStr(bytes)),
308 }))
309 }
310}
311
312impl<'a> DnsName<CowBytesStr<'a>> {
313 #[inline]
314 fn try_from_static(value: &'static [u8]) -> Result<Self, InvalidName> {
315 DnsName::try_from(CowBytes::Owned(Bytes::from_static(value)))
316 }
317}
318
319#[derive(Debug, Snafu)]
324pub enum InvalidName {
325 #[snafu(display("name too long (max {} characters)", Name::MAX_LENGTH))]
326 TooLong {},
327 #[snafu(display("label too long (max {} characters)", Name::MAX_LABEL_LENGTH))]
328 LabelTooLong {},
329 #[snafu(display("name contains empty or numeric / hyphen only label"))]
330 EmptyLabel {},
331 #[snafu(display("name contains invalid characters"))]
332 InvalidCharacter {},
333 #[snafu(display("name is missing required suffix {suffix}"))]
334 MissingSuffix { suffix: String },
335}
336
337#[derive(Clone, Debug)]
347pub struct Name<'a>(DnsName<CowBytesStr<'a>>);
348
349impl Name<'_> {
350 pub const MAX_LABEL_LENGTH: usize = DnsName::<CowBytes<'static>>::MAX_LABEL_LENGTH;
351 pub const MAX_LENGTH: usize = DnsName::<CowBytes<'static>>::MAX_LENGTH;
352
353 #[inline]
359 pub fn from_dhttp_shorthand<'a>(
360 input: impl Into<CowBytes<'a>>,
361 ) -> Result<Name<'a>, InvalidName> {
362 let input = expand_dhttp_shorthand(input.into());
363 DnsName::try_from(input).map(Name::from)
364 }
365
366 #[inline]
368 pub fn as_str(&self) -> &str {
369 self.0.as_ref()
370 }
371
372 #[inline]
374 pub fn as_full(&self) -> &str {
375 self.as_str()
376 }
377
378 #[inline]
380 pub fn to_owned(&self) -> Name<'static> {
381 Name(DnsName(self.0.0.clone().into_owned()))
382 }
383
384 #[inline]
386 pub fn into_owned(self) -> Name<'static> {
387 Name(DnsName(self.0.0.into_owned()))
388 }
389
390 #[inline]
395 pub fn into_bytes(self) -> Bytes {
396 self.0.0.into_bytes()
397 }
398
399 #[inline]
404 pub fn to_wildcard(self) -> Name<'static> {
405 if self.is_wildcard() {
406 return self.into_owned();
407 }
408 if let Some((_head, tail)) = self.as_str().split_once('.') {
409 let wild = format!("*.{tail}");
410 return wild.parse().expect("wildcard of valid name must be valid");
411 }
412 self.into_owned()
414 }
415
416 #[inline]
418 pub fn is_wildcard(&self) -> bool {
419 self.as_str().starts_with('*')
420 }
421
422 #[inline]
428 pub fn matches(&self, name: &Name) -> bool {
429 if !self.is_wildcard() {
430 return self == name;
431 }
432
433 let self_tails = &self.as_str()[2..]; name.as_str()
435 .split_once('.')
436 .is_some_and(|(.., tails)| tails == self_tails)
437 }
438
439 #[inline]
440 pub fn try_from_static(bytes: &'static [u8]) -> Result<Name<'static>, InvalidName> {
441 Ok(Name::from(
442 DnsName::<CowBytesStr<'static>>::try_from_static(bytes)?,
443 ))
444 }
445}
446
447impl Deref for Name<'_> {
450 type Target = str;
451
452 #[inline]
453 fn deref(&self) -> &str {
454 self.as_str()
455 }
456}
457
458impl Hash for Name<'_> {
459 #[inline]
460 fn hash<H: Hasher>(&self, state: &mut H) {
461 <str as Hash>::hash(Borrow::<str>::borrow(self), state)
462 }
463}
464
465impl Borrow<str> for Name<'_> {
466 #[inline]
467 fn borrow(&self) -> &str {
468 self.as_str()
469 }
470}
471
472impl PartialEq for Name<'_> {
473 #[inline]
474 fn eq(&self, other: &Self) -> bool {
475 self.as_str() == other.as_str()
476 }
477}
478
479impl Eq for Name<'_> {}
480
481impl Display for Name<'_> {
482 #[inline]
483 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
484 f.write_str(self.as_str())
485 }
486}
487
488impl Serialize for Name<'_> {
489 #[inline]
490 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
491 where
492 S: serde::Serializer,
493 {
494 serializer.serialize_str(self.as_str())
495 }
496}
497
498impl<'de> Deserialize<'de> for Name<'static> {
499 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
500 where
501 D: serde::Deserializer<'de>,
502 {
503 let s: String = String::deserialize(deserializer)?;
504 Name::try_from(s).map_err(serde::de::Error::custom)
505 }
506}
507
508impl<'a> From<DnsName<CowBytesStr<'a>>> for Name<'a> {
509 #[inline]
510 fn from(mut value: DnsName<CowBytesStr<'a>>) -> Self {
511 if value.as_ref().bytes().any(|byte| byte.is_ascii_uppercase()) {
512 value.0.modify(|string| string.make_ascii_lowercase());
513 }
514 Name(value)
515 }
516}
517
518impl<'a> TryFrom<&'a str> for Name<'a> {
522 type Error = InvalidName;
523
524 #[inline]
525 fn try_from(s: &'a str) -> Result<Self, Self::Error> {
526 Name::try_from(s.as_bytes())
527 }
528}
529
530impl<'a> TryFrom<&'a [u8]> for Name<'a> {
532 type Error = InvalidName;
533
534 #[inline]
535 fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> {
536 DnsName::try_from(CowBytes::Borrowed(bytes)).map(Name::from)
537 }
538}
539
540impl<'a, const N: usize> TryFrom<&'a [u8; N]> for Name<'a> {
541 type Error = InvalidName;
542
543 #[inline]
544 fn try_from(bytes: &'a [u8; N]) -> Result<Self, Self::Error> {
545 Name::try_from(&bytes[..])
546 }
547}
548
549impl FromStr for Name<'static> {
553 type Err = InvalidName;
554
555 #[inline]
556 fn from_str(s: &str) -> Result<Self, Self::Err> {
557 Name::try_from(s).map(Name::into_owned)
558 }
559}
560
561impl TryFrom<String> for Name<'_> {
562 type Error = InvalidName;
563
564 #[inline]
565 fn try_from(s: String) -> Result<Self, Self::Error> {
566 Name::try_from(s.into_bytes())
567 }
568}
569
570impl TryFrom<Vec<u8>> for Name<'_> {
571 type Error = InvalidName;
572
573 #[inline]
574 fn try_from(v: Vec<u8>) -> Result<Self, Self::Error> {
575 Name::try_from(Bytes::from(v))
576 }
577}
578
579impl TryFrom<Bytes> for Name<'_> {
580 type Error = InvalidName;
581
582 #[inline]
583 fn try_from(bytes: Bytes) -> Result<Self, Self::Error> {
584 DnsName::try_from(CowBytes::Owned(bytes)).map(Name::from)
585 }
586}
587
588impl<'a> TryFrom<Cow<'a, str>> for Name<'a> {
591 type Error = InvalidName;
592
593 #[inline]
594 fn try_from(cow: Cow<'a, str>) -> Result<Self, Self::Error> {
595 match cow {
596 Cow::Borrowed(s) => Name::try_from(s),
597 Cow::Owned(s) => Name::try_from(s),
598 }
599 }
600}
601
602impl<'a> TryFrom<Cow<'a, [u8]>> for Name<'a> {
603 type Error = InvalidName;
604
605 #[inline]
606 fn try_from(cow: Cow<'a, [u8]>) -> Result<Self, Self::Error> {
607 match cow {
608 Cow::Borrowed(bytes) => Name::try_from(bytes),
609 Cow::Owned(bytes) => Name::try_from(bytes),
610 }
611 }
612}
613
614#[derive(Debug, Snafu)]
619pub enum InvalidDhttpName {
620 #[snafu(transparent)]
621 InvalidName { source: InvalidName },
622}
623
624#[derive(Debug, Snafu)]
625#[snafu(module)]
626pub enum ExpandAuthorityError {
627 #[snafu(transparent)]
628 InvalidName { source: InvalidDhttpName },
629 #[snafu(display("cannot expand bare dhttp shorthand without a base name"))]
630 MissingBaseName,
631 #[snafu(display("failed to parse expanded authority `{authority}`"))]
632 ParseAuthority {
633 authority: String,
634 source: http::uri::InvalidUri,
635 },
636}
637
638#[derive(Debug, Snafu)]
639#[snafu(module)]
640pub enum ExpandUriError {
641 #[snafu(display("failed to expand dhttp shorthand in uri authority"))]
642 Authority { source: ExpandAuthorityError },
643 #[snafu(display("failed to reconstruct uri with expanded dhttp name"))]
644 ReconstructUri { source: http::uri::InvalidUriParts },
645}
646
647#[derive(Clone, Debug)]
656pub struct DhttpName<'a>(Name<'a>);
657
658impl DhttpName<'_> {
659 pub const SUFFIX: &'static str = ".dhttp.net";
660
661 #[inline]
663 pub fn validate(input: &[u8]) -> Result<(), InvalidDhttpName> {
664 if !input.ends_with(Self::SUFFIX.as_bytes()) {
665 return Err(InvalidName::MissingSuffix {
666 suffix: Self::SUFFIX.to_string(),
667 }
668 .into());
669 }
670 match DnsName::<&[u8]>::validate(input) {
671 Ok(_) => Ok(()),
672 Err(source) => Err(source.into()),
673 }
674 }
675
676 #[inline]
677 pub fn try_from_static(input: &'static [u8]) -> Result<DhttpName<'static>, InvalidDhttpName> {
678 DhttpName::try_from(Bytes::from_static(input))
679 }
680
681 #[inline]
683 pub fn into_name(self) -> Name<'static> {
684 self.0.into_owned()
685 }
686
687 #[inline]
694 pub fn as_partial(&self) -> &str {
695 debug_assert!(self.0.as_str().ends_with(Self::SUFFIX));
696 &self.0.as_str()[..self.0.as_str().len() - Self::SUFFIX.len()]
697 }
698
699 #[inline]
701 pub fn as_full(&self) -> &str {
702 self.0.as_str()
703 }
704
705 #[inline]
707 pub fn as_name(&self) -> &Name<'_> {
708 &self.0
709 }
710
711 #[inline]
713 pub fn borrow(&self) -> DhttpName<'_> {
714 DhttpName(Name(DnsName(CowBytesStr::Borrowed(self.0.as_str()))))
715 }
716
717 #[inline]
719 pub fn to_wildcard(self) -> DhttpName<'static> {
720 DhttpName(self.0.to_wildcard())
721 }
722
723 #[inline]
729 pub fn expand_uri(&self, uri: http::Uri) -> Result<http::Uri, ExpandUriError> {
730 Self::expand_uri_with_base(Some(self), uri)
731 }
732
733 pub fn expand_authority_with_base(
739 base: Option<&DhttpName<'_>>,
740 authority: http::uri::Authority,
741 ) -> Result<http::uri::Authority, ExpandAuthorityError> {
742 let raw = authority.as_str();
743 let host = authority.host();
744
745 let replacement = if host == "~" {
746 base.context(expand_authority_error::MissingBaseNameSnafu)?
747 .as_name()
748 .to_owned()
749 } else if host.as_bytes().contains(&b'~') {
750 Name::from_dhttp_shorthand(host)
751 .map_err(|source| ExpandAuthorityError::InvalidName {
752 source: InvalidDhttpName::InvalidName { source },
753 })?
754 .into_owned()
755 } else if host.len() >= Self::SUFFIX.len()
756 && host[host.len() - Self::SUFFIX.len()..].eq_ignore_ascii_case(Self::SUFFIX)
757 {
758 let name = match Name::try_from(host) {
759 Ok(name) => name,
760 Err(source) => {
761 return Err(ExpandAuthorityError::InvalidName {
762 source: InvalidDhttpName::InvalidName { source },
763 });
764 }
765 };
766 DhttpName::try_from(name)?.into_name()
767 } else {
768 return Ok(authority);
769 };
770
771 if raw == host {
772 let authority = replacement.as_full().to_owned();
773 return http::uri::Authority::from_maybe_shared(replacement.into_bytes())
774 .context(expand_authority_error::ParseAuthoritySnafu { authority });
775 }
776
777 let user_info_len = raw
778 .split_once('@')
779 .map(|(user_info, ..)| user_info.len() + 1)
780 .unwrap_or_default();
781 let host_len = host.len();
782 let authority = format!(
783 "{user_info}{host}{port}",
784 user_info = &raw[..user_info_len],
785 host = replacement.as_full(),
786 port = &raw[user_info_len + host_len..],
787 );
788 authority
789 .parse()
790 .context(expand_authority_error::ParseAuthoritySnafu {
791 authority: &authority,
792 })
793 }
794
795 pub fn expand_uri_with_base(
801 base: Option<&DhttpName<'_>>,
802 uri: http::Uri,
803 ) -> Result<http::Uri, ExpandUriError> {
804 let mut parts = uri.into_parts();
805
806 if let Some(authority) = parts.authority {
807 parts.authority = Some(
808 Self::expand_authority_with_base(base, authority)
809 .context(expand_uri_error::AuthoritySnafu)?,
810 );
811 }
812
813 http::Uri::from_parts(parts).context(expand_uri_error::ReconstructUriSnafu)
814 }
815}
816
817impl<'a> Deref for DhttpName<'a> {
820 type Target = Name<'a>;
821
822 #[inline]
823 fn deref(&self) -> &Name<'a> {
824 &self.0
825 }
826}
827
828impl Display for DhttpName<'_> {
834 #[inline]
835 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
836 f.write_str(self.as_partial())
837 }
838}
839
840impl From<DhttpName<'static>> for Name<'static> {
841 #[inline]
842 fn from(dn: DhttpName<'static>) -> Self {
843 dn.0
844 }
845}
846
847impl PartialEq for DhttpName<'_> {
848 #[inline]
849 fn eq(&self, other: &Self) -> bool {
850 self.0 == other.0
851 }
852}
853
854impl Eq for DhttpName<'_> {}
855
856impl Hash for DhttpName<'_> {
857 #[inline]
858 fn hash<H: Hasher>(&self, state: &mut H) {
859 self.0.hash(state)
860 }
861}
862
863impl Serialize for DhttpName<'_> {
864 #[inline]
865 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
866 where
867 S: serde::Serializer,
868 {
869 serializer.serialize_str(self.as_partial())
870 }
871}
872
873impl<'de> Deserialize<'de> for DhttpName<'static> {
874 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
875 where
876 D: serde::Deserializer<'de>,
877 {
878 let s: String = String::deserialize(deserializer)?;
879 DhttpName::try_from(s).map_err(serde::de::Error::custom)
880 }
881}
882
883impl FromStr for DhttpName<'static> {
884 type Err = InvalidDhttpName;
885
886 #[inline]
887 fn from_str(s: &str) -> Result<Self, Self::Err> {
888 DhttpName::try_from(s).map(DhttpName::into_owned)
889 }
890}
891
892impl<'a> TryFrom<&'a str> for DhttpName<'a> {
893 type Error = InvalidDhttpName;
894
895 #[inline]
896 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
897 DhttpName::try_from(value.as_bytes())
898 }
899}
900
901impl<'a> TryFrom<String> for DhttpName<'a> {
902 type Error = InvalidDhttpName;
903
904 #[inline]
905 fn try_from(value: String) -> Result<Self, Self::Error> {
906 DhttpName::try_from(value.into_bytes())
907 }
908}
909
910impl<'a> TryFrom<&'a [u8]> for DhttpName<'a> {
911 type Error = InvalidDhttpName;
912
913 #[inline]
914 fn try_from(value: &'a [u8]) -> Result<Self, Self::Error> {
915 DhttpName::try_from(CowBytes::Borrowed(value))
916 }
917}
918
919impl<'a, const N: usize> TryFrom<&'a [u8; N]> for DhttpName<'a> {
920 type Error = InvalidDhttpName;
921
922 #[inline]
923 fn try_from(value: &'a [u8; N]) -> Result<Self, Self::Error> {
924 DhttpName::try_from(&value[..])
925 }
926}
927
928impl<'a> TryFrom<Bytes> for DhttpName<'a> {
929 type Error = InvalidDhttpName;
930
931 #[inline]
932 fn try_from(value: Bytes) -> Result<Self, Self::Error> {
933 DhttpName::try_from(CowBytes::Owned(value))
934 }
935}
936
937impl<'a> TryFrom<Vec<u8>> for DhttpName<'a> {
938 type Error = InvalidDhttpName;
939
940 #[inline]
941 fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
942 DhttpName::try_from(Bytes::from(value))
943 }
944}
945
946impl<'a> TryFrom<Name<'a>> for DhttpName<'a> {
947 type Error = InvalidDhttpName;
948
949 #[inline]
950 fn try_from(value: Name<'a>) -> Result<Self, Self::Error> {
951 if !value.as_str().ends_with(Self::SUFFIX) {
952 return Err(InvalidName::MissingSuffix {
953 suffix: DhttpName::SUFFIX.to_string(),
954 }
955 .into());
956 }
957 Ok(DhttpName(value))
958 }
959}
960
961impl<'a> TryFrom<CowBytes<'a>> for DhttpName<'a> {
962 type Error = InvalidDhttpName;
963
964 #[inline]
965 fn try_from(input: CowBytes<'a>) -> Result<Self, Self::Error> {
966 if input.as_ref().ends_with(Self::SUFFIX.as_bytes()) {
967 return match input {
968 CowBytes::Borrowed(input) => match Name::try_from(input) {
969 Ok(name) => Ok(DhttpName(name)),
970 Err(source) => Err(source.into()),
971 },
972 CowBytes::Owned(input) => match Name::try_from(input) {
973 Ok(name) => Ok(DhttpName(name)),
974 Err(source) => Err(source.into()),
975 },
976 };
977 }
978
979 let mut input = match input {
980 CowBytes::Borrowed(input) => BytesMut::from(input),
981 CowBytes::Owned(input) => BytesMut::from(input),
982 };
983 if input.ends_with(b"~") {
984 input.truncate(input.len() - 1);
985 }
986 input.extend_from_slice(Self::SUFFIX.as_bytes());
987 match Name::try_from(input.freeze()) {
988 Ok(name) => Ok(DhttpName(name)),
989 Err(source) => Err(source.into()),
990 }
991 }
992}
993
994impl<'a> TryFrom<Cow<'a, str>> for DhttpName<'a> {
995 type Error = InvalidDhttpName;
996
997 #[inline]
998 fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> {
999 match value {
1000 Cow::Borrowed(value) => DhttpName::try_from(value),
1001 Cow::Owned(value) => DhttpName::try_from(value),
1002 }
1003 }
1004}
1005
1006impl<'a> TryFrom<Cow<'a, [u8]>> for DhttpName<'a> {
1007 type Error = InvalidDhttpName;
1008
1009 #[inline]
1010 fn try_from(value: Cow<'a, [u8]>) -> Result<Self, Self::Error> {
1011 match value {
1012 Cow::Borrowed(value) => DhttpName::try_from(value),
1013 Cow::Owned(value) => DhttpName::try_from(value),
1014 }
1015 }
1016}
1017
1018impl DhttpName<'_> {
1019 #[inline]
1021 pub fn to_owned(&self) -> DhttpName<'static> {
1022 DhttpName(self.0.to_owned())
1023 }
1024
1025 #[inline]
1027 pub fn into_owned(self) -> DhttpName<'static> {
1028 DhttpName(self.0.into_owned())
1029 }
1030}
1031
1032#[cfg(test)]
1033mod tests {
1034 use super::*;
1035 use std::borrow::Cow;
1036
1037 #[test]
1038 fn name_try_from_static_lowercase() {
1039 let n = Name::try_from_static(b"example.com").unwrap();
1040 assert_eq!(n.as_str(), "example.com");
1041 }
1042
1043 #[test]
1044 fn name_try_from_static_mixed_case() {
1045 let n = Name::try_from_static(b"Example.COM").unwrap();
1046 assert_eq!(n.as_str(), "example.com");
1047 }
1048
1049 #[test]
1050 fn name_try_from_static_wildcard() {
1051 let n = Name::try_from_static(b"*.example.com").unwrap();
1052 assert!(n.is_wildcard());
1053 assert_eq!(n.as_str(), "*.example.com");
1054 }
1055
1056 #[test]
1057 fn name_try_from_static_invalid() {
1058 let err = Name::try_from_static(b"!!!").unwrap_err();
1059 assert!(matches!(err, InvalidName::InvalidCharacter {}));
1060 }
1061
1062 #[test]
1063 fn name_try_from_static_bytes_reuses_static_bytes_path() {
1064 let name = Name::try_from_static(b"Example.COM").unwrap();
1065
1066 assert_eq!(name.as_str(), "example.com");
1067 }
1068
1069 #[test]
1070 fn name_from_str_trait() {
1071 let n: Name = "example.com".parse().unwrap();
1072 assert_eq!(n.as_str(), "example.com");
1073 }
1074
1075 #[test]
1076 fn name_from_str_trait_rejects_invalid() {
1077 let result: Result<Name, _> = "INVALID!!!".parse();
1078 assert!(result.is_err());
1079 }
1080
1081 #[test]
1082 fn name_from_dhttp_shorthand_leaves_plain_name_unchanged() {
1083 let input = "alice.margatroid";
1084
1085 let name = Name::from_dhttp_shorthand(input).expect("plain name should parse");
1086
1087 assert_eq!(name.as_full(), "alice.margatroid");
1088 }
1089
1090 #[test]
1091 fn name_from_dhttp_shorthand_borrows_lowercase_without_marker() {
1092 let input = "alice.margatroid";
1093
1094 let name = Name::from_dhttp_shorthand(input).expect("plain name should parse");
1095
1096 assert_eq!(name.as_full(), "alice.margatroid");
1097 assert_eq!(name.as_str().as_ptr(), input.as_ptr());
1098 }
1099
1100 #[test]
1101 fn name_from_dhttp_shorthand_reuses_owned_bytes_without_marker() {
1102 let input = Bytes::from_static(b"alice.margatroid");
1103 let input_ptr = input.as_ptr();
1104
1105 let name = Name::from_dhttp_shorthand(input).expect("plain name should parse");
1106 let bytes = name.into_bytes();
1107
1108 assert_eq!(bytes.as_ref(), b"alice.margatroid");
1109 assert_eq!(bytes.as_ptr(), input_ptr);
1110 }
1111
1112 #[test]
1113 fn name_from_dhttp_shorthand_lowercases_plain_name() {
1114 let name =
1115 Name::from_dhttp_shorthand("Alice.Margatroid").expect("mixed-case name should parse");
1116
1117 assert_eq!(name.as_full(), "alice.margatroid");
1118 }
1119
1120 #[test]
1121 fn name_from_dhttp_shorthand_expands_suffix_marker() {
1122 let name =
1123 Name::from_dhttp_shorthand("alice.margatroid~").expect("dhttp shorthand should parse");
1124
1125 assert_eq!(name.as_full(), "alice.margatroid.dhttp.net");
1126 }
1127
1128 #[test]
1129 fn name_from_dhttp_shorthand_expands_every_suffix_marker() {
1130 let name = Name::from_dhttp_shorthand("alice~bar")
1131 .expect("any-position dhttp shorthand should parse when expanded name is valid");
1132
1133 assert_eq!(name.as_full(), "alice.dhttp.netbar");
1134 }
1135
1136 #[test]
1137 fn name_from_dhttp_shorthand_rejects_bare_suffix_marker() {
1138 let error = Name::from_dhttp_shorthand("~").expect_err("bare shorthand is not a name");
1139
1140 assert!(matches!(error, InvalidName::EmptyLabel { .. }));
1141 }
1142
1143 #[test]
1144 fn name_from_dhttp_shorthand_accepts_owned_bytes() {
1145 let name = Name::from_dhttp_shorthand(Bytes::from_static(b"alice~"))
1146 .expect("owned bytes shorthand should parse");
1147
1148 assert_eq!(name.as_full(), "alice.dhttp.net");
1149 }
1150
1151 #[test]
1152 fn name_try_from_str_valid() {
1153 let n: Name = "example.com".parse().unwrap();
1154 assert_eq!(n.as_str(), "example.com");
1155 }
1156
1157 #[test]
1158 fn name_try_from_str_too_long() {
1159 let long = "a".repeat(254);
1160 let err: Result<Name, _> = long.parse();
1161 assert!(matches!(err.unwrap_err(), InvalidName::TooLong {}));
1162 }
1163
1164 #[test]
1165 fn name_try_from_str_empty() {
1166 let err: Result<Name, _> = "".parse();
1167 assert!(matches!(err.unwrap_err(), InvalidName::EmptyLabel {}));
1168 }
1169
1170 #[test]
1171 fn name_try_from_str_invalid_char() {
1172 let err: Result<Name, _> = "hello!".parse();
1173 assert!(matches!(err.unwrap_err(), InvalidName::InvalidCharacter {}));
1174 }
1175
1176 #[test]
1177 fn name_try_from_str_label_too_long() {
1178 let long_label = format!("{}.com", "a".repeat(64));
1179 let err: Result<Name, _> = long_label.parse();
1180 assert!(matches!(err.unwrap_err(), InvalidName::LabelTooLong {}));
1181 }
1182
1183 #[test]
1184 fn name_wildcard() {
1185 let n: Name = "*.example.com".parse().unwrap();
1186 assert!(n.is_wildcard());
1187
1188 let m: Name = "foo.example.com".parse().unwrap();
1189 assert!(n.matches(&m));
1190 assert!(n.matches(&n));
1191 }
1192
1193 #[test]
1194 fn name_no_wildcard_match() {
1195 let n: Name = "a.example.com".parse().unwrap();
1196 let m: Name = "b.example.com".parse().unwrap();
1197 assert!(!n.matches(&m));
1198 }
1199
1200 #[test]
1201 fn name_exact_match() {
1202 let n: Name = "foo.example.com".parse().unwrap();
1203 let m: Name = "foo.example.com".parse().unwrap();
1204 assert!(n.matches(&m));
1205 }
1206
1207 #[test]
1208 fn name_hash_borrow_consistency() {
1209 use std::collections::HashSet;
1210 let n: Name = "example.com".parse().unwrap();
1211 let mut set = HashSet::new();
1212 set.insert(n.clone());
1213 assert!(set.contains("example.com"));
1214 }
1215
1216 #[test]
1217 fn name_clone_owned() {
1218 let n: Name = "example.com".parse().unwrap();
1219 let c = n.clone();
1220 assert_eq!(n, c);
1221 }
1222
1223 #[test]
1224 fn name_to_wildcard_name() {
1225 let n: Name = "foo.example.com".parse().unwrap();
1226 let w = n.to_wildcard();
1227 assert!(w.is_wildcard());
1228 assert_eq!(w.as_str(), "*.example.com");
1229 }
1230
1231 #[test]
1232 fn name_wildcard_already() {
1233 let n: Name = "*.example.com".parse().unwrap();
1234 let w = n.to_wildcard();
1235 assert_eq!(w.as_str(), "*.example.com");
1236 }
1237
1238 #[test]
1239 fn name_serialize_deserialize() {
1240 let n: Name = "example.com".parse().unwrap();
1241 let json = serde_json::to_string(&n).unwrap();
1242 assert_eq!(json, r#""example.com""#);
1243 let d: Name<'static> = serde_json::from_str(&json).unwrap();
1244 assert_eq!(n, d);
1245 }
1246
1247 #[test]
1248 fn name_display() {
1249 let n: Name = "Example.COM".parse().unwrap();
1250 assert_eq!(format!("{n}"), "example.com");
1251 }
1252
1253 #[test]
1256 fn name_try_from_ref_str_lowercase() {
1257 let n = Name::try_from("example.com").unwrap();
1258 assert_eq!(n.as_str(), "example.com");
1259 }
1260
1261 #[test]
1262 fn name_try_from_ref_str_mixed_case() {
1263 let n = Name::try_from("Example.COM").unwrap();
1264 assert_eq!(n.as_str(), "example.com");
1265 }
1266
1267 #[test]
1268 fn name_try_from_ref_str_wildcard() {
1269 let n = Name::try_from("*.example.com").unwrap();
1270 assert!(n.is_wildcard());
1271 assert_eq!(n.as_str(), "*.example.com");
1272 }
1273
1274 #[test]
1275 fn name_try_from_ref_str_invalid() {
1276 let err = Name::try_from("!!!").unwrap_err();
1277 assert!(matches!(err, InvalidName::InvalidCharacter {}));
1278 }
1279
1280 #[test]
1281 fn name_try_from_ref_str_borrowed_variant() {
1282 let input = "example.com";
1283 let n = Name::try_from(input).unwrap();
1284 assert_eq!(n.as_str(), "example.com");
1285 }
1286
1287 #[test]
1290 fn name_try_from_ref_bytes_lowercase() {
1291 let input: &[u8] = b"example.com";
1292 let n = Name::try_from(input).unwrap();
1293 assert_eq!(n.as_str(), "example.com");
1294 }
1295
1296 #[test]
1297 fn name_try_from_ref_bytes_mixed_case() {
1298 let input: &[u8] = b"Example.COM";
1299 let n = Name::try_from(input).unwrap();
1300 assert_eq!(n.as_str(), "example.com");
1301 }
1302
1303 #[test]
1304 fn name_try_from_ref_bytes_wildcard() {
1305 let input: &[u8] = b"*.example.com";
1306 let n = Name::try_from(input).unwrap();
1307 assert!(n.is_wildcard());
1308 assert_eq!(n.as_str(), "*.example.com");
1309 }
1310
1311 #[test]
1312 fn name_try_from_ref_bytes_invalid() {
1313 let input: &[u8] = b"!!!";
1314 let err = Name::try_from(input).unwrap_err();
1315 assert!(matches!(err, InvalidName::InvalidCharacter {}));
1316 }
1317
1318 #[test]
1321 fn name_try_from_string_mixed_case() {
1322 let s = String::from("Hello.World");
1323 let n = Name::try_from(s).unwrap();
1324 assert_eq!(n.as_str(), "hello.world");
1325 }
1326
1327 #[test]
1328 fn name_try_from_string_invalid() {
1329 let s = String::from("!!!");
1330 let err = Name::try_from(s).unwrap_err();
1331 assert!(matches!(err, InvalidName::InvalidCharacter {}));
1332 }
1333
1334 #[test]
1335 fn name_try_from_string_empty() {
1336 let s = String::new();
1337 let err = Name::try_from(s).unwrap_err();
1338 assert!(matches!(err, InvalidName::EmptyLabel {}));
1339 }
1340
1341 #[test]
1344 fn name_try_from_vec_u8_lowercase() {
1345 let n = Name::try_from(b"example.com".to_vec()).unwrap();
1346 assert_eq!(n.as_str(), "example.com");
1347 }
1348
1349 #[test]
1350 fn name_try_from_vec_u8_mixed_case() {
1351 let n = Name::try_from(b"Hello.World".to_vec()).unwrap();
1352 assert_eq!(n.as_str(), "hello.world");
1353 }
1354
1355 #[test]
1356 fn name_try_from_vec_u8_invalid() {
1357 let err = Name::try_from(b"!!!".to_vec()).unwrap_err();
1358 assert!(matches!(err, InvalidName::InvalidCharacter {}));
1359 }
1360
1361 #[test]
1364 fn name_try_from_cow_borrowed_lowercase() {
1365 let cow: Cow<'_, str> = Cow::Borrowed("example.com");
1366 let n = Name::try_from(cow).unwrap();
1367 assert_eq!(n.as_str(), "example.com");
1368 }
1369
1370 #[test]
1371 fn name_try_from_cow_borrowed_mixed_case() {
1372 let cow: Cow<'_, str> = Cow::Borrowed("Example.COM");
1373 let n = Name::try_from(cow).unwrap();
1374 assert_eq!(n.as_str(), "example.com");
1375 }
1376
1377 #[test]
1378 fn name_try_from_cow_owned_lowercase() {
1379 let cow: Cow<'_, str> = Cow::Owned("example.com".to_string());
1380 let n = Name::try_from(cow).unwrap();
1381 assert_eq!(n.as_str(), "example.com");
1382 }
1383
1384 #[test]
1385 fn name_try_from_cow_owned_mixed_case() {
1386 let cow: Cow<'_, str> = Cow::Owned("Example.COM".to_string());
1387 let n = Name::try_from(cow).unwrap();
1388 assert_eq!(n.as_str(), "example.com");
1389 }
1390
1391 #[test]
1392 fn name_try_from_cow_invalid() {
1393 let cow: Cow<'_, str> = Cow::Borrowed("!!!");
1394 let err = Name::try_from(cow).unwrap_err();
1395 assert!(matches!(err, InvalidName::InvalidCharacter {}));
1396 }
1397
1398 #[test]
1399 fn name_try_from_cow_bytes_borrowed_and_owned() {
1400 let borrowed = Cow::<[u8]>::Borrowed(b"Example.COM");
1401 let owned: Cow<'_, [u8]> = Cow::Owned(b"Reimu.Pilot".to_vec());
1402
1403 let borrowed_name = Name::try_from(borrowed).unwrap();
1404 let owned_name = Name::try_from(owned).unwrap();
1405
1406 assert_eq!(borrowed_name.as_str(), "example.com");
1407 assert_eq!(owned_name.as_str(), "reimu.pilot");
1408 }
1409
1410 #[test]
1413 fn dhttp_name_suffix_is_dhttp_net() {
1414 assert_eq!(DhttpName::SUFFIX, ".dhttp.net");
1415 }
1416
1417 #[test]
1418 fn dhttp_name_parse_full() {
1419 let dn = "hello.dhttp.net".parse::<DhttpName>().unwrap();
1420 assert_eq!(dn.as_full(), "hello.dhttp.net");
1421 assert_eq!(dn.as_partial(), "hello");
1422 }
1423
1424 #[test]
1425 fn dhttp_name_parse_partial_multi_label() {
1426 let dn = "reimu.pilot".parse::<DhttpName>().unwrap();
1427 assert_eq!(dn.as_full(), "reimu.pilot.dhttp.net");
1428 assert_eq!(dn.as_partial(), "reimu.pilot");
1429 }
1430
1431 #[test]
1432 fn dhttp_name_parse_partial_single_label_rejected() {
1433 let name = "hello".parse::<DhttpName>().unwrap();
1434
1435 assert_eq!(name.as_full(), "hello.dhttp.net");
1436 }
1437
1438 #[test]
1439 fn dhttp_name_serialize() {
1440 let dn = "reimu.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1441 let json = serde_json::to_string(&dn).unwrap();
1442 assert_eq!(json, "\"reimu.pilot\"");
1443 }
1444
1445 #[test]
1446 fn dhttp_name_deserialize_from_partial() {
1447 let dn: DhttpName<'static> = serde_json::from_str("\"reimu.pilot\"").unwrap();
1448 assert_eq!(dn.as_full(), "reimu.pilot.dhttp.net");
1449 }
1450
1451 #[test]
1452 fn dhttp_name_deserialize_from_full() {
1453 let dn: DhttpName<'static> = serde_json::from_str("\"reimu.pilot.dhttp.net\"").unwrap();
1454 assert_eq!(dn.as_full(), "reimu.pilot.dhttp.net");
1455 }
1456
1457 #[test]
1458 fn dhttp_name_deserialize_rejects_invalid() {
1459 let result: Result<DhttpName<'static>, _> = serde_json::from_str("\"!!!\"");
1460 assert!(result.is_err());
1461 }
1462
1463 #[test]
1464 fn dhttp_name_hash_consistent_with_name() {
1465 use std::hash::{DefaultHasher, Hasher};
1466 let dn = "reimu.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1467 let n = Name::try_from_static(b"reimu.pilot.dhttp.net").unwrap();
1468 let hash_dn = {
1469 let mut h = DefaultHasher::new();
1470 dn.hash(&mut h);
1471 h.finish()
1472 };
1473 let hash_n = {
1474 let mut h = DefaultHasher::new();
1475 n.hash(&mut h);
1476 h.finish()
1477 };
1478 assert_eq!(hash_dn, hash_n);
1479 }
1480
1481 #[test]
1482 fn dhttp_name_eq() {
1483 let a = "reimu.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1484 let b = "reimu.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1485 let c = "other.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1486 assert_eq!(a, b);
1487 assert_ne!(a, c);
1488 }
1489
1490 #[test]
1491 fn dhttp_name_to_owned_and_clone() {
1492 let dn = "reimu.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1493 let owned = dn.to_owned();
1494 assert_eq!(owned.as_full(), "reimu.pilot.dhttp.net");
1495 let cloned = owned.clone();
1496 assert_eq!(cloned.as_full(), "reimu.pilot.dhttp.net");
1497 }
1498
1499 #[test]
1500 fn dhttp_name_into_owned() {
1501 let dn = "reimu.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1502 let owned = dn.into_owned();
1503 assert_eq!(owned.as_full(), "reimu.pilot.dhttp.net");
1504 }
1505
1506 #[test]
1507 fn dhttp_name_to_wildcard_replaces_first_label() {
1508 let dn = "reimu.pilot.dhttp.net".parse::<DhttpName>().unwrap();
1509
1510 let wildcard = dn.to_wildcard();
1511
1512 assert_eq!(wildcard.as_full(), "*.pilot.dhttp.net");
1513 }
1514
1515 #[test]
1516 fn dhttp_name_from_str_trait() {
1517 let dn: DhttpName = "reimu.pilot.dhttp.net".parse().unwrap();
1518 assert_eq!(dn.as_full(), "reimu.pilot.dhttp.net");
1519 }
1520
1521 #[test]
1522 fn dhttp_name_from_str_trait_rejects_invalid() {
1523 let result: Result<DhttpName, _> = "!!!".parse();
1524 assert!(result.is_err());
1525 }
1526
1527 #[test]
1528 fn dhttp_name_legacy_borrow_method() {
1529 let dn = "reimu.pilot".parse::<DhttpName>().unwrap();
1530 let borrowed = dn.borrow();
1531 assert_eq!(borrowed.as_full(), dn.as_full());
1532 }
1533
1534 #[test]
1535 fn dhttp_name_legacy_validate() {
1536 DhttpName::validate(b"reimu.pilot.dhttp.net").unwrap();
1537 assert!(DhttpName::validate(b"reimu.pilot").is_err());
1538 }
1539
1540 #[test]
1541 fn dhttp_name_try_from_str_expands_partial_name() {
1542 let name = DhttpName::try_from("reimu.pilot").unwrap();
1543 assert_eq!(name.as_full(), "reimu.pilot.dhttp.net");
1544 }
1545
1546 #[test]
1547 fn dhttp_name_try_from_string_expands_tilde_name() {
1548 let name = DhttpName::try_from(String::from("reimu.pilot~")).unwrap();
1549 assert_eq!(name.as_full(), "reimu.pilot.dhttp.net");
1550 }
1551
1552 #[test]
1553 fn dhttp_name_try_from_bytes_and_cow_bytes_append_suffix() {
1554 let from_bytes = DhttpName::try_from(Bytes::from_static(b"Reimu.Pilot")).unwrap();
1555 let from_cow: DhttpName<'_> =
1556 DhttpName::try_from(Cow::<[u8]>::Borrowed(b"Device")).unwrap();
1557
1558 assert_eq!(from_bytes.as_full(), "reimu.pilot.dhttp.net");
1559 assert_eq!(from_cow.as_full(), "device.dhttp.net");
1560 }
1561
1562 #[test]
1563 fn dhttp_name_try_from_static_bytes_appends_suffix() {
1564 let name = DhttpName::try_from_static(b"Device").unwrap();
1565
1566 assert_eq!(name.as_full(), "device.dhttp.net");
1567 }
1568
1569 #[test]
1570 fn dhttp_name_try_from_name_accepts_full_name_without_reparsing_string() {
1571 let name = Name::try_from("reimu.pilot.dhttp.net").unwrap();
1572
1573 let dhttp_name = DhttpName::try_from(name).unwrap();
1574
1575 assert_eq!(dhttp_name.as_full(), "reimu.pilot.dhttp.net");
1576 }
1577
1578 #[test]
1579 fn dhttp_name_try_from_name_rejects_missing_suffix() {
1580 let name = Name::try_from("example.com").unwrap();
1581
1582 let error = DhttpName::try_from(name).unwrap_err();
1583
1584 assert!(matches!(
1585 error,
1586 InvalidDhttpName::InvalidName {
1587 source: InvalidName::MissingSuffix { .. }
1588 }
1589 ));
1590 }
1591
1592 #[test]
1593 fn expand_uri_replaces_bare_tilde_with_self_name() {
1594 let name = "reimu.pilot".parse::<DhttpName>().unwrap();
1595 let uri = "https://~/api?q=1".parse().unwrap();
1596
1597 let expanded = name.expand_uri(uri).unwrap();
1598
1599 assert_eq!(
1600 expanded.to_string(),
1601 "https://reimu.pilot.dhttp.net/api?q=1"
1602 );
1603 }
1604
1605 #[test]
1606 fn expand_uri_expands_tilde_suffix_and_preserves_userinfo_port() {
1607 let name = "self.host".parse::<DhttpName>().unwrap();
1608 let uri = "https://alice@reimu.pilot~:443/api".parse().unwrap();
1609
1610 let expanded = name.expand_uri(uri).unwrap();
1611
1612 assert_eq!(
1613 expanded.to_string(),
1614 "https://alice@reimu.pilot.dhttp.net:443/api"
1615 );
1616 }
1617
1618 #[test]
1619 fn expand_authority_expands_tilde_suffix_and_preserves_userinfo_port() {
1620 let name = "self.host".parse::<DhttpName>().unwrap();
1621 let authority = "alice@reimu.pilot~:443".parse().unwrap();
1622
1623 let expanded = DhttpName::expand_authority_with_base(Some(&name), authority).unwrap();
1624
1625 assert_eq!(expanded.as_str(), "alice@reimu.pilot.dhttp.net:443");
1626 }
1627
1628 #[test]
1629 fn expand_authority_expands_any_position_tilde_suffix() {
1630 let authority = "alice@a~:443".parse().unwrap();
1631
1632 let expanded = DhttpName::expand_authority_with_base(None, authority).unwrap();
1633
1634 assert_eq!(expanded.as_str(), "alice@a.dhttp.net:443");
1635 }
1636
1637 #[test]
1638 fn expand_authority_expands_middle_tilde_suffix() {
1639 let authority = "a~b".parse().unwrap();
1640
1641 let expanded = DhttpName::expand_authority_with_base(None, authority).unwrap();
1642
1643 assert_eq!(expanded.as_str(), "a.dhttp.netb");
1644 }
1645
1646 #[test]
1647 fn expand_authority_keeps_bare_tilde_as_self_with_base() {
1648 let base = "reimu.pilot".parse::<DhttpName>().unwrap();
1649 let authority = "~".parse().unwrap();
1650
1651 let expanded = DhttpName::expand_authority_with_base(Some(&base), authority).unwrap();
1652
1653 assert_eq!(expanded.as_str(), "reimu.pilot.dhttp.net");
1654 }
1655
1656 #[test]
1657 fn expand_authority_rejects_invalid_shorthand_host() {
1658 let authority = "~bar".parse().unwrap();
1659
1660 let error = DhttpName::expand_authority_with_base(None, authority).unwrap_err();
1661
1662 assert!(matches!(error, ExpandAuthorityError::InvalidName { .. }));
1663 }
1664
1665 #[test]
1666 fn expand_authority_canonicalizes_mixed_case_host_only_dhttp_name() {
1667 let authority = "Reimu.Pilot.Dhttp.Net".parse().unwrap();
1668
1669 let expanded = DhttpName::expand_authority_with_base(None, authority).unwrap();
1670
1671 assert_eq!(expanded.as_str(), "reimu.pilot.dhttp.net");
1672 }
1673
1674 #[test]
1675 fn expand_authority_canonicalizes_mixed_case_decorated_dhttp_name() {
1676 let authority = "alice@Reimu.Pilot.Dhttp.Net:443".parse().unwrap();
1677
1678 let expanded = DhttpName::expand_authority_with_base(None, authority).unwrap();
1679
1680 assert_eq!(expanded.as_str(), "alice@reimu.pilot.dhttp.net:443");
1681 }
1682
1683 #[test]
1684 fn expand_authority_host_only_partial_uses_canonical_name() {
1685 let authority = "Reimu.Pilot~".parse().unwrap();
1686
1687 let expanded = DhttpName::expand_authority_with_base(None, authority).unwrap();
1688
1689 assert_eq!(expanded.as_str(), "reimu.pilot.dhttp.net");
1690 }
1691
1692 #[test]
1693 fn expand_authority_with_base_requires_base_name_for_bare_tilde() {
1694 let authority = "~".parse().unwrap();
1695
1696 let error = DhttpName::expand_authority_with_base(None, authority).unwrap_err();
1697
1698 assert!(matches!(error, ExpandAuthorityError::MissingBaseName));
1699 }
1700
1701 #[test]
1702 fn expand_uri_leaves_plain_host_unchanged() {
1703 let name = "self.host".parse::<DhttpName>().unwrap();
1704 let uri: http::Uri = "https://example.com/api".parse().unwrap();
1705
1706 let expanded = name.expand_uri(uri.clone()).unwrap();
1707
1708 assert_eq!(expanded, uri);
1709 }
1710
1711 #[test]
1712 fn expand_uri_rejects_invalid_expanded_name() {
1713 let name = "self.host".parse::<DhttpName>().unwrap();
1714 let uri = "https://123~/api".parse().unwrap();
1715
1716 let error = name.expand_uri(uri).unwrap_err();
1717
1718 assert!(matches!(
1719 error,
1720 ExpandUriError::Authority {
1721 source: ExpandAuthorityError::InvalidName { .. }
1722 }
1723 ));
1724 }
1725
1726 #[test]
1727 fn expand_uri_with_base_expands_partial_without_base_name() {
1728 let uri = "https://reimu.pilot~/api".parse().unwrap();
1729
1730 let expanded = DhttpName::expand_uri_with_base(None, uri).unwrap();
1731
1732 assert_eq!(expanded.to_string(), "https://reimu.pilot.dhttp.net/api");
1733 }
1734
1735 #[test]
1736 fn expand_uri_with_base_requires_base_name_for_bare_tilde() {
1737 let uri = "https://~/api".parse().unwrap();
1738
1739 let error = DhttpName::expand_uri_with_base(None, uri).unwrap_err();
1740
1741 assert!(matches!(
1742 error,
1743 ExpandUriError::Authority {
1744 source: ExpandAuthorityError::MissingBaseName
1745 }
1746 ));
1747 }
1748}