1#![allow(clippy::doc_overindented_list_items)]
3
4use 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
34trait Sealed {}
39impl Sealed for Allowed {}
40impl Sealed for usize {}
41
42#[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 pub fn matches(&self, refname: &Qualified) -> bool {
86 let spec = match self.0.as_str().split_once(ASTERISK) {
94 None => self.0.to_string(),
95 Some((prefix, "")) => {
97 let mut spec = prefix.to_string();
98 spec.push_str("**/*");
99 spec
100 }
101 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
125impl 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 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 (Some(_), None) => ComponentOrdering::MatchLength(Ordering::Greater),
224 (None, Some(_)) => ComponentOrdering::MatchLength(Ordering::Less),
226 (Some(li), Some(ri)) => {
227 if li != ri {
228 ComponentOrdering::MatchLength(li.cmp(&ri).reverse())
230 } else if l.len() != r.len() {
231 ComponentOrdering::MatchLength(l.len().cmp(&r.len()).reverse())
233 } else {
234 ComponentOrdering::Lexicographic(l.cmp(r))
236 }
237 }
238 (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, (Some(_), None) => return Ordering::Less, (Some(lhs), Some(rhs)) => {
251 result.merge(cmp_component(lhs, rhs));
252 }
253 (None, None) => return result.into(),
254 }
255 }
256 }
257}
258
259pub type RawRule = Rule<Allowed, usize>;
263
264impl RawRule {
265 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#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
299pub struct RawRules {
300 #[serde(flatten)]
305 pub rules: BTreeMap<Pattern, RawRule>,
306}
307
308impl RawRules {
309 pub fn iter(&self) -> impl Iterator<Item = (&Pattern, &RawRule)> {
312 self.rules.iter()
313 }
314
315 pub fn insert(&mut self, pattern: Pattern, rule: RawRule) -> Option<RawRule> {
319 self.rules.insert(pattern, rule)
320 }
321
322 pub fn remove(&mut self, pattern: &Pattern) -> Option<RawRule> {
326 self.rules.remove(pattern)
327 }
328
329 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 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
377pub type ValidRule = Rule<ResolvedDelegates, doc::Threshold>;
385
386impl ValidRule {
387 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 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#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
439pub enum Allowed {
440 #[serde(rename = "delegates")]
442 #[default]
443 Delegates,
444 #[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#[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#[derive(Debug)]
510pub struct MatchedRule<'a> {
511 refname: Qualified<'a>,
512 rule: ValidRule,
513}
514
515impl MatchedRule<'_> {
516 pub fn refname(&self) -> &Qualified<'_> {
518 &self.refname
519 }
520
521 pub fn rule(&self) -> &ValidRule {
523 &self.rule
524 }
525
526 pub fn allowed(&self) -> &doc::Delegates {
528 self.rule().allowed()
529 }
530
531 pub fn threshold(&self) -> &doc::Threshold {
533 self.rule().threshold()
534 }
535}
536
537#[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 pub fn iter(&self) -> impl Iterator<Item = (&Pattern, &ValidRule)> {
597 self.rules.iter()
598 }
599
600 pub fn is_empty(&self) -> bool {
602 self.rules.is_empty()
603 }
604
605 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 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 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#[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 allow: D,
669 threshold: T,
672
673 #[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 pub fn new(allow: D, threshold: T) -> Self {
683 Self {
684 allow,
685 threshold,
686 extensions: json::Map::new(),
687 }
688 }
689
690 pub fn allowed(&self) -> &D {
692 &self.allow
693 }
694
695 pub fn threshold(&self) -> &T {
697 &self.threshold
698 }
699
700 pub fn extensions(&self) -> &json::Map<String, json::Value> {
702 &self.extensions
703 }
704
705 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 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 assert!(pattern02 == pattern02);
961 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 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 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 doc.delegate(contributor.public_key().into());
1132
1133 let failing_tag = git::fmt::refname!("release/candidates/v1.0");
1137 let tags = [
1138 git::fmt::refname!("v1.0"),
1140 git::fmt::refname!("release/v1.0"),
1142 failing_tag.clone(),
1143 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 (
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 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}