Skip to main content

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}