1use 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#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct Read {
16 pub scopes: Vec<Scope>,
19 pub warnings: Vec<ScopeWarning>,
22}
23
24pub(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
70fn 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
88fn 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
110fn 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
138fn 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
147fn 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
160fn 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}