blazingly_aasa/model.rs
1//! The wire model: what an `apple-app-site-association` file literally says.
2//!
3//! This layer is deliberately permissive. It keeps `appID` and `appIDs` separate so the validator
4//! can report a document that sets both, and it keeps legacy `paths` alongside modern `components`
5//! so a mixed document can be diagnosed rather than silently reinterpreted. Normalisation into a
6//! single matchable form happens in [`CompiledAasa`](crate::CompiledAasa).
7
8use std::collections::BTreeMap;
9
10/// A parsed `apple-app-site-association` document.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct AasaDocument {
13 /// The `applinks` (universal links) section.
14 pub applinks: Option<AppLinks>,
15 /// The `webcredentials` (shared web credentials) section.
16 pub webcredentials: Option<AppService>,
17 /// The `appclips` section.
18 pub appclips: Option<AppService>,
19 /// The `activitycontinuation` (Handoff) section.
20 pub activitycontinuation: Option<AppService>,
21 /// Top-level keys this crate does not recognize, in source order.
22 pub unknown_keys: Vec<String>,
23 pub(crate) structural: Vec<crate::diagnostics::Diagnostic>,
24 pub(crate) byte_len: usize,
25}
26
27/// A service that is configured with a flat list of app identifiers.
28///
29/// Used by `webcredentials`, `appclips`, and `activitycontinuation`, none of which perform URL
30/// component matching.
31#[derive(Debug, Clone, Default, PartialEq, Eq)]
32pub struct AppService {
33 /// The application identifiers listed under `apps`.
34 pub apps: Vec<String>,
35}
36
37/// The `applinks` section.
38#[derive(Debug, Clone, Default, PartialEq, Eq)]
39pub struct AppLinks {
40 /// The legacy `apps` key, which Apple requires to be empty when present.
41 pub apps: Option<Vec<String>>,
42 /// Domain-level pattern-matching defaults.
43 pub defaults: Option<MatchDefaults>,
44 /// The app entries, in source order. Order is significant.
45 pub details: Vec<AppLinkDetail>,
46 /// Custom `substitutionVariables`.
47 pub substitution_variables: BTreeMap<String, Vec<String>>,
48 /// Whether `details` used the oldest dictionary-keyed-by-appID form rather than an array.
49 pub details_were_dictionary: bool,
50}
51
52/// One entry of `applinks.details`.
53#[derive(Debug, Clone, Default, PartialEq, Eq)]
54pub struct AppLinkDetail {
55 /// The singular `appID` key.
56 pub app_id: Option<String>,
57 /// The plural `appIDs` key.
58 pub app_ids: Option<Vec<String>>,
59 /// Modern ordered component rules.
60 pub components: Option<Vec<ComponentRule>>,
61 /// Legacy `paths` patterns, including `NOT `-prefixed exclusions.
62 pub paths: Option<Vec<String>>,
63 /// App-level pattern-matching defaults.
64 pub defaults: Option<MatchDefaults>,
65}
66
67impl AppLinkDetail {
68 /// Every application identifier this entry declares, `appID` first.
69 #[must_use]
70 pub fn declared_app_ids(&self) -> Vec<&str> {
71 let mut out = Vec::new();
72 if let Some(app_id) = &self.app_id {
73 out.push(app_id.as_str());
74 }
75 if let Some(app_ids) = &self.app_ids {
76 out.extend(app_ids.iter().map(String::as_str));
77 }
78 out
79 }
80}
81
82/// One entry of a `components` array.
83#[derive(Debug, Clone, Default, PartialEq, Eq)]
84pub struct ComponentRule {
85 /// The `/` key: a pattern for the URL path.
86 pub path: Option<String>,
87 /// The `?` key: a pattern or dictionary for the URL query.
88 pub query: Option<QueryRule>,
89 /// The `#` key: a pattern for the URL fragment.
90 pub fragment: Option<String>,
91 /// `exclude`: stop matching and refuse to open the URL.
92 pub exclude: Option<bool>,
93 /// `comment`: ignored by the system, preserved here for traces.
94 pub comment: Option<String>,
95 /// `caseSensitive` override.
96 pub case_sensitive: Option<bool>,
97 /// `percentEncoded` override.
98 pub percent_encoded: Option<bool>,
99}
100
101impl ComponentRule {
102 /// Whether the rule constrains no URL component at all, and so matches everything.
103 #[must_use]
104 pub fn is_unconstrained(&self) -> bool {
105 self.path.is_none() && self.query.is_none() && self.fragment.is_none()
106 }
107}
108
109/// The `?` key, which Apple allows to be either a pattern or a dictionary of predicates.
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum QueryRule {
112 /// A single pattern matched against the whole query string.
113 Whole(String),
114 /// Named predicates that must all be satisfied.
115 Items(BTreeMap<String, QueryPredicate>),
116}
117
118/// One entry of a `?` dictionary.
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub enum QueryPredicate {
121 /// A pattern matched against the query item's value.
122 Pattern(String),
123 /// A value whose meaning Apple does not document; it is reported rather than guessed at.
124 Unsupported {
125 /// The JSON type that appeared in place of a string.
126 json_type: &'static str,
127 },
128}
129
130/// Pattern-matching defaults, which may appear at the domain and app level.
131#[derive(Debug, Clone, Default, PartialEq, Eq)]
132pub struct MatchDefaults {
133 /// `caseSensitive` default for everything below this level.
134 pub case_sensitive: Option<bool>,
135 /// `percentEncoded` default for everything below this level.
136 pub percent_encoded: Option<bool>,
137 /// Other keys present in the object, which Apple documents as legal but does not specify.
138 pub other_keys: Vec<String>,
139}
140
141impl MatchDefaults {
142 /// Whether the object carried no setting this crate acts on.
143 #[must_use]
144 pub fn is_empty(&self) -> bool {
145 self.case_sensitive.is_none() && self.percent_encoded.is_none()
146 }
147}
148
149/// Apple's documented default: patterns are case-sensitive.
150pub const DEFAULT_CASE_SENSITIVE: bool = true;
151/// Apple's documented default: patterns are written percent-encoded.
152pub const DEFAULT_PERCENT_ENCODED: bool = true;
153
154/// The effective pattern-matching settings for one rule, after resolving the defaults hierarchy.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
156pub struct EffectiveDefaults {
157 /// Effective `caseSensitive`.
158 pub case_sensitive: bool,
159 /// Effective `percentEncoded`.
160 pub percent_encoded: bool,
161}
162
163impl Default for EffectiveDefaults {
164 fn default() -> Self {
165 Self {
166 case_sensitive: DEFAULT_CASE_SENSITIVE,
167 percent_encoded: DEFAULT_PERCENT_ENCODED,
168 }
169 }
170}
171
172impl EffectiveDefaults {
173 /// Applies a less specific layer of defaults, with existing values winning.
174 #[must_use]
175 pub fn overridden_by(self, defaults: Option<&MatchDefaults>) -> Self {
176 let Some(defaults) = defaults else {
177 return self;
178 };
179 Self {
180 case_sensitive: defaults.case_sensitive.unwrap_or(self.case_sensitive),
181 percent_encoded: defaults.percent_encoded.unwrap_or(self.percent_encoded),
182 }
183 }
184}