Skip to main content

radicle/git/canonical/
rules.rs

1// Weird lint, see <https://github.com/rust-lang/rust-clippy/issues/14275>
2#![allow(clippy::doc_overindented_list_items)]
3
4//! Implementation of RIP-0004 Canonical References
5//!
6//! [`RawRules`] is intended to be deserialized and then validated into a set of
7//! [`Rules`]. These can then be used to see if a [`Qualified`] reference
8//! matches any of the rules, using [`Rules::matches`]. Using [`Canonical`] with
9//! the first matched rule, and this can be used to calculate the
10//! [`Canonical::quorum`].
11
12use core::fmt;
13use std::cmp::Ordering;
14use std::collections::BTreeMap;
15use std::sync::LazyLock;
16
17use nonempty::NonEmpty;
18use serde::{Deserialize, Serialize};
19use serde_json as json;
20use thiserror::Error;
21
22use crate::git;
23use crate::git::canonical;
24use crate::git::canonical::Canonical;
25use crate::git::fmt::refspec::QualifiedPattern;
26use crate::git::fmt::Qualified;
27use crate::git::fmt::{refname, RefString};
28use crate::identity::{doc, Did};
29
30const ASTERISK: char = '*';
31
32static REFS_RAD: LazyLock<RefString> = LazyLock::new(|| refname!("refs/rad"));
33
34/// Private trait to ensure that not any `Rule` can be deserialized.
35/// Implementations are provided for `Allowed` and `usize` so that `RawRule`s
36/// can be deserialized, while `ValidRule`s cannot – preventing deserialization
37/// bugs for that type.
38trait Sealed {}
39impl Sealed for Allowed {}
40impl Sealed for usize {}
41
42/// A `Pattern` is a `QualifiedPattern` reference, however, it disallows any
43/// references under the `refs/rad` hierarchy.
44#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(into = "QualifiedPattern", try_from = "QualifiedPattern")]
46pub struct Pattern(QualifiedPattern<'static>);
47
48impl fmt::Display for Pattern {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        f.write_str(self.0.as_str())
51    }
52}
53
54impl From<Pattern> for QualifiedPattern<'static> {
55    fn from(Pattern(pattern): Pattern) -> Self {
56        pattern
57    }
58}
59
60impl<'a> TryFrom<QualifiedPattern<'a>> for Pattern {
61    type Error = PatternError;
62
63    fn try_from(pattern: QualifiedPattern<'a>) -> Result<Self, Self::Error> {
64        if pattern.starts_with(REFS_RAD.as_str()) {
65            Err(PatternError::ProtectedRef {
66                prefix: (*REFS_RAD).clone(),
67                pattern: pattern.to_owned(),
68            })
69        } else {
70            Ok(Self(pattern.to_owned()))
71        }
72    }
73}
74
75impl<'a> TryFrom<Qualified<'a>> for Pattern {
76    type Error = PatternError;
77
78    fn try_from(name: Qualified<'a>) -> Result<Self, Self::Error> {
79        Self::try_from(QualifiedPattern::from(name))
80    }
81}
82
83impl Pattern {
84    /// Check if the `refname` matches the rule's `refspec`.
85    pub fn matches(&self, refname: &Qualified) -> bool {
86        // N.b. Git's refspecs do not quite match with glob-star semantics. A
87        // single `*` in a refspec is expected to match all references under
88        // that namespace, even if they are further down the hierarchy.
89        // Thus, the following rules are applied:
90        //
91        //   - a trailing `*` changes to `**/*`
92        //   - a `*` in between path components changes to `**`
93        let spec = match self.0.as_str().split_once(ASTERISK) {
94            None => self.0.to_string(),
95            // Expand `refs/tags/*` to `refs/tags/**/*`
96            Some((prefix, "")) => {
97                let mut spec = prefix.to_string();
98                spec.push_str("**/*");
99                spec
100            }
101            // Expand `refs/tags/*/v1.0` to `refs/tags/**/v1.0`
102            Some((prefix, suffix)) => {
103                let mut spec = prefix.to_string();
104                spec.push_str("**");
105                spec.push_str(suffix);
106                spec
107            }
108        };
109        fast_glob::glob_match(&spec, refname.as_str())
110    }
111}
112
113impl AsRef<QualifiedPattern<'static>> for Pattern {
114    fn as_ref(&self) -> &QualifiedPattern<'static> {
115        &self.0
116    }
117}
118
119impl PartialOrd for Pattern {
120    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
121        Some(self.cmp(other))
122    }
123}
124
125/// Patterns are ordered by their specificity.
126///
127/// This is heavily influenced by the evaluation priority of Rules. For a
128/// candidate reference name, we want the rule associated with the most specific
129/// pattern to apply, i.e. to take priority over all other rules with less
130/// specific patterns.
131///
132/// For two patterns `φ` and `ψ`, we say that "`φ` is more specific than `ψ`", denoted
133/// `φ < ψ` if:
134///
135///  1. The number of components in `φ` is larger than the number of components
136///     in `ψ`. (Note that the number of components is equal to the number of
137///     occurrences of the symbol '/' in the pattern, plus 1).
138///     The justification is, that refnames might be interpreted as a hierarchy
139///     where a match on more components would mean a match at a lower level in
140///     the hierarchy, thus being more specific.
141///     Imagine a refname hierarchy that maps to a corporate hierarchy.
142///     The pattern "department-1" matches all refnames that are administered
143///     by a particular department, and thus is not very specific.
144///     To contrast, the pattern "department-1/team-a/project-i/nice-feature"
145///     is very specific as it matches all refnames that relate to the
146///     development of a particular feature for a particular project by a
147///     particular team.
148///     Note that this would also apply when the connection between the `φ` and `ψ`
149///     is not as obvious, e.g. also `a/b/c/d/* < */x`.
150///
151/// (Note that for the following items, one may assume that `φ` and `ψ` have the
152/// same number of components.)
153///
154///  2. If path component i of `φ`, denoted `φ[i]`, is more specific than path
155///     component i of `ψ`, denoted `ψ[i]`. This is the case if:
156///      a. `φ[i]` does not contain an asterisk and `ψ[i]` contains an asterisk,
157///         i.e. the symbol `*`, e.g. `a < * and abc < a*`.
158///         Note that this is important to capture specificity across
159///         components, i.e. to conclude that `a/b/* < a/*/c`.
160///      b. Both `φ[i]` and `ψ[i]` contain an asterisk.
161///          A. The asterisk in `φ[i]` is further right than the asterisk in `φ[i]`,
162///             e.g. `aa* < a*`.
163///          B. The asterisk in `φ[i]` and `ψ[i]` is equally far to the right,
164///             and `φ[i]` is longer than `ψ[i]`, e.g. `a*b < a*`.
165///
166///  3. Otherwise, fall back to a lexicographic ordering.
167///
168/// Some examples (justification in parentheses):
169///
170/// ```text, no_run
171/// refs/tags/release/candidates/* <(1.)   refs/tags/release/* <(1.) refs/tags/*
172/// refs/tags/v1.0                 <(2.a.) refs/tags/*
173/// refs/heads/*                   <(3.)   refs/tags/*
174/// refs/heads/main                <(3.)   refs/tags/v1.0
175/// ```
176impl Ord for Pattern {
177    fn cmp(&self, other: &Self) -> Ordering {
178        #[derive(Debug, Clone, Copy)]
179        #[repr(i8)]
180        enum ComponentOrdering {
181            MatchLength(Ordering),
182            Lexicographic(Ordering),
183        }
184
185        impl ComponentOrdering {
186            fn merge(&mut self, other: Self) {
187                *self = match (*self, other) {
188                    (Self::Lexicographic(Ordering::Equal), Self::Lexicographic(other)) => {
189                        Self::Lexicographic(other)
190                    }
191                    (Self::Lexicographic(_), Self::MatchLength(other)) => Self::MatchLength(other),
192                    (Self::MatchLength(Ordering::Equal), Self::MatchLength(other)) => {
193                        Self::MatchLength(other)
194                    }
195                    (clone, _) => clone,
196                }
197            }
198        }
199
200        impl From<ComponentOrdering> for Ordering {
201            fn from(value: ComponentOrdering) -> Self {
202                match value {
203                    ComponentOrdering::MatchLength(ordering) => ordering,
204                    ComponentOrdering::Lexicographic(ordering) => ordering,
205                }
206            }
207        }
208
209        impl Default for ComponentOrdering {
210            /// The weakest value of Self, which will be absorbed by any
211            /// other in [`ComponentOrdering::merge`].
212            fn default() -> Self {
213                Self::Lexicographic(Ordering::Equal)
214            }
215        }
216
217        use git::fmt::refspec::Component;
218
219        fn cmp_component(lhs: Component<'_>, rhs: Component<'_>) -> ComponentOrdering {
220            let (l, r) = (lhs.as_str(), rhs.as_str());
221            match (l.find(ASTERISK), r.find(ASTERISK)) {
222                // (2.a.)
223                (Some(_), None) => ComponentOrdering::MatchLength(Ordering::Greater),
224                // (2.a.)
225                (None, Some(_)) => ComponentOrdering::MatchLength(Ordering::Less),
226                (Some(li), Some(ri)) => {
227                    if li != ri {
228                        // (2.b.A)
229                        ComponentOrdering::MatchLength(li.cmp(&ri).reverse())
230                    } else if l.len() != r.len() {
231                        // (2.b.B)
232                        ComponentOrdering::MatchLength(l.len().cmp(&r.len()).reverse())
233                    } else {
234                        // (3.)
235                        ComponentOrdering::Lexicographic(l.cmp(r))
236                    }
237                }
238                // (3.)
239                (None, None) => ComponentOrdering::Lexicographic(l.cmp(r)),
240            }
241        }
242
243        let mut result = ComponentOrdering::default();
244        let mut lhs = self.0.components();
245        let mut rhs = other.0.components();
246        loop {
247            match (lhs.next(), rhs.next()) {
248                (None, Some(_)) => return Ordering::Greater, // (1.)
249                (Some(_), None) => return Ordering::Less,    // (1.)
250                (Some(lhs), Some(rhs)) => {
251                    result.merge(cmp_component(lhs, rhs));
252                }
253                (None, None) => return result.into(),
254            }
255        }
256    }
257}
258
259/// A [`Rule`] that can be serialized and deserialized safely.
260///
261/// Should be converted to a [`ValidRule`] via [`Rule::validate`].
262pub type RawRule = Rule<Allowed, usize>;
263
264impl RawRule {
265    /// Validate the `Rule` into a form that can be used for calculating
266    /// canonical references.
267    ///
268    /// The `resolve` callback is to allow the caller to specify the DIDs of the
269    /// identity document, in the case that the allowed value is
270    /// [`Allowed::Delegates`].
271    pub fn validate<R>(self, resolve: &mut R) -> Result<ValidRule, ValidationError>
272    where
273        R: Fn() -> doc::Delegates,
274    {
275        let Self {
276            allow: delegates,
277            threshold,
278            ..
279        } = self;
280        let allow = match &delegates {
281            Allowed::Delegates => ResolvedDelegates::Delegates(resolve()),
282            Allowed::Set(delegates) => {
283                let valid =
284                    doc::Delegates::new(delegates.clone()).map_err(ValidationError::from)?;
285                ResolvedDelegates::Set(valid)
286            }
287        };
288        let threshold = doc::Threshold::new(threshold, &allow)?;
289        Ok(Rule {
290            allow,
291            threshold,
292            extensions: self.extensions,
293        })
294    }
295}
296
297/// A set of `RawRule`s that can be serialized and deserialized.
298#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
299pub struct RawRules {
300    /// The reference pattern that this rule applies to.
301    ///
302    /// Note that this can be a fully-qualified pattern, e.g. `refs/heads/qa`,
303    /// as well as a wild-card pattern, e.g. `refs/tags/*`.
304    #[serde(flatten)]
305    pub rules: BTreeMap<Pattern, RawRule>,
306}
307
308impl RawRules {
309    /// Returns an iterator over the [`Pattern`] and [`RawRule`] in the set of
310    /// rules.
311    pub fn iter(&self) -> impl Iterator<Item = (&Pattern, &RawRule)> {
312        self.rules.iter()
313    }
314
315    /// Add a new [`RawRule`] to the set of rules.
316    ///
317    /// Returns the replaced rule, if it existed.
318    pub fn insert(&mut self, pattern: Pattern, rule: RawRule) -> Option<RawRule> {
319        self.rules.insert(pattern, rule)
320    }
321
322    /// Remove the rule that matches the `pattern` parameter.
323    ///
324    /// Returns the rule if it existed.
325    pub fn remove(&mut self, pattern: &Pattern) -> Option<RawRule> {
326        self.rules.remove(pattern)
327    }
328
329    /// Check to see if there is an exact match for `refname` in the rules.
330    pub fn exact_match(&self, refname: &Qualified) -> bool {
331        let refname = refname.as_str();
332        self.rules
333            .iter()
334            .any(|(pattern, _)| pattern.0.as_str() == refname)
335    }
336
337    /// Check if the `refname` matches any existing rules, including glob
338    /// matches.
339    pub fn matches<'a, 'b>(
340        &self,
341        refname: &Qualified<'b>,
342    ) -> impl Iterator<Item = (&Pattern, &RawRule)> + use<'a, '_, 'b> {
343        let refname = refname.clone();
344        self.rules
345            .iter()
346            .filter(move |(pattern, _)| pattern.matches(&refname))
347    }
348}
349
350impl Extend<(Pattern, RawRule)> for RawRules {
351    fn extend<T: IntoIterator<Item = (Pattern, RawRule)>>(&mut self, iter: T) {
352        self.rules.extend(iter)
353    }
354}
355
356impl From<BTreeMap<Pattern, RawRule>> for RawRules {
357    fn from(rules: BTreeMap<Pattern, RawRule>) -> Self {
358        RawRules { rules }
359    }
360}
361
362impl FromIterator<(Pattern, RawRule)> for RawRules {
363    fn from_iter<T: IntoIterator<Item = (Pattern, RawRule)>>(iter: T) -> Self {
364        iter.into_iter().collect::<BTreeMap<_, _>>().into()
365    }
366}
367
368impl IntoIterator for RawRules {
369    type Item = (Pattern, RawRule);
370    type IntoIter = std::collections::btree_map::IntoIter<Pattern, RawRule>;
371
372    fn into_iter(self) -> Self::IntoIter {
373        self.rules.into_iter()
374    }
375}
376
377/// A [`Rule`] that has been validated. See [`Rules`] and [`Rules::matches`] for
378/// its main usage.
379///
380/// N.b. a `ValidRule` can be serialized, however, it cannot be deserialized.
381/// This is due to the fact that the `allow` field may have a value of
382/// `delegates`. In those cases the value needs to be looked up via the identity
383/// document and validated.
384pub type ValidRule = Rule<ResolvedDelegates, doc::Threshold>;
385
386impl ValidRule {
387    /// Initialize a `ValidRule` for the default branch, given by `name`. The
388    /// rule will contain the single `did` as the allowed DID, and use a
389    /// threshold of `1`.
390    ///
391    /// Note that the serialization of the rule will use the `delegates` token
392    /// for the rule. E.g.
393    /// ```json
394    /// {
395    ///   "pattern": "refs/heads/main",
396    ///   "allow": ["did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"],
397    ///   "threshold": 1
398    /// }
399    /// ```
400    ///
401    /// # Errors
402    ///
403    /// If the `name` reference begins with `refs/rad`.
404    pub fn default_branch(
405        did: Did,
406        name: &git::fmt::RefStr,
407    ) -> Result<(Pattern, Self), PatternError> {
408        let pattern = Pattern::try_from(git::refs::branch(name).to_owned())?;
409        let rule = Self {
410            allow: ResolvedDelegates::Delegates(doc::Delegates::from(did)),
411            // N.B. this needs to be the minimum since we only have one
412            // delegate.
413            threshold: doc::Threshold::MIN,
414            extensions: json::Map::new(),
415        };
416        Ok((pattern, rule))
417    }
418}
419
420impl From<ValidRule> for RawRule {
421    fn from(rule: ValidRule) -> Self {
422        let Rule {
423            allow,
424            threshold,
425            extensions,
426        } = rule;
427        Self {
428            allow: allow.into(),
429            threshold: threshold.into(),
430            extensions,
431        }
432    }
433}
434
435/// A representation of a set of allowed DIDs.
436///
437/// `Allowed` is used in a `RawRule`.
438#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
439pub enum Allowed {
440    /// Pointer to the identity document's set of delegates.
441    #[serde(rename = "delegates")]
442    #[default]
443    Delegates,
444    /// Explicit list of allowed DIDs.
445    ///
446    /// The elements of the list of allowed DIDs will be made unique, i.e.
447    /// duplicate DIDs will be discarded.
448    ///
449    /// # Validation
450    ///
451    /// The list of allowed DIDs, `allowed`, must satisfy:
452    /// ```text
453    /// 1 <= allowed.len() <= 255
454    /// ```
455    #[serde(untagged)]
456    Set(NonEmpty<Did>),
457}
458
459impl From<NonEmpty<Did>> for Allowed {
460    fn from(dids: NonEmpty<Did>) -> Self {
461        Self::Set(dids)
462    }
463}
464
465impl From<Did> for Allowed {
466    fn from(did: Did) -> Self {
467        Self::Set(NonEmpty::new(did))
468    }
469}
470
471/// A marker `enum` that is used in a [`ValidRule`].
472///
473/// It ensures that a rule that has been deserialized, resolving the `delegates`
474/// token to a set of DIDs, is still serialized back to the `delegates` token –
475/// as opposed to serializing it to the set of DIDs.
476///
477/// The variants mirror the [`Allowed::Delegates`] and [`Allowed::Set`]
478/// variants.
479#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
480#[serde(into = "Allowed")]
481pub enum ResolvedDelegates {
482    Delegates(doc::Delegates),
483    Set(doc::Delegates),
484}
485
486impl From<ResolvedDelegates> for Allowed {
487    fn from(ds: ResolvedDelegates) -> Self {
488        match ds {
489            ResolvedDelegates::Delegates(_) => Self::Delegates,
490            ResolvedDelegates::Set(ds) => Self::Set(ds.into()),
491        }
492    }
493}
494
495impl std::ops::Deref for ResolvedDelegates {
496    type Target = doc::Delegates;
497
498    fn deref(&self) -> &Self::Target {
499        match self {
500            ResolvedDelegates::Delegates(ds) => ds,
501            ResolvedDelegates::Set(ds) => ds,
502        }
503    }
504}
505
506/// A reference that has been matched against a [`ValidRule`].
507///
508/// Can be constructed by using [`Rules::matches`].
509#[derive(Debug)]
510pub struct MatchedRule<'a> {
511    refname: Qualified<'a>,
512    rule: ValidRule,
513}
514
515impl MatchedRule<'_> {
516    /// Return the reference name that was used for checking if it was a match.
517    pub fn refname(&self) -> &Qualified<'_> {
518        &self.refname
519    }
520
521    /// Return the rule that was matched.
522    pub fn rule(&self) -> &ValidRule {
523        &self.rule
524    }
525
526    /// Return the allowed DIDs for the matched rule.
527    pub fn allowed(&self) -> &doc::Delegates {
528        self.rule().allowed()
529    }
530
531    /// Return the [`doc::Threshold`] for the matched rule.
532    pub fn threshold(&self) -> &doc::Threshold {
533        self.rule().threshold()
534    }
535}
536
537/// A set of valid [`Rule`]s, where the set of DIDs and threshold are fully
538/// resolved and valid. Since the rules are constructed via a `BTreeMap`, they
539/// cannot be duplicated.
540///
541/// To construct the set of rules, use [`Rules::from_raw`], which validates a
542/// set of [`RawRule`]s, and their [`Pattern`] references, into a set of
543/// [`ValidRule`]s.
544#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
545pub struct Rules {
546    #[serde(flatten)]
547    rules: BTreeMap<Pattern, ValidRule>,
548}
549
550impl FromIterator<(Pattern, ValidRule)> for Rules {
551    fn from_iter<T: IntoIterator<Item = (Pattern, ValidRule)>>(iter: T) -> Self {
552        Self {
553            rules: iter.into_iter().collect(),
554        }
555    }
556}
557
558impl<'a> IntoIterator for &'a Rules {
559    type Item = (&'a Pattern, &'a ValidRule);
560    type IntoIter = std::collections::btree_map::Iter<'a, Pattern, ValidRule>;
561
562    fn into_iter(self) -> Self::IntoIter {
563        self.rules.iter()
564    }
565}
566
567impl IntoIterator for Rules {
568    type Item = (Pattern, ValidRule);
569    type IntoIter = std::collections::btree_map::IntoIter<Pattern, ValidRule>;
570
571    fn into_iter(self) -> Self::IntoIter {
572        self.rules.into_iter()
573    }
574}
575
576impl Extend<(Pattern, ValidRule)> for Rules {
577    fn extend<T: IntoIterator<Item = (Pattern, ValidRule)>>(&mut self, iter: T) {
578        self.rules.extend(iter)
579    }
580}
581
582impl From<Rules> for RawRules {
583    fn from(Rules { rules }: Rules) -> Self {
584        Self {
585            rules: rules
586                .into_iter()
587                .map(|(pattern, rule)| (pattern, rule.into()))
588                .collect(),
589        }
590    }
591}
592
593impl Rules {
594    /// Returns an iterator over the [`Pattern`] and [`ValidRule`] in the set of
595    /// rules.
596    pub fn iter(&self) -> impl Iterator<Item = (&Pattern, &ValidRule)> {
597        self.rules.iter()
598    }
599
600    /// Returns `true` is the set of rules is empty.
601    pub fn is_empty(&self) -> bool {
602        self.rules.is_empty()
603    }
604
605    /// Construct a set of `Rules` given a set of `RawRule`s.
606    pub fn from_raw<R>(
607        rules: impl IntoIterator<Item = (Pattern, RawRule)>,
608        resolve: &mut R,
609    ) -> Result<Self, ValidationError>
610    where
611        R: Fn() -> doc::Delegates,
612    {
613        let valid = rules
614            .into_iter()
615            .map(|(pattern, rule)| rule.validate(resolve).map(|rule| (pattern, rule)))
616            .collect::<Result<_, _>>()?;
617        Ok(Self { rules: valid })
618    }
619
620    /// Return the matching rules for the given `refname`.
621    pub fn matches<'a>(
622        &self,
623        refname: &Qualified<'a>,
624    ) -> impl Iterator<Item = (&Pattern, &ValidRule)> + use<'a, '_> {
625        let refname_cloned = refname.clone();
626        self.rules
627            .iter()
628            .filter(move |(pattern, _)| pattern.matches(&refname_cloned))
629    }
630
631    /// Match given refname, take the most specific rule, and prepare evaluation
632    /// as [`Canonical`]
633    ///
634    /// N.b. it will find the first rule that is most specific for the given
635    /// `refname`.
636    pub fn canonical<'a, 'b, 'r, R>(
637        &'a self,
638        refname: Qualified<'b>,
639        repo: &'r R,
640    ) -> Option<Canonical<'b, 'a, 'r, R, canonical::Initial>>
641    where
642        R: canonical::effects::Ancestry
643            + canonical::effects::FindMergeBase
644            + canonical::effects::FindObjects,
645    {
646        self.matches(&refname)
647            .next()
648            .map(|(_, rule)| Canonical::new(refname, rule, repo))
649    }
650}
651
652/// A `Rule` defines how a reference or set of references can be made canonical,
653/// i.e. have a top-level `refs/*` entry – see [`Pattern`].
654///
655/// The [`Rule::allowed`] type is generic to allow for [`Allowed`] to be used
656/// for serialization and deserialization, however, the use of
657/// [`Rule::validate`] should be used to get a valid rule.
658///
659/// The [`Rule::threshold`], similarly, allows for [`doc::Threshold`] to be used, and
660/// [`Rule::validate`] should be used to get a valid rule.
661// N.b. it's safe to derive `Serialize` since we only allow constructing a
662// `Rule` via `Rule::validate`, and we seal `Deserialize` by ensuring that only
663// `RawRule` can be deserialized.
664#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
665#[serde(bound(deserialize = "D: Sealed + Deserialize<'de>, T: Sealed + Deserialize<'de>"))]
666pub struct Rule<D, T> {
667    /// The set of allowed DIDs that are considered for voting for this rule.
668    allow: D,
669    /// The threshold the votes must pass for the reference(s) to be considered
670    /// canonical.
671    threshold: T,
672
673    /// Optional extensions in rules. This is intended to preserve backwards and
674    /// forward-compatibility
675    #[serde(skip_serializing_if = "json::Map::is_empty")]
676    #[serde(flatten)]
677    extensions: json::Map<String, json::Value>,
678}
679
680impl<D, T> Rule<D, T> {
681    /// Construct a new `Rule` with the given `allow` and `threshold`.
682    pub fn new(allow: D, threshold: T) -> Self {
683        Self {
684            allow,
685            threshold,
686            extensions: json::Map::new(),
687        }
688    }
689
690    /// Get the set of DIDs this `Rule` was created with.
691    pub fn allowed(&self) -> &D {
692        &self.allow
693    }
694
695    /// Get the set of threshold this `Rule` was created with.
696    pub fn threshold(&self) -> &T {
697        &self.threshold
698    }
699
700    /// Get the extensions that may have been added to this `Rule`.
701    pub fn extensions(&self) -> &json::Map<String, json::Value> {
702        &self.extensions
703    }
704
705    /// If the [`Rule::extensions`] is not set, the provided `extensions` will
706    /// be used.
707    ///
708    /// Otherwise, it expects that the JSON value is a `Map` and the
709    /// `extensions` are merged. If the existing value is any other kind of JSON
710    /// value, this is a no-op.
711    pub fn add_extensions(&mut self, extensions: impl Into<json::Map<String, json::Value>>) {
712        self.extensions.extend(extensions.into());
713    }
714}
715
716#[derive(Debug, Error)]
717pub enum PatternError {
718    #[error("cannot create rule for '{pattern}' since references under '{prefix}' are protected")]
719    ProtectedRef {
720        prefix: RefString,
721        pattern: QualifiedPattern<'static>,
722    },
723}
724
725#[derive(Debug, Error)]
726pub enum ValidationError {
727    #[error(transparent)]
728    Threshold(#[from] doc::ThresholdError),
729    #[error(transparent)]
730    Delegates(#[from] doc::DelegatesError),
731    #[error("cannot create rule for reserved `rad` references '{pattern}'")]
732    RadRef { pattern: QualifiedPattern<'static> },
733}
734
735#[derive(Debug, Error)]
736pub enum CanonicalError {
737    #[error(transparent)]
738    Git(#[from] crate::git::raw::Error),
739}
740
741#[cfg(test)]
742#[allow(clippy::unwrap_used)]
743mod tests {
744    use std::collections::BTreeMap;
745
746    use nonempty::nonempty;
747
748    use crate::crypto::{test::signer::MockSigner, Signer};
749    use crate::git;
750    use crate::git::fmt::qualified_pattern;
751    use crate::git::fmt::RefString;
752    use crate::identity::doc::Doc;
753    use crate::identity::Visibility;
754    use crate::node::device::Device;
755    use crate::rad;
756    use crate::storage::refs::{IDENTITY_BRANCH, IDENTITY_ROOT, SIGREFS_BRANCH, SIGREFS_PARENT};
757    use crate::storage::{git::transport, ReadStorage};
758    use crate::test::{arbitrary, fixtures};
759    use crate::Storage;
760
761    use super::*;
762
763    fn roundtrip(rule: &Rule<Allowed, usize>) {
764        let json = serde_json::to_string(rule).unwrap();
765        assert_eq!(
766            *rule,
767            serde_json::from_str(&json).unwrap(),
768            "failed to roundtrip: {json}"
769        )
770    }
771
772    fn did(s: &str) -> Did {
773        s.parse().unwrap()
774    }
775
776    fn pattern(qp: QualifiedPattern<'static>) -> Pattern {
777        Pattern::try_from(qp).unwrap()
778    }
779
780    fn resolve_from_doc(doc: &Doc) -> doc::Delegates {
781        doc.delegates().clone()
782    }
783
784    fn tag(name: RefString, head: git::raw::Oid, repo: &git::raw::Repository) -> git::Oid {
785        let commit = fixtures::commit(name.as_str(), &[head], repo);
786        let target = repo.find_object(commit.into(), None).unwrap();
787        let tagger = repo.signature().unwrap();
788        repo.tag(name.as_str(), &target, &tagger, name.as_str(), false)
789            .unwrap()
790            .into()
791    }
792
793    #[test]
794    fn test_roundtrip() {
795        let rule1 = Rule::new(Allowed::Delegates, 1);
796        let rule2 = Rule::new(Allowed::Delegates, 1);
797        let rule3 = Rule::new(Allowed::Delegates, 1);
798        let mut rule4 = Rule::new(
799            Allowed::Set(nonempty![
800                did("did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K"),
801                did("did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"),
802            ]),
803            2,
804        );
805        rule4.add_extensions(
806            serde_json::json!({
807                "foo": "bar",
808                "quux": 5,
809            })
810            .as_object()
811            .cloned()
812            .unwrap(),
813        );
814        roundtrip(&rule1);
815        roundtrip(&rule2);
816        roundtrip(&rule3);
817        roundtrip(&rule4);
818    }
819
820    #[test]
821    fn test_deserialization() {
822        let examples = r#"
823{
824  "refs/heads/main": {
825    "threshold": 2,
826    "allow": [
827      "did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K",
828      "did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"
829    ]
830  },
831  "refs/tags/releases/*": {
832    "threshold": 2,
833    "allow": [
834      "did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56",
835      "did:key:z6Mkq2E5Se5H9gk1DsL1EMwR2t4CqSg3GFkNN2UeG4FNqXoP",
836      "did:key:z6MkqRmXW5fbP9hJ1Y8j2N4CgVdJ2XJ6TsyXYf3FQ2NJgXax"
837    ]
838  },
839  "refs/heads/development": {
840    "threshold": 1,
841    "allow": [
842      "did:key:z6MkhH7ENYE62JAjTiRZPU71MGZ6xCwnbyHHWfrBu3fr6PVG"
843    ]
844  },
845  "refs/heads/release/*": {
846    "threshold": 1,
847    "allow": "delegates"
848  }
849}
850 "#;
851        let expected = [
852            (
853                pattern(qualified_pattern!("refs/heads/main")),
854                Rule::new(
855                    Allowed::Set(nonempty![
856                        did("did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K"),
857                        did("did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"),
858                    ]),
859                    2,
860                ),
861            ),
862            (
863                pattern(qualified_pattern!("refs/tags/releases/*")),
864                Rule::new(
865                    Allowed::Set(nonempty![
866                        did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"),
867                        did("did:key:z6Mkq2E5Se5H9gk1DsL1EMwR2t4CqSg3GFkNN2UeG4FNqXoP"),
868                        did("did:key:z6MkqRmXW5fbP9hJ1Y8j2N4CgVdJ2XJ6TsyXYf3FQ2NJgXax")
869                    ]),
870                    2,
871                ),
872            ),
873            (
874                pattern(qualified_pattern!("refs/heads/development")),
875                Rule::new(
876                    Allowed::Set(nonempty![did(
877                        "did:key:z6MkhH7ENYE62JAjTiRZPU71MGZ6xCwnbyHHWfrBu3fr6PVG"
878                    )]),
879                    1,
880                ),
881            ),
882            (
883                pattern(qualified_pattern!("refs/heads/release/*")),
884                Rule::new(Allowed::Delegates, 1),
885            ),
886        ]
887        .into_iter()
888        .collect::<RawRules>();
889        let rules = serde_json::from_str::<BTreeMap<Pattern, RawRule>>(examples)
890            .unwrap()
891            .into();
892        assert_eq!(expected, rules)
893    }
894
895    #[test]
896    fn test_order() {
897        assert!(
898            pattern(qualified_pattern!("refs/heads/a/b/c/d/*"))
899                < pattern(qualified_pattern!("refs/heads/*/x")),
900            "example 1"
901        );
902        assert!(
903            pattern(qualified_pattern!("refs/heads/a"))
904                < pattern(qualified_pattern!("refs/heads/*")),
905            "example 2.a"
906        );
907        assert!(
908            pattern(qualified_pattern!("refs/heads/abc"))
909                < pattern(qualified_pattern!("refs/heads/a*")),
910            "example 2.a"
911        );
912        assert!(
913            pattern(qualified_pattern!("refs/heads/a/b/*"))
914                < pattern(qualified_pattern!("refs/heads/a/*/c")),
915            "example 2.a"
916        );
917        assert!(
918            pattern(qualified_pattern!("refs/heads/aa*"))
919                < pattern(qualified_pattern!("refs/heads/a*")),
920            "example 2.b.A"
921        );
922        assert!(
923            pattern(qualified_pattern!("refs/heads/a*b"))
924                < pattern(qualified_pattern!("refs/heads/a*")),
925            "example 2.b.B"
926        );
927
928        let pattern01 = pattern(qualified_pattern!("refs/tags/*"));
929        let pattern02 = pattern(qualified_pattern!("refs/tags/v1"));
930        let pattern04 = pattern(qualified_pattern!("refs/tags/v1.0.0"));
931        let pattern05 = pattern(qualified_pattern!("refs/tags/release/v1.0.0"));
932        let pattern03 = pattern(qualified_pattern!("refs/heads/main"));
933        let pattern06 = pattern(qualified_pattern!("refs/tags/*/v1.0.0"));
934
935        let pattern07 = pattern(qualified_pattern!("refs/tags/x*"));
936        let pattern08 = pattern(qualified_pattern!("refs/tags/xx*"));
937
938        let pattern09 = pattern(qualified_pattern!("refs/foos/*"));
939
940        let pattern10 = pattern(qualified_pattern!("refs/heads/a"));
941        let pattern11 = pattern(qualified_pattern!("refs/heads/b"));
942
943        let pattern12 = pattern(qualified_pattern!("refs/heads/a/*"));
944        let pattern13 = pattern(qualified_pattern!("refs/heads/b/*"));
945
946        let pattern14 = pattern(qualified_pattern!("refs/heads/a/*/ab"));
947        let pattern15 = pattern(qualified_pattern!("refs/heads/a/*/a"));
948
949        let pattern16 = pattern(qualified_pattern!("refs/heads/a/*/b"));
950        let pattern17 = pattern(qualified_pattern!("refs/heads/a/*/a"));
951
952        // Test priority for path specificity
953        assert!(
954            pattern06 < pattern02,
955            "match for 06 is always more specific since it has more components"
956        );
957        assert!(pattern02 < pattern01, "match for 02 is also match for 01");
958        assert!(pattern08 < pattern07, "match for 08 is also match for 07");
959        // Test equality
960        assert!(pattern02 == pattern02);
961        // Test lexicographical fallback when paths are equally specific
962        assert!(pattern02 < pattern04);
963        assert!(pattern03 < pattern01);
964        assert!(pattern09 < pattern01);
965        assert!(pattern10 < pattern11);
966        assert!(pattern12 < pattern13);
967        assert!(pattern15 < pattern14);
968        assert!(
969            pattern17 < pattern16,
970            "matches have same length, but lexicographically, 'a' < 'b'"
971        );
972
973        // Test example from docs
974        let pattern18 = pattern(qualified_pattern!("refs/tags/release/candidates/*"));
975        let pattern19 = pattern(qualified_pattern!("refs/tags/release/*"));
976        let pattern20 = pattern(qualified_pattern!("refs/tags/*"));
977
978        assert!(pattern18 < pattern19);
979        assert!(pattern19 < pattern20);
980
981        let pattern21 = pattern(qualified_pattern!("refs/heads/dev"));
982
983        assert!(pattern21 < pattern03);
984
985        let mut patterns = [
986            pattern01.clone(),
987            pattern02.clone(),
988            pattern03.clone(),
989            pattern04.clone(),
990            pattern05.clone(),
991            pattern06.clone(),
992        ];
993        patterns.sort();
994
995        assert_eq!(
996            patterns,
997            [pattern05, pattern06, pattern03, pattern02, pattern04, pattern01]
998        );
999    }
1000
1001    #[test]
1002    fn test_deserialize_extensions() {
1003        let example = r#"
1004{
1005  "threshold": 2,
1006  "allow": [
1007    "did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K",
1008    "did:key:z6MknG1nYDftMYUQ7eTBSGgqB2PL1xK5Pif33J3sRym3e8ye"
1009  ],
1010  "foo": "bar",
1011  "quux": 5
1012}
1013"#;
1014        let rule = serde_json::from_str::<Rule<Allowed, usize>>(example).unwrap();
1015        assert!(!rule.extensions().is_empty());
1016        let extensions = rule.extensions();
1017        assert_eq!(
1018            extensions.get("foo"),
1019            Some(serde_json::Value::String("bar".to_string())).as_ref()
1020        );
1021        assert_eq!(
1022            extensions.get("quux"),
1023            Some(serde_json::Value::Number(5.into())).as_ref()
1024        );
1025    }
1026
1027    #[test]
1028    fn test_rule_validate_success() {
1029        let doc = arbitrary::gen::<Doc>(1);
1030        let delegates = Allowed::Set(doc.delegates().as_ref().clone());
1031        let threshold = doc.majority();
1032
1033        let rule = Rule::new(delegates, threshold);
1034        let result = rule.validate(&mut || resolve_from_doc(&doc));
1035        assert!(result.is_ok(), "failed to validate doc: {result:?}");
1036
1037        let rule = Rule::new(Allowed::Delegates, 1);
1038        let result = rule.validate(&mut || resolve_from_doc(&doc));
1039        assert!(result.is_ok(), "failed to validate doc: {result:?}");
1040    }
1041
1042    #[test]
1043    fn test_rule_validate_failures() {
1044        let doc = arbitrary::gen::<Doc>(1);
1045        let pattern = pattern(qualified_pattern!("refs/heads/main"));
1046
1047        assert!(matches!(
1048            Rule::new(Allowed::Delegates, 256).validate(&mut || resolve_from_doc(&doc)),
1049            Err(ValidationError::Threshold(_))
1050        ));
1051
1052        let threshold = doc.delegates().len().saturating_add(1);
1053        assert!(matches!(
1054            Rule::new(Allowed::Delegates, threshold).validate(&mut || resolve_from_doc(&doc)),
1055            Err(ValidationError::Threshold(_))
1056        ));
1057
1058        let delegates = NonEmpty::from_vec(arbitrary::vec::<Did>(256)).unwrap();
1059        assert!(matches!(
1060            Rule::new(delegates.into(), 1).validate(&mut || resolve_from_doc(&doc)),
1061            Err(ValidationError::Delegates(_))
1062        ));
1063
1064        let delegates = nonempty![
1065            did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"),
1066            did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56")
1067        ];
1068        let expected = Rule {
1069            allow: ResolvedDelegates::Set(
1070                doc::Delegates::new(nonempty![did(
1071                    "did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"
1072                )])
1073                .unwrap(),
1074            ),
1075            threshold: doc::Threshold::MIN,
1076            extensions: json::Map::new(),
1077        };
1078        assert_eq!(
1079            Rule::new(delegates.into(), 1)
1080                .validate(&mut || resolve_from_doc(&doc))
1081                .unwrap(),
1082            expected,
1083        );
1084
1085        // Duplicate rules are overwritten
1086        let rules = vec![
1087            (pattern.clone(), Rule::new(Allowed::Delegates, 1)),
1088            (
1089                pattern.clone(),
1090                Rule::new(doc.delegates().as_ref().clone().into(), 1),
1091            ),
1092        ];
1093        let expected = [(
1094            pattern,
1095            Rule::new(
1096                ResolvedDelegates::Set(doc.delegates().clone()),
1097                doc::Threshold::MIN,
1098            ),
1099        )]
1100        .into_iter()
1101        .collect::<Rules>();
1102        assert_eq!(
1103            Rules::from_raw(rules, &mut || resolve_from_doc(&doc)).unwrap(),
1104            expected
1105        );
1106    }
1107
1108    #[test]
1109    fn test_canonical() {
1110        let tempdir = tempfile::tempdir().unwrap();
1111        let storage = Storage::open(tempdir.path().join("storage"), fixtures::user()).unwrap();
1112
1113        transport::local::register(storage.clone());
1114
1115        let delegate = Device::mock_from_seed([0xff; 32]);
1116        let contributor = MockSigner::from_seed([0xfe; 32]);
1117        let (repo, head) = fixtures::repository(tempdir.path().join("working"));
1118        let (rid, doc, _) = rad::init(
1119            &repo,
1120            "heartwood".try_into().unwrap(),
1121            "Radicle Heartwood Protocol & Stack",
1122            git::fmt::refname!("master"),
1123            Visibility::default(),
1124            &delegate,
1125            &storage,
1126        )
1127        .unwrap();
1128
1129        let mut doc = doc.edit();
1130        // Ensure there is a second delegate for testing overlapping rules
1131        doc.delegate(contributor.public_key().into());
1132
1133        // Create tags and keep track of their OIDs
1134        //
1135        // follows the `refs/tags/release/candidates/*` rule
1136        let failing_tag = git::fmt::refname!("release/candidates/v1.0");
1137        let tags = [
1138            // follows the `refs/tags/*` rule
1139            git::fmt::refname!("v1.0"),
1140            // follows the `refs/tags/release/*` rule
1141            git::fmt::refname!("release/v1.0"),
1142            failing_tag.clone(),
1143            // follows the `refs/tags/*` rule
1144            git::fmt::refname!("qa/v1.0"),
1145        ]
1146        .into_iter()
1147        .map(|name| {
1148            (
1149                git::fmt::lit::refs_tags(name.clone()).into(),
1150                tag(name, head, &repo),
1151            )
1152        })
1153        .collect::<BTreeMap<Qualified, _>>();
1154
1155        git::push(
1156            &repo,
1157            &rad::REMOTE_NAME,
1158            [
1159                (
1160                    &git::fmt::qualified!("refs/tags/v1.0"),
1161                    &git::fmt::qualified!("refs/tags/v1.0"),
1162                ),
1163                (
1164                    &git::fmt::qualified!("refs/tags/release/v1.0"),
1165                    &git::fmt::qualified!("refs/tags/release/v1.0"),
1166                ),
1167                (
1168                    &git::fmt::qualified!("refs/tags/release/candidates/v1.0"),
1169                    &git::fmt::qualified!("refs/tags/release/candidates/v1.0"),
1170                ),
1171                (
1172                    &git::fmt::qualified!("refs/tags/qa/v1.0"),
1173                    &git::fmt::qualified!("refs/tags/qa/v1.0"),
1174                ),
1175            ],
1176        )
1177        .unwrap();
1178
1179        let rules = Rules::from_raw(
1180            [
1181                (
1182                    pattern(qualified_pattern!("refs/tags/*")),
1183                    Rule::new(Allowed::Delegates, 1),
1184                ),
1185                (
1186                    pattern(qualified_pattern!("refs/tags/release/*")),
1187                    Rule::new(Allowed::Delegates, 1),
1188                ),
1189                // Ensure that none of the other rules apply by ensuring we need
1190                // both delegates to get the quorum of the
1191                // `refs/tags/release/candidates/v1.0` reference
1192                (
1193                    pattern(qualified_pattern!("refs/tags/release/candidates/*")),
1194                    Rule::new(Allowed::Delegates, 2),
1195                ),
1196            ],
1197            &mut || resolve_from_doc(&doc.clone().verified().unwrap()),
1198        )
1199        .unwrap();
1200
1201        // All tags should succeed at getting their canonical commit other than the
1202        // candidates tag.
1203        let stored = storage.repository(rid).unwrap();
1204        let failing = git::fmt::Qualified::from(git::fmt::lit::refs_tags(failing_tag));
1205        for (refname, oid) in tags.into_iter() {
1206            let canonical = rules
1207                .canonical(refname.clone(), &stored)
1208                .unwrap_or_else(|| {
1209                    panic!("there should be a matching rule for {refname}, rules: {rules:#?}")
1210                });
1211            if refname == failing {
1212                assert!(canonical.find_objects().unwrap().quorum().is_err());
1213            } else {
1214                assert_eq!(
1215                    canonical
1216                        .find_objects()
1217                        .unwrap()
1218                        .quorum()
1219                        .unwrap_or_else(|e| panic!("quorum error for {refname}: {e}")),
1220                    canonical::Quorum {
1221                        refname,
1222                        object: canonical::Object::Tag { id: oid },
1223                    }
1224                )
1225            }
1226        }
1227    }
1228
1229    #[test]
1230    fn test_special_branches() {
1231        assert!(Pattern::try_from((*IDENTITY_BRANCH).clone()).is_err());
1232        assert!(Pattern::try_from((*SIGREFS_BRANCH).clone()).is_err());
1233        assert!(Pattern::try_from((*SIGREFS_PARENT).clone()).is_err());
1234        assert!(Pattern::try_from((*IDENTITY_ROOT).clone()).is_err());
1235    }
1236}