Skip to main content

didbot_scope/
pattern.rs

1//! The NSID and MIME patterns a scope's positional value may be.
2
3use std::fmt;
4
5use didbot_lexicon::nsid_syntax;
6
7use crate::ScopeParseError;
8
9/// An NSID pattern: exact, a recursive prefix wildcard, or the bare `*`.
10///
11/// See the module doc for why prefix wildcards are read as recursive.
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub enum NsidPattern {
14    /// `*` — every collection or method.
15    Any,
16    /// `some.prefix.*` — stored without the trailing `.*`.
17    Prefix(String),
18    /// One exact NSID.
19    Exact(String),
20}
21
22impl NsidPattern {
23    pub(crate) fn parse(s: &str) -> Result<Self, ScopeParseError> {
24        if s == "*" {
25            return Ok(NsidPattern::Any);
26        }
27        if let Some(prefix) = s.strip_suffix(".*") {
28            if prefix.is_empty() || !valid_nsid_prefix(prefix) {
29                return Err(ScopeParseError::MalformedNsid(s.to_owned()));
30            }
31            return Ok(NsidPattern::Prefix(prefix.to_owned()));
32        }
33        if nsid_syntax::validate(s).is_err() {
34            return Err(ScopeParseError::MalformedNsid(s.to_owned()));
35        }
36        Ok(NsidPattern::Exact(s.to_owned()))
37    }
38
39    /// Does `self` admit every collection/method that `other` admits?
40    pub(crate) fn contains(&self, other: &NsidPattern) -> bool {
41        match (self, other) {
42            (NsidPattern::Any, _) => true,
43            (_, NsidPattern::Any) => false,
44            (NsidPattern::Exact(a), NsidPattern::Exact(b)) => a == b,
45            (NsidPattern::Exact(_), NsidPattern::Prefix(_)) => false,
46            (NsidPattern::Prefix(p), NsidPattern::Exact(nsid)) => under_prefix(p, nsid),
47            (NsidPattern::Prefix(p), NsidPattern::Prefix(q)) => q == p || under_prefix(p, q),
48        }
49    }
50
51    /// The most specific pattern both admit, if their admitted sets overlap
52    /// at all.
53    pub(crate) fn intersect(&self, other: &NsidPattern) -> Option<NsidPattern> {
54        if self.contains(other) {
55            return Some(other.clone());
56        }
57        if other.contains(self) {
58            return Some(self.clone());
59        }
60        None
61    }
62}
63
64pub(crate) fn under_prefix(prefix: &str, candidate: &str) -> bool {
65    candidate == prefix
66        || candidate
67            .strip_prefix(prefix)
68            .is_some_and(|rest| rest.starts_with('.'))
69}
70
71/// Is `prefix` a syntactically valid recursive-prefix wildcard's prefix?
72///
73/// A prefix names authority segments only — it has no name segment of its
74/// own, so it is never itself a complete NSID (`didbot_lexicon::nsid_syntax`
75/// requires one). Pairing it with a synthetic, always-valid name segment
76/// before handing it to the real validator exercises every rule about the
77/// authority segments themselves (length, character set, hyphen placement, a
78/// non-numeric top-level domain) without requiring the prefix to already be
79/// a complete NSID.
80fn valid_nsid_prefix(prefix: &str) -> bool {
81    nsid_syntax::validate(&format!("{prefix}.x")).is_ok()
82}
83
84impl fmt::Display for NsidPattern {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            NsidPattern::Any => write!(f, "*"),
88            NsidPattern::Prefix(p) => write!(f, "{p}.*"),
89            NsidPattern::Exact(nsid) => write!(f, "{nsid}"),
90        }
91    }
92}
93
94/// A MIME pattern: `type/subtype`, with either half (or both) as `*`.
95#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
96pub struct MimePattern {
97    ty: MimePart,
98    subtype: MimePart,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
102enum MimePart {
103    Any,
104    Exact(String),
105}
106
107impl MimePattern {
108    pub(crate) fn parse(s: &str) -> Result<Self, ScopeParseError> {
109        let (ty, subtype) = s
110            .split_once('/')
111            .ok_or_else(|| ScopeParseError::MalformedMime(s.to_owned()))?;
112        if ty.is_empty() || subtype.is_empty() {
113            return Err(ScopeParseError::MalformedMime(s.to_owned()));
114        }
115        let part = |p: &str| {
116            if p == "*" {
117                MimePart::Any
118            } else {
119                MimePart::Exact(p.to_owned())
120            }
121        };
122        Ok(MimePattern {
123            ty: part(ty),
124            subtype: part(subtype),
125        })
126    }
127
128    pub(crate) fn contains(&self, other: &MimePattern) -> bool {
129        fn part_contains(a: &MimePart, b: &MimePart) -> bool {
130            match (a, b) {
131                (MimePart::Any, _) => true,
132                (_, MimePart::Any) => false,
133                (MimePart::Exact(a), MimePart::Exact(b)) => a == b,
134            }
135        }
136        part_contains(&self.ty, &other.ty) && part_contains(&self.subtype, &other.subtype)
137    }
138
139    pub(crate) fn intersect(&self, other: &MimePattern) -> Option<MimePattern> {
140        if self.contains(other) {
141            return Some(other.clone());
142        }
143        if other.contains(self) {
144            return Some(self.clone());
145        }
146        None
147    }
148}
149
150impl fmt::Display for MimePattern {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        let part = |p: &MimePart| match p {
153            MimePart::Any => "*".to_owned(),
154            MimePart::Exact(s) => s.clone(),
155        };
156        write!(f, "{}/{}", part(&self.ty), part(&self.subtype))
157    }
158}