//! The NSID and MIME patterns a scope's positional value may be. use std::fmt; use didbot_lexicon::nsid_syntax; use crate::ScopeParseError; /// An NSID pattern: exact, a recursive prefix wildcard, or the bare `*`. /// /// See the module doc for why prefix wildcards are read as recursive. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum NsidPattern { /// `*` — every collection or method. Any, /// `some.prefix.*` — stored without the trailing `.*`. Prefix(String), /// One exact NSID. Exact(String), } impl NsidPattern { pub(crate) fn parse(s: &str) -> Result { if s == "*" { return Ok(NsidPattern::Any); } if let Some(prefix) = s.strip_suffix(".*") { if prefix.is_empty() || !valid_nsid_prefix(prefix) { return Err(ScopeParseError::MalformedNsid(s.to_owned())); } return Ok(NsidPattern::Prefix(prefix.to_owned())); } if nsid_syntax::validate(s).is_err() { return Err(ScopeParseError::MalformedNsid(s.to_owned())); } Ok(NsidPattern::Exact(s.to_owned())) } /// Does `self` admit every collection/method that `other` admits? pub(crate) fn contains(&self, other: &NsidPattern) -> bool { match (self, other) { (NsidPattern::Any, _) => true, (_, NsidPattern::Any) => false, (NsidPattern::Exact(a), NsidPattern::Exact(b)) => a == b, (NsidPattern::Exact(_), NsidPattern::Prefix(_)) => false, (NsidPattern::Prefix(p), NsidPattern::Exact(nsid)) => under_prefix(p, nsid), (NsidPattern::Prefix(p), NsidPattern::Prefix(q)) => q == p || under_prefix(p, q), } } /// The most specific pattern both admit, if their admitted sets overlap /// at all. pub(crate) fn intersect(&self, other: &NsidPattern) -> Option { if self.contains(other) { return Some(other.clone()); } if other.contains(self) { return Some(self.clone()); } None } } pub(crate) fn under_prefix(prefix: &str, candidate: &str) -> bool { candidate == prefix || candidate .strip_prefix(prefix) .is_some_and(|rest| rest.starts_with('.')) } /// Is `prefix` a syntactically valid recursive-prefix wildcard's prefix? /// /// A prefix names authority segments only — it has no name segment of its /// own, so it is never itself a complete NSID (`didbot_lexicon::nsid_syntax` /// requires one). Pairing it with a synthetic, always-valid name segment /// before handing it to the real validator exercises every rule about the /// authority segments themselves (length, character set, hyphen placement, a /// non-numeric top-level domain) without requiring the prefix to already be /// a complete NSID. fn valid_nsid_prefix(prefix: &str) -> bool { nsid_syntax::validate(&format!("{prefix}.x")).is_ok() } impl fmt::Display for NsidPattern { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { NsidPattern::Any => write!(f, "*"), NsidPattern::Prefix(p) => write!(f, "{p}.*"), NsidPattern::Exact(nsid) => write!(f, "{nsid}"), } } } /// A MIME pattern: `type/subtype`, with either half (or both) as `*`. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct MimePattern { ty: MimePart, subtype: MimePart, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] enum MimePart { Any, Exact(String), } impl MimePattern { pub(crate) fn parse(s: &str) -> Result { let (ty, subtype) = s .split_once('/') .ok_or_else(|| ScopeParseError::MalformedMime(s.to_owned()))?; if ty.is_empty() || subtype.is_empty() { return Err(ScopeParseError::MalformedMime(s.to_owned())); } let part = |p: &str| { if p == "*" { MimePart::Any } else { MimePart::Exact(p.to_owned()) } }; Ok(MimePattern { ty: part(ty), subtype: part(subtype), }) } pub(crate) fn contains(&self, other: &MimePattern) -> bool { fn part_contains(a: &MimePart, b: &MimePart) -> bool { match (a, b) { (MimePart::Any, _) => true, (_, MimePart::Any) => false, (MimePart::Exact(a), MimePart::Exact(b)) => a == b, } } part_contains(&self.ty, &other.ty) && part_contains(&self.subtype, &other.subtype) } pub(crate) fn intersect(&self, other: &MimePattern) -> Option { if self.contains(other) { return Some(other.clone()); } if other.contains(self) { return Some(self.clone()); } None } } impl fmt::Display for MimePattern { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let part = |p: &MimePart| match p { MimePart::Any => "*".to_owned(), MimePart::Exact(s) => s.clone(), }; write!(f, "{}/{}", part(&self.ty), part(&self.subtype)) } }