Skip to main content

dhttp_identity/
name.rs

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// ============================================================================
14// BytesStr — private string backed by Bytes for O(1) cloning
15// ============================================================================
16
17/// Internal string type backed by Bytes for O(1) cloning.
18/// Never exposed publicly — used by [`Name::Owned`] variant.
19#[derive(Clone, Debug)]
20struct BytesStr(Bytes);
21
22impl Deref for BytesStr {
23    type Target = str;
24
25    #[inline]
26    fn deref(&self) -> &str {
27        // SAFETY: constructed only from valid UTF-8 (validated ASCII lowercase)
28        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    /// Validate DNS name rules without checking for any suffix.
229    ///
230    /// Rules enforced:
231    /// - Total length ≤ 253 bytes
232    /// - Each label ≤ 63 characters
233    /// - No empty labels (consecutive dots, leading/trailing dot, label
234    ///   starting/ending with hyphen)
235    /// - No purely numeric labels
236    /// - Only ASCII letters, digits, hyphens, underscores, dots, and leading `*`
237    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                // SAFETY: DnsName::validate accepts only ASCII DNS-name bytes,
304                // which are valid UTF-8.
305                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// ============================================================================
320// InvalidName — DNS name validation errors
321// ============================================================================
322
323#[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// ============================================================================
338// Name<'a> — DNS name, always lowercase
339// ============================================================================
340
341/// A DNS name stored as either a borrowed `&str` or an owned [`BytesStr`].
342///
343/// All names are normalised to ASCII lowercase. The type implements
344/// [`Borrow<str>`] so that it can be used as a key in `HashMap` / `DashMap`
345/// for O(1) lookups via `&str`.
346#[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    /// Parse a DNS name while treating each `~` byte as shorthand for
354    /// [`DhttpName::SUFFIX`].
355    ///
356    /// Unlike [`DhttpName::try_from`], this does not implicitly append the
357    /// DHTTP suffix when `~` is absent.
358    #[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    /// Return the name as a `&str`.
367    #[inline]
368    pub fn as_str(&self) -> &str {
369        self.0.as_ref()
370    }
371
372    /// Return the complete DNS name.
373    #[inline]
374    pub fn as_full(&self) -> &str {
375        self.as_str()
376    }
377
378    /// Clone to an owned [`Name<'static>`].
379    #[inline]
380    pub fn to_owned(&self) -> Name<'static> {
381        Name(DnsName(self.0.0.clone().into_owned()))
382    }
383
384    /// Consume and return an owned [`Name<'static>`].
385    #[inline]
386    pub fn into_owned(self) -> Name<'static> {
387        Name(DnsName(self.0.0.into_owned()))
388    }
389
390    /// Consume and return this name as bytes.
391    ///
392    /// Owned names reuse the existing [`Bytes`] allocation. Borrowed names are
393    /// copied because the returned bytes must own their storage.
394    #[inline]
395    pub fn into_bytes(self) -> Bytes {
396        self.0.0.into_bytes()
397    }
398
399    /// Replace the first label with `*` to create a wildcard name.
400    ///
401    /// If the name is already a wildcard, returns itself as owned.
402    /// If the name is a single label (no dot), returns itself unchanged.
403    #[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        // Single label — cannot create wildcard, return as-is.
413        self.into_owned()
414    }
415
416    /// Whether the first label is `*`.
417    #[inline]
418    pub fn is_wildcard(&self) -> bool {
419        self.as_str().starts_with('*')
420    }
421
422    /// Exact match or wildcard suffix match.
423    ///
424    /// If `self` is a wildcard name (e.g. `*.example.com`), matches any name
425    /// whose suffix after the first label equals the wildcard's suffix.
426    /// Otherwise, performs exact string comparison.
427    #[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..]; // skip `*.`
434        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
447// --- Trait implementations for Name ---
448
449impl 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
518// --- Borrowed-reference conversions (Ref path) ---
519
520/// `TryFrom<&str>` — zero-copy when the validated name is already lowercase.
521impl<'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
530/// `TryFrom<&[u8]>` — zero-copy when the validated name is already lowercase.
531impl<'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
549// --- Owned conversions (always `Name<'static>`) ---
550
551/// `FromStr` — always returns `Name<'static>`.
552impl 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
588/// `TryFrom<Cow<str>>` — borrows borrowed input when possible and reuses owned
589/// input storage.
590impl<'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// ============================================================================
615// InvalidDhttpName — DhttpName parse errors
616// ============================================================================
617
618#[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// ============================================================================
648// DhttpName<'a> — Name with mandatory `.dhttp.net` suffix
649// ============================================================================
650
651/// A [`Name`] guaranteed to end with `.dhttp.net`.
652///
653/// Created via [`FromStr`] or [`TryFrom`], which handle `~` shorthand expansion
654/// and append the suffix when missing.
655#[derive(Clone, Debug)]
656pub struct DhttpName<'a>(Name<'a>);
657
658impl DhttpName<'_> {
659    pub const SUFFIX: &'static str = ".dhttp.net";
660
661    /// Validate DHttp name rules, including the mandatory suffix.
662    #[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    /// Consume and return the inner [`Name`].
682    #[inline]
683    pub fn into_name(self) -> Name<'static> {
684        self.0.into_owned()
685    }
686
687    /// Return the name without the `.dhttp.net` suffix.
688    ///
689    /// # Panics
690    ///
691    /// Panics in debug if the name does not end with the suffix (should never
692    /// happen — the constructor guarantees it).
693    #[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    /// Return the full name including the `.dhttp.net` suffix.
700    #[inline]
701    pub fn as_full(&self) -> &str {
702        self.0.as_str()
703    }
704
705    /// Return a reference to the inner [`Name`].
706    #[inline]
707    pub fn as_name(&self) -> &Name<'_> {
708        &self.0
709    }
710
711    /// Return a borrowed DHttp name.
712    #[inline]
713    pub fn borrow(&self) -> DhttpName<'_> {
714        DhttpName(Name(DnsName(CowBytesStr::Borrowed(self.0.as_str()))))
715    }
716
717    /// Replace the first label with `*` to create a wildcard DHttp name.
718    #[inline]
719    pub fn to_wildcard(self) -> DhttpName<'static> {
720        DhttpName(self.0.to_wildcard())
721    }
722
723    /// Expand DHttp shorthand in the authority of `uri`.
724    ///
725    /// The bare host `~` expands to this name. Other hosts containing `~`
726    /// expand each marker to the DHttp suffix. Ordinary host names pass through
727    /// unchanged.
728    #[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    /// Expand DHttp shorthand in `authority` with an optional base name.
734    ///
735    /// The bare host `~` expands to `base` and fails when `base` is absent. Other
736    /// hosts containing `~` expand each marker to the DHttp suffix and do not
737    /// require `base`. Ordinary host names pass through unchanged.
738    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    /// Expand DHttp shorthand in the authority of `uri` with an optional base name.
796    ///
797    /// The bare host `~` expands to `base` and fails when `base` is absent. Other
798    /// hosts containing `~` expand each marker to the DHttp suffix and do not
799    /// require `base`. Ordinary host names pass through unchanged.
800    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
817// --- Trait implementations for DhttpName ---
818
819impl<'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
828/// Formats the name without the `.dhttp.net` suffix.
829///
830/// `Display` and [`Serialize`] both output the partial name (e.g. `reimu.pilot`),
831/// while [`Deserialize`] and [`FromStr`] accept both partial and full forms.
832/// Use [`DhttpName::as_full`] to obtain the complete name including the suffix.
833impl 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    /// Clone to an owned [`DhttpName<'static>`].
1020    #[inline]
1021    pub fn to_owned(&self) -> DhttpName<'static> {
1022        DhttpName(self.0.to_owned())
1023    }
1024
1025    /// Consume and return an owned [`DhttpName<'static>`].
1026    #[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    // --- TryFrom<&str> tests ---
1254
1255    #[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    // --- TryFrom<&[u8]> tests ---
1288
1289    #[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    // --- TryFrom<String> tests ---
1319
1320    #[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    // --- TryFrom<Vec<u8>> tests ---
1342
1343    #[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    // --- TryFrom<Cow<str>> tests ---
1362
1363    #[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    // --- DhttpName tests ---
1411
1412    #[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}