//! Splitting one atom into its kind, its positional part and its //! parameters, as the permission spec's "Scope String Syntax" writes them. use percent_encoding::percent_decode_str; use crate::{ScopeParseError, ScopeWarning}; /// Splits an atom into its kind and everything after it. /// /// An atom names its kind before the first `:`, or before the first `?` when /// it has no positional value at all — `blob?accept=image/*` is the `blob` /// kind with no positional half, which is how the spec writes a scope /// carrying several values of one parameter. pub(crate) fn split_kind(atom: &str) -> Option<(&str, &str)> { match (atom.find(':'), atom.find('?')) { (Some(colon), query) if query.is_none_or(|q| colon < q) => { Some((&atom[..colon], &atom[colon + 1..])) } (_, Some(query)) => Some(atom.split_at(query)), _ => None, } } /// One atom's positional part and `name=value` parameters, every value /// percent-decoded, and the warnings reading them has raised so far. /// /// A kind takes the parameters it reads, and [`Syntax::finish`] ignores /// whatever is left. An empty positional part, as in `identity:?attr=handle`, /// gives no value. pub(crate) struct Syntax<'a> { positional: Option, params: Vec<(&'a str, String)>, warnings: Vec, } impl<'a> Syntax<'a> { /// Splits `atom`, whose kind [`split_kind`] found to be `kind`. pub(crate) fn split(atom: &'a str, kind: &str) -> Result { let after = &atom[kind.len()..]; let (positional, query) = match after.strip_prefix(':') { Some(rest) => match rest.split_once('?') { Some((positional, query)) => (positional, query), None => (rest, ""), }, None => ("", after.strip_prefix('?').unwrap_or_default()), }; let params = query .split('&') .filter(|pair| !pair.is_empty()) .map(|pair| { pair.split_once('=') .map(|(name, value)| (name, decode(value))) .ok_or_else(|| { ScopeParseError::MalformedQuery(pair.to_owned(), atom.to_owned()) }) }) .collect::>()?; Ok(Self { positional: (!positional.is_empty()).then(|| decode(positional)), params, warnings: Vec::new(), }) } /// Every value of the list parameter `name`: the positional part when /// `name` is its kind's positional parameter, then each `name=` in order. pub(crate) fn list(&mut self, name: &str, positional: bool) -> Vec { let mut values: Vec = match positional { true => self.positional.take().into_iter().collect(), false => Vec::new(), }; let named = self.take(name); if !values.is_empty() && !named.is_empty() { self.warn(ScopeWarning::RepeatedList(name.to_owned())); } values.extend(named); values } /// The parameter `name`, which takes one value: the positional part when /// `name` is its kind's positional parameter, or its `name=`. pub(crate) fn single(&mut self, name: &str, positional: bool) -> Option { let mut values = match positional { true => self.positional.take().into_iter().collect(), false => Vec::new(), }; values.extend(self.take(name)); if values.len() > 1 { self.warn(ScopeWarning::RepeatedValue(name.to_owned())); } values.into_iter().next() } /// Records `warning`, once. pub(crate) fn warn(&mut self, warning: ScopeWarning) { if !self.warnings.contains(&warning) { self.warnings.push(warning); } } /// Ends the read. Each parameter the kind did not take is ignored, with a /// warning naming it. pub(crate) fn finish(mut self) -> Vec { for (name, _) in std::mem::take(&mut self.params) { self.warn(ScopeWarning::UnknownParameter(name.to_owned())); } self.warnings } /// Takes every `name=` value out of the parameters, in order. fn take(&mut self, name: &str) -> Vec { let (taken, kept) = std::mem::take(&mut self.params) .into_iter() .partition(|(key, _)| *key == name); self.params = kept; taken.into_iter().map(|(_, value)| value).collect() } } /// `value` percent-decoded. A byte sequence that is not UTF-8 becomes U+FFFD, /// which no NSID, MIME type or DID holds. fn decode(value: &str) -> String { percent_decode_str(value).decode_utf8_lossy().into_owned() }