Skip to main content

didbot_scope/
read.rs

1//! Reading one atom: the scopes it names, and the warnings its form carries.
2
3use std::collections::BTreeSet;
4
5use crate::include::is_service_reference;
6use crate::parse::{split_kind, Syntax};
7use crate::scope::ANY_AUD;
8use crate::{
9    Action, ActionSet, MimePattern, NsidPattern, Scope, ScopeParseError, ScopeWarning, Transition,
10    MAX_ATOM_BYTES,
11};
12
13/// One atom, read.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Read {
16    /// The scopes the atom names: one, or one for each collection or method
17    /// it lists.
18    pub scopes: Vec<Scope>,
19    /// How the atom's form departs from the permission spec's. Empty for a
20    /// spec form.
21    pub warnings: Vec<ScopeWarning>,
22}
23
24/// Reads one atom, such as `repo:app.bsky.feed.post?action=create`.
25pub(crate) fn read(atom: &str) -> Result<Read, ScopeParseError> {
26    if atom.is_empty() {
27        return Err(ScopeParseError::Empty);
28    }
29    if atom.len() > MAX_ATOM_BYTES {
30        return Err(ScopeParseError::AtomTooLong(atom.len()));
31    }
32    if let Some(c) = atom
33        .chars()
34        .find(|c| !matches!(c, '\x21' | '\x23'..='\x5B' | '\x5D'..='\x7E'))
35    {
36        return Err(ScopeParseError::ForbiddenCharacter(c));
37    }
38    if atom == "atproto" {
39        return Ok(Read {
40            scopes: vec![Scope::Atproto],
41            warnings: Vec::new(),
42        });
43    }
44    let Some((kind, rest)) = split_kind(atom) else {
45        return Err(ScopeParseError::UnknownKind(atom.to_owned()));
46    };
47    let reader: fn(&str, &mut Syntax<'_>) -> Result<Vec<Scope>, ScopeParseError> = match kind {
48        "transition" => {
49            return Ok(Read {
50                scopes: vec![Scope::Transition(Transition::parse(rest)?)],
51                warnings: Vec::new(),
52            })
53        }
54        "repo" => repo,
55        "rpc" => rpc,
56        "blob" => |atom, syntax| Ok(vec![blob(atom, syntax)?]),
57        "include" => |atom, syntax| Ok(vec![include(atom, syntax)?]),
58        "account" => |atom, syntax| Ok(vec![crate::account::read(atom, syntax)?]),
59        "identity" => |atom, syntax| Ok(vec![crate::identity::read(atom, syntax)?]),
60        other => return Err(ScopeParseError::UnknownKind(other.to_owned())),
61    };
62    let mut syntax = Syntax::split(atom, kind)?;
63    let scopes = reader(atom, &mut syntax)?;
64    Ok(Read {
65        scopes,
66        warnings: syntax.finish(),
67    })
68}
69
70/// `repo:`: one scope for each collection, each with the atom's actions.
71fn repo(atom: &str, syntax: &mut Syntax<'_>) -> Result<Vec<Scope>, ScopeParseError> {
72    let collections = syntax.list("collection", true);
73    if collections.is_empty() {
74        return Err(ScopeParseError::MissingValue(atom.to_owned()));
75    }
76    let actions = actions(syntax)?;
77    collections
78        .iter()
79        .map(|collection| {
80            Ok(Scope::Repo {
81                collection: nsid(collection, syntax)?,
82                actions: actions.clone(),
83            })
84        })
85        .collect()
86}
87
88/// `repo:`'s `action`s. None given is every action.
89fn actions(syntax: &mut Syntax<'_>) -> Result<ActionSet, ScopeParseError> {
90    let values = syntax.list("action", false);
91    if values.is_empty() {
92        return Ok(ActionSet::All);
93    }
94    let mut actions = BTreeSet::new();
95    for value in &values {
96        if value.contains(',') {
97            syntax.warn(ScopeWarning::ActionList);
98        }
99        for word in value.split(',') {
100            let action = Action::parse(word)?;
101            if action == Action::Manage {
102                syntax.warn(ScopeWarning::RepoManage);
103            }
104            actions.insert(action);
105        }
106    }
107    Ok(ActionSet::Only(actions))
108}
109
110/// `rpc:`: one scope for each method, each at the atom's audience.
111fn rpc(atom: &str, syntax: &mut Syntax<'_>) -> Result<Vec<Scope>, ScopeParseError> {
112    let methods = syntax.list("lxm", true);
113    if methods.is_empty() {
114        return Err(ScopeParseError::MissingValue(atom.to_owned()));
115    }
116    let aud = syntax.single("aud", false);
117    match aud.as_deref() {
118        None => syntax.warn(ScopeWarning::NoAudience),
119        Some(aud) if aud == ANY_AUD || is_service_reference(aud) => {}
120        Some(aud) => syntax.warn(ScopeWarning::NotAServiceReference(aud.to_owned())),
121    }
122    let every_audience = aud.as_deref().is_none_or(|aud| aud == ANY_AUD);
123    methods
124        .iter()
125        .map(|method| {
126            let method = nsid(method, syntax)?;
127            if method == NsidPattern::Any && every_audience {
128                syntax.warn(ScopeWarning::EveryMethodAndAudience);
129            }
130            Ok(Scope::Rpc {
131                method,
132                aud: aud.clone(),
133            })
134        })
135        .collect()
136}
137
138/// A `repo:` collection or an `rpc:` method.
139fn nsid(value: &str, syntax: &mut Syntax<'_>) -> Result<NsidPattern, ScopeParseError> {
140    let pattern = NsidPattern::parse(value)?;
141    if matches!(pattern, NsidPattern::Prefix(_)) {
142        syntax.warn(ScopeWarning::PartialWildcard(value.to_owned()));
143    }
144    Ok(pattern)
145}
146
147/// `blob:`: one scope accepting every type the atom names.
148fn blob(atom: &str, syntax: &mut Syntax<'_>) -> Result<Scope, ScopeParseError> {
149    let accept = syntax.list("accept", true);
150    if accept.is_empty() {
151        return Err(ScopeParseError::MissingValue(atom.to_owned()));
152    }
153    let accept = accept
154        .iter()
155        .map(|mime| MimePattern::parse(mime))
156        .collect::<Result<_, _>>()?;
157    Ok(Scope::blob(accept))
158}
159
160/// `include:`: the set's name, and the audience its permissions may take.
161/// Whether either names anything is [`crate::Include::new`]'s to say.
162fn include(atom: &str, syntax: &mut Syntax<'_>) -> Result<Scope, ScopeParseError> {
163    let nsid = syntax
164        .single("nsid", true)
165        .filter(|nsid| !nsid.is_empty())
166        .ok_or_else(|| ScopeParseError::MissingValue(atom.to_owned()))?;
167    let aud = syntax.single("aud", false);
168    Ok(Scope::Include { nsid, aud })
169}