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