radicle/crefs/protect: Module for protected refs
Separate module for the concern of protecting `refs/rad`. Note that this also fixes a bug, as previously all refs *starting with* `refs/rad`, such as `refs/radieschen` were protected.
This commit is contained in:
parent
e822623dc1
commit
ef101d9ab9
|
|
@ -352,8 +352,11 @@ fn set_canonical_refs(
|
|||
let identity = repo.identity()?;
|
||||
let rules = identity
|
||||
.canonical_refs_or_default(|| {
|
||||
let rule = identity.doc().default_branch_rule()?;
|
||||
Ok::<_, CanonicalRefsError>(CanonicalRefs::from_iter([rule]))
|
||||
identity
|
||||
.doc()
|
||||
.default_branch_rule()
|
||||
.map(CanonicalRefs::new)
|
||||
.map_err(CanonicalRefsError::from)
|
||||
})?
|
||||
.rules()
|
||||
.clone();
|
||||
|
|
|
|||
|
|
@ -354,8 +354,9 @@ pub(super) fn run(
|
|||
PushAction::PushRef { dst } => {
|
||||
let identity = stored.identity()?;
|
||||
let crefs = identity.canonical_refs_or_default(|| {
|
||||
let rule = identity.doc().default_branch_rule()?;
|
||||
Ok::<_, CanonicalRefsError>(CanonicalRefs::from_iter([rule]))
|
||||
Ok::<_, CanonicalRefsError>(CanonicalRefs::new(
|
||||
identity.doc().default_branch_rule()?,
|
||||
))
|
||||
})?;
|
||||
let rules = crefs.rules();
|
||||
let me = Did::from(nid);
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use quorum::{CommitQuorum, CommitQuorumFailure, TagQuorum, TagQuorumFailure};
|
|||
mod voting;
|
||||
|
||||
pub mod effects;
|
||||
pub mod protect;
|
||||
pub mod rules;
|
||||
|
||||
pub use rules::{MatchedRule, RawRule, Rules, ValidRule};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
//! Some reference names are protected and cannot be used with canonical
|
||||
//! references. This module contains checks for these cases.
|
||||
//!
|
||||
//! Protected references are:
|
||||
//! 1. `refs/rad`
|
||||
//! 2. Any reference matching `refs/rad/*`, e.g. `refs/rad/id`, `refs/rad/foo/bar`.
|
||||
|
||||
const REFS_RAD: &str = "refs/rad";
|
||||
|
||||
/// Reference-like types, which we encounter when working with canonical references.
|
||||
pub(crate) trait RefLike: AsRef<str> + Ord + std::fmt::Display + serde::Serialize {}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("reference-like string '{REFS_RAD}' is protected")]
|
||||
RefsRad,
|
||||
#[error("reference-like string '{reflike}' is protected because it starts with '{REFS_RAD}/'")]
|
||||
RefsRadChild { reflike: String },
|
||||
}
|
||||
|
||||
/// A witnesses that the inner reference-like value is not protected.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
|
||||
#[repr(transparent)]
|
||||
#[serde(transparent)]
|
||||
pub(super) struct Unprotected<T: RefLike>(T);
|
||||
|
||||
impl<T: RefLike> Unprotected<T> {
|
||||
pub fn new(reflike: T) -> Result<Self, Error> {
|
||||
match reflike
|
||||
.as_ref()
|
||||
.strip_prefix(REFS_RAD)
|
||||
.map(|rest| rest.get(..1))
|
||||
{
|
||||
Some(None) => Err(Error::RefsRad),
|
||||
Some(Some("/")) => Err(Error::RefsRadChild {
|
||||
reflike: reflike.to_string(),
|
||||
}),
|
||||
Some(_) | None => Ok(Self(reflike)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: RefLike> AsRef<T> for Unprotected<T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de, T: RefLike + serde::Deserialize<'de>> serde::Deserialize<'de> for Unprotected<T> {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
Self::new(T::deserialize(deserializer)?).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: RefLike + std::fmt::Display> std::fmt::Display for Unprotected<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// For types that are commonly used in conjunction with [`Unprotected`]
|
||||
/// have some `impl`s and companion infallible injections.
|
||||
mod impls {
|
||||
use crate::git::fmt::{RefString, refspec::QualifiedPattern};
|
||||
|
||||
use super::*;
|
||||
|
||||
/// [`RefString`] models reference names, thus the prototype of what it
|
||||
/// means to be [`RefLike`].
|
||||
impl RefLike for RefString {}
|
||||
|
||||
/// A [`QualifiedPattern`] is [`RefLike`] in the sense that it matches a
|
||||
/// (possibly infinite) set of [`crate::git::Qualified`].
|
||||
impl RefLike for QualifiedPattern<'_> {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use crate::assert_matches;
|
||||
use crate::git::fmt::refname;
|
||||
|
||||
use super::{Error::*, Unprotected};
|
||||
|
||||
#[test]
|
||||
fn refs_rad() {
|
||||
assert_matches!(Unprotected::new(refname!("refs/rad")), Err(RefsRad))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refs_rad_id() {
|
||||
assert_matches!(
|
||||
Unprotected::new(refname!("refs/rad/id")),
|
||||
Err(RefsRadChild { .. })
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refs_radieschen() {
|
||||
assert_matches!(Unprotected::new(refname!("refs/radieschen")), Ok(_))
|
||||
}
|
||||
}
|
||||
|
|
@ -9,10 +9,8 @@
|
|||
//! the first matched rule, and this can be used to calculate the
|
||||
//! [`Canonical::quorum`].
|
||||
|
||||
use core::fmt;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -24,12 +22,12 @@ use crate::git::canonical;
|
|||
use crate::git::canonical::Canonical;
|
||||
use crate::git::fmt::Qualified;
|
||||
use crate::git::fmt::refspec::QualifiedPattern;
|
||||
use crate::git::fmt::{RefString, refname};
|
||||
use crate::identity::{Did, doc};
|
||||
|
||||
const ASTERISK: char = '*';
|
||||
use super::protect;
|
||||
use super::protect::Unprotected;
|
||||
|
||||
static REFS_RAD: LazyLock<RefString> = LazyLock::new(|| refname!("refs/rad"));
|
||||
const ASTERISK: char = '*';
|
||||
|
||||
/// Private trait to ensure that not any `Rule` can be deserialized.
|
||||
/// Implementations are provided for `Allowed` and `usize` so that `RawRule`s
|
||||
|
|
@ -39,94 +37,39 @@ trait Sealed {}
|
|||
impl Sealed for Allowed {}
|
||||
impl Sealed for usize {}
|
||||
|
||||
/// A `Pattern` is a `QualifiedPattern` reference, however, it disallows any
|
||||
/// references under the `refs/rad` hierarchy.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(into = "QualifiedPattern", try_from = "QualifiedPattern")]
|
||||
pub struct Pattern(QualifiedPattern<'static>);
|
||||
/// A pattern for a rule is an unprotected qualified reference pattern.
|
||||
/// Requiring [`Unprotected`] makes rules that would create protected references
|
||||
/// unrepresentable.
|
||||
pub(super) type Pattern = Unprotected<QualifiedPattern<'static>>;
|
||||
|
||||
impl fmt::Display for Pattern {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.0.as_str())
|
||||
}
|
||||
}
|
||||
pub type RawPattern = QualifiedPattern<'static>;
|
||||
|
||||
impl From<Pattern> for QualifiedPattern<'static> {
|
||||
fn from(Pattern(pattern): Pattern) -> Self {
|
||||
pattern
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<QualifiedPattern<'a>> for Pattern {
|
||||
type Error = PatternError;
|
||||
|
||||
fn try_from(pattern: QualifiedPattern<'a>) -> Result<Self, Self::Error> {
|
||||
if pattern.starts_with(REFS_RAD.as_str()) {
|
||||
Err(PatternError::ProtectedRef {
|
||||
prefix: (*REFS_RAD).clone(),
|
||||
pattern: pattern.to_owned(),
|
||||
})
|
||||
} else {
|
||||
Ok(Self(pattern.to_owned()))
|
||||
/// Check if the `pattern` matches the `refname`.
|
||||
fn matches(pattern: &RawPattern, refname: &Qualified) -> bool {
|
||||
// N.b. Git's refspecs do not quite match with glob-star semantics. A
|
||||
// single `*` in a refspec is expected to match all references under
|
||||
// that namespace, even if they are further down the hierarchy.
|
||||
// Thus, the following rules are applied:
|
||||
//
|
||||
// - a trailing `*` changes to `**/*`
|
||||
// - a `*` in between path components changes to `**`
|
||||
let spec = match pattern.as_str().split_once(ASTERISK) {
|
||||
None => pattern.to_string(),
|
||||
// Expand `refs/tags/*` to `refs/tags/**/*`
|
||||
Some((prefix, "")) => {
|
||||
let mut spec = prefix.to_string();
|
||||
spec.push_str("**/*");
|
||||
spec
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<Qualified<'a>> for Pattern {
|
||||
type Error = PatternError;
|
||||
|
||||
fn try_from(name: Qualified<'a>) -> Result<Self, Self::Error> {
|
||||
Self::try_from(QualifiedPattern::from(name))
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
/// Construct a [`Pattern`] that matches a branch exactly.
|
||||
///
|
||||
/// The resulting [`Pattern`] will match `refs/heads/<name>`.
|
||||
pub fn refs_heads_exact(name: &git::fmt::RefStr) -> Self {
|
||||
Self(QualifiedPattern::from(git::refs::branch(name)))
|
||||
}
|
||||
|
||||
/// Check if the `refname` matches the rule's `refspec`.
|
||||
pub fn matches(&self, refname: &Qualified) -> bool {
|
||||
// N.b. Git's refspecs do not quite match with glob-star semantics. A
|
||||
// single `*` in a refspec is expected to match all references under
|
||||
// that namespace, even if they are further down the hierarchy.
|
||||
// Thus, the following rules are applied:
|
||||
//
|
||||
// - a trailing `*` changes to `**/*`
|
||||
// - a `*` in between path components changes to `**`
|
||||
let spec = match self.0.as_str().split_once(ASTERISK) {
|
||||
None => self.0.to_string(),
|
||||
// Expand `refs/tags/*` to `refs/tags/**/*`
|
||||
Some((prefix, "")) => {
|
||||
let mut spec = prefix.to_string();
|
||||
spec.push_str("**/*");
|
||||
spec
|
||||
}
|
||||
// Expand `refs/tags/*/v1.0` to `refs/tags/**/v1.0`
|
||||
Some((prefix, suffix)) => {
|
||||
let mut spec = prefix.to_string();
|
||||
spec.push_str("**");
|
||||
spec.push_str(suffix);
|
||||
spec
|
||||
}
|
||||
};
|
||||
fast_glob::glob_match(&spec, refname.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<QualifiedPattern<'static>> for Pattern {
|
||||
fn as_ref(&self) -> &QualifiedPattern<'static> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Pattern {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
// Expand `refs/tags/*/v1.0` to `refs/tags/**/v1.0`
|
||||
Some((prefix, suffix)) => {
|
||||
let mut spec = prefix.to_string();
|
||||
spec.push_str("**");
|
||||
spec.push_str(suffix);
|
||||
spec
|
||||
}
|
||||
};
|
||||
fast_glob::glob_match(&spec, refname.as_str())
|
||||
}
|
||||
|
||||
/// Patterns are ordered by their specificity.
|
||||
|
|
@ -248,8 +191,8 @@ impl Ord for Pattern {
|
|||
}
|
||||
|
||||
let mut result = ComponentOrdering::default();
|
||||
let mut lhs = self.0.components();
|
||||
let mut rhs = other.0.components();
|
||||
let mut lhs = self.as_ref().components();
|
||||
let mut rhs = other.as_ref().components();
|
||||
loop {
|
||||
match (lhs.next(), rhs.next()) {
|
||||
(None, Some(_)) => return Ordering::Greater, // (1.)
|
||||
|
|
@ -263,6 +206,12 @@ impl Ord for Pattern {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Pattern {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Rule`] that can be serialized and deserialized safely.
|
||||
///
|
||||
/// Should be converted to a [`ValidRule`] via [`Rule::validate`].
|
||||
|
|
@ -309,27 +258,27 @@ pub struct RawRules {
|
|||
/// Note that this can be a fully-qualified pattern, e.g. `refs/heads/qa`,
|
||||
/// as well as a wild-card pattern, e.g. `refs/tags/*`.
|
||||
#[serde(flatten)]
|
||||
pub rules: BTreeMap<Pattern, RawRule>,
|
||||
pub rules: BTreeMap<RawPattern, RawRule>,
|
||||
}
|
||||
|
||||
impl RawRules {
|
||||
/// Returns an iterator over the [`Pattern`] and [`RawRule`] in the set of
|
||||
/// rules.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&Pattern, &RawRule)> {
|
||||
/// Returns an iterator over the [`RawPattern`] and [`RawRule`]
|
||||
/// in the set of rules.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&RawPattern, &RawRule)> {
|
||||
self.rules.iter()
|
||||
}
|
||||
|
||||
/// Add a new [`RawRule`] to the set of rules.
|
||||
///
|
||||
/// Returns the replaced rule, if it existed.
|
||||
pub fn insert(&mut self, pattern: Pattern, rule: RawRule) -> Option<RawRule> {
|
||||
pub fn insert(&mut self, pattern: RawPattern, rule: RawRule) -> Option<RawRule> {
|
||||
self.rules.insert(pattern, rule)
|
||||
}
|
||||
|
||||
/// Remove the rule that matches the `pattern` parameter.
|
||||
///
|
||||
/// Returns the rule if it existed.
|
||||
pub fn remove(&mut self, pattern: &Pattern) -> Option<RawRule> {
|
||||
pub fn remove(&mut self, pattern: &RawPattern) -> Option<RawRule> {
|
||||
self.rules.remove(pattern)
|
||||
}
|
||||
|
||||
|
|
@ -338,7 +287,7 @@ impl RawRules {
|
|||
let refname = refname.as_str();
|
||||
self.rules
|
||||
.iter()
|
||||
.any(|(pattern, _)| pattern.0.as_str() == refname)
|
||||
.any(|(pattern, _)| pattern.as_str() == refname)
|
||||
}
|
||||
|
||||
/// Check if the `refname` matches any existing rules, including glob
|
||||
|
|
@ -346,35 +295,35 @@ impl RawRules {
|
|||
pub fn matches<'a, 'b>(
|
||||
&self,
|
||||
refname: &Qualified<'b>,
|
||||
) -> impl Iterator<Item = (&Pattern, &RawRule)> + use<'a, '_, 'b> {
|
||||
) -> impl Iterator<Item = (&RawPattern, &RawRule)> + use<'a, '_, 'b> {
|
||||
let refname = refname.clone();
|
||||
self.rules
|
||||
.iter()
|
||||
.filter(move |(pattern, _)| pattern.matches(&refname))
|
||||
.filter(move |(pattern, _)| matches(pattern, &refname))
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<(Pattern, RawRule)> for RawRules {
|
||||
fn extend<T: IntoIterator<Item = (Pattern, RawRule)>>(&mut self, iter: T) {
|
||||
impl Extend<(RawPattern, RawRule)> for RawRules {
|
||||
fn extend<T: IntoIterator<Item = (RawPattern, RawRule)>>(&mut self, iter: T) {
|
||||
self.rules.extend(iter)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BTreeMap<Pattern, RawRule>> for RawRules {
|
||||
fn from(rules: BTreeMap<Pattern, RawRule>) -> Self {
|
||||
impl From<BTreeMap<RawPattern, RawRule>> for RawRules {
|
||||
fn from(rules: BTreeMap<RawPattern, RawRule>) -> Self {
|
||||
RawRules { rules }
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(Pattern, RawRule)> for RawRules {
|
||||
fn from_iter<T: IntoIterator<Item = (Pattern, RawRule)>>(iter: T) -> Self {
|
||||
impl FromIterator<(RawPattern, RawRule)> for RawRules {
|
||||
fn from_iter<T: IntoIterator<Item = (RawPattern, RawRule)>>(iter: T) -> Self {
|
||||
iter.into_iter().collect::<BTreeMap<_, _>>().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for RawRules {
|
||||
type Item = (Pattern, RawRule);
|
||||
type IntoIter = std::collections::btree_map::IntoIter<Pattern, RawRule>;
|
||||
type Item = (RawPattern, RawRule);
|
||||
type IntoIter = std::collections::btree_map::IntoIter<RawPattern, RawRule>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.rules.into_iter()
|
||||
|
|
@ -390,40 +339,6 @@ impl IntoIterator for RawRules {
|
|||
/// document and validated.
|
||||
pub type ValidRule = Rule<ResolvedDelegates, doc::Threshold>;
|
||||
|
||||
impl ValidRule {
|
||||
/// Initialize a `ValidRule` for the default branch, given by `name`. The
|
||||
/// rule will contain the single `did` as the allowed DID, and use a
|
||||
/// threshold of `1`.
|
||||
///
|
||||
/// Note that the serialization of the rule will use the `delegates` token
|
||||
/// for the rule. E.g.
|
||||
/// ```json
|
||||
/// {
|
||||
/// "pattern": "refs/heads/main",
|
||||
/// "allow": ["did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"],
|
||||
/// "threshold": 1
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If the `name` reference begins with `refs/rad`.
|
||||
pub fn default_branch(
|
||||
did: Did,
|
||||
name: &git::fmt::RefStr,
|
||||
) -> Result<(Pattern, Self), PatternError> {
|
||||
let pattern = Pattern::try_from(git::refs::branch(name).to_owned())?;
|
||||
let rule = Self {
|
||||
allow: ResolvedDelegates::Delegates(doc::Delegates::from(did)),
|
||||
// N.B. this needs to be the minimum since we only have one
|
||||
// delegate.
|
||||
threshold: doc::Threshold::MIN,
|
||||
extensions: json::Map::new(),
|
||||
};
|
||||
Ok((pattern, rule))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ValidRule> for RawRule {
|
||||
fn from(rule: ValidRule) -> Self {
|
||||
let Rule {
|
||||
|
|
@ -546,7 +461,7 @@ impl MatchedRule<'_> {
|
|||
/// cannot be duplicated.
|
||||
///
|
||||
/// To construct the set of rules, use [`Rules::from_raw`], which validates a
|
||||
/// set of [`RawRule`]s, and their [`Pattern`] references, into a set of
|
||||
/// set of [`RawRule`]s, and their [`RawPattern`] references, into a set of
|
||||
/// [`ValidRule`]s.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct Rules {
|
||||
|
|
@ -562,48 +477,18 @@ impl FromIterator<(Pattern, ValidRule)> for Rules {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Rules {
|
||||
type Item = (&'a Pattern, &'a ValidRule);
|
||||
type IntoIter = std::collections::btree_map::Iter<'a, Pattern, ValidRule>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.rules.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Rules {
|
||||
type Item = (Pattern, ValidRule);
|
||||
type IntoIter = std::collections::btree_map::IntoIter<Pattern, ValidRule>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.rules.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<(Pattern, ValidRule)> for Rules {
|
||||
fn extend<T: IntoIterator<Item = (Pattern, ValidRule)>>(&mut self, iter: T) {
|
||||
self.rules.extend(iter)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rules> for RawRules {
|
||||
fn from(Rules { rules }: Rules) -> Self {
|
||||
Self {
|
||||
rules: rules
|
||||
.into_iter()
|
||||
.map(|(pattern, rule)| (pattern, rule.into()))
|
||||
.map(|(pattern, rule)| (pattern.into_inner(), rule.into()))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Rules {
|
||||
/// Returns an iterator over the [`Pattern`] and [`ValidRule`] in the set of
|
||||
/// rules.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&Pattern, &ValidRule)> {
|
||||
self.rules.iter()
|
||||
}
|
||||
|
||||
/// Returns `true` is the set of rules is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.rules.is_empty()
|
||||
|
|
@ -611,7 +496,7 @@ impl Rules {
|
|||
|
||||
/// Construct a set of `Rules` given a set of `RawRule`s.
|
||||
pub fn from_raw<R>(
|
||||
rules: impl IntoIterator<Item = (Pattern, RawRule)>,
|
||||
rules: impl IntoIterator<Item = (RawPattern, RawRule)>,
|
||||
resolve: &mut R,
|
||||
) -> Result<Self, ValidationError>
|
||||
where
|
||||
|
|
@ -619,7 +504,10 @@ impl Rules {
|
|||
{
|
||||
let valid = rules
|
||||
.into_iter()
|
||||
.map(|(pattern, rule)| rule.validate(resolve).map(|rule| (pattern, rule)))
|
||||
.map(|(pattern, rule)| {
|
||||
let pattern = Unprotected::new(pattern)?;
|
||||
rule.validate(resolve).map(|rule| (pattern, rule))
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
Ok(Self { rules: valid })
|
||||
}
|
||||
|
|
@ -628,11 +516,12 @@ impl Rules {
|
|||
pub fn matches<'a>(
|
||||
&self,
|
||||
refname: &Qualified<'a>,
|
||||
) -> impl Iterator<Item = (&Pattern, &ValidRule)> + use<'a, '_> {
|
||||
) -> impl Iterator<Item = (&RawPattern, &ValidRule)> + use<'a, '_> {
|
||||
let refname_cloned = refname.clone();
|
||||
self.rules
|
||||
.iter()
|
||||
.filter(move |(pattern, _)| pattern.matches(&refname_cloned))
|
||||
.filter(move |(pattern, _)| matches(pattern.as_ref(), &refname_cloned))
|
||||
.map(|(pattern, rule)| (pattern.as_ref(), rule))
|
||||
}
|
||||
|
||||
/// Match given refname, take the most specific rule, and prepare evaluation
|
||||
|
|
@ -657,7 +546,7 @@ impl Rules {
|
|||
}
|
||||
|
||||
/// A `Rule` defines how a reference or set of references can be made canonical,
|
||||
/// i.e. have a top-level `refs/*` entry – see [`Pattern`].
|
||||
/// i.e. have a top-level `refs/*` entry – see [`RawPattern`].
|
||||
///
|
||||
/// The [`Rule::allowed`] type is generic to allow for [`Allowed`] to be used
|
||||
/// for serialization and deserialization, however, the use of
|
||||
|
|
@ -720,23 +609,14 @@ impl<D, T> Rule<D, T> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PatternError {
|
||||
#[error("cannot create rule for '{pattern}' since references under '{prefix}' are protected")]
|
||||
ProtectedRef {
|
||||
prefix: RefString,
|
||||
pattern: QualifiedPattern<'static>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ValidationError {
|
||||
#[error(transparent)]
|
||||
Threshold(#[from] doc::ThresholdError),
|
||||
#[error(transparent)]
|
||||
Delegates(#[from] doc::DelegatesError),
|
||||
#[error("cannot create rule for reserved `rad` references '{pattern}'")]
|
||||
RadRef { pattern: QualifiedPattern<'static> },
|
||||
#[error(transparent)]
|
||||
Protected(#[from] protect::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
@ -780,8 +660,12 @@ mod tests {
|
|||
s.parse().unwrap()
|
||||
}
|
||||
|
||||
fn raw_pattern(qp: QualifiedPattern<'static>) -> RawPattern {
|
||||
qp
|
||||
}
|
||||
|
||||
fn pattern(qp: QualifiedPattern<'static>) -> Pattern {
|
||||
Pattern::try_from(qp).unwrap()
|
||||
Pattern::new(qp).unwrap()
|
||||
}
|
||||
|
||||
fn resolve_from_doc(doc: &Doc) -> doc::Delegates {
|
||||
|
|
@ -857,7 +741,7 @@ mod tests {
|
|||
"#;
|
||||
let expected = [
|
||||
(
|
||||
pattern(qualified_pattern!("refs/heads/main")),
|
||||
raw_pattern(qualified_pattern!("refs/heads/main")),
|
||||
Rule::new(
|
||||
Allowed::Set(nonempty![
|
||||
did("did:key:z6MkpQTLwr8QyADGmBGAMsGttvWzP4PojUMs4hREZW5T5E3K"),
|
||||
|
|
@ -867,7 +751,7 @@ mod tests {
|
|||
),
|
||||
),
|
||||
(
|
||||
pattern(qualified_pattern!("refs/tags/releases/*")),
|
||||
raw_pattern(qualified_pattern!("refs/tags/releases/*")),
|
||||
Rule::new(
|
||||
Allowed::Set(nonempty![
|
||||
did("did:key:z6MknLWe8A7UJxvTfY36JcB8XrP1KTLb5HFTX38hEmdY3b56"),
|
||||
|
|
@ -878,7 +762,7 @@ mod tests {
|
|||
),
|
||||
),
|
||||
(
|
||||
pattern(qualified_pattern!("refs/heads/development")),
|
||||
raw_pattern(qualified_pattern!("refs/heads/development")),
|
||||
Rule::new(
|
||||
Allowed::Set(nonempty![did(
|
||||
"did:key:z6MkhH7ENYE62JAjTiRZPU71MGZ6xCwnbyHHWfrBu3fr6PVG"
|
||||
|
|
@ -887,13 +771,13 @@ mod tests {
|
|||
),
|
||||
),
|
||||
(
|
||||
pattern(qualified_pattern!("refs/heads/release/*")),
|
||||
raw_pattern(qualified_pattern!("refs/heads/release/*")),
|
||||
Rule::new(Allowed::Delegates, 1),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<RawRules>();
|
||||
let rules = serde_json::from_str::<BTreeMap<Pattern, RawRule>>(examples)
|
||||
let rules = serde_json::from_str::<BTreeMap<RawPattern, RawRule>>(examples)
|
||||
.unwrap()
|
||||
.into();
|
||||
assert_eq!(expected, rules)
|
||||
|
|
@ -1093,9 +977,12 @@ mod tests {
|
|||
|
||||
// Duplicate rules are overwritten
|
||||
let rules = vec![
|
||||
(pattern.clone(), Rule::new(Allowed::Delegates, 1)),
|
||||
(
|
||||
pattern.clone(),
|
||||
pattern.clone().into_inner(),
|
||||
Rule::new(Allowed::Delegates, 1),
|
||||
),
|
||||
(
|
||||
pattern.clone().into_inner(),
|
||||
Rule::new(doc.delegates().as_ref().clone().into(), 1),
|
||||
),
|
||||
];
|
||||
|
|
@ -1188,18 +1075,18 @@ mod tests {
|
|||
let rules = Rules::from_raw(
|
||||
[
|
||||
(
|
||||
pattern(qualified_pattern!("refs/tags/*")),
|
||||
raw_pattern(qualified_pattern!("refs/tags/*")),
|
||||
Rule::new(Allowed::Delegates, 1),
|
||||
),
|
||||
(
|
||||
pattern(qualified_pattern!("refs/tags/release/*")),
|
||||
raw_pattern(qualified_pattern!("refs/tags/release/*")),
|
||||
Rule::new(Allowed::Delegates, 1),
|
||||
),
|
||||
// Ensure that none of the other rules apply by ensuring we need
|
||||
// both delegates to get the quorum of the
|
||||
// `refs/tags/release/candidates/v1.0` reference
|
||||
(
|
||||
pattern(qualified_pattern!("refs/tags/release/candidates/*")),
|
||||
raw_pattern(qualified_pattern!("refs/tags/release/candidates/*")),
|
||||
Rule::new(Allowed::Delegates, 2),
|
||||
),
|
||||
],
|
||||
|
|
@ -1237,9 +1124,9 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_special_branches() {
|
||||
assert!(Pattern::try_from((*IDENTITY_BRANCH).clone()).is_err());
|
||||
assert!(Pattern::try_from((*SIGREFS_BRANCH).clone()).is_err());
|
||||
assert!(Pattern::try_from((*SIGREFS_PARENT).clone()).is_err());
|
||||
assert!(Pattern::try_from((*IDENTITY_ROOT).clone()).is_err());
|
||||
assert!(Pattern::new((*IDENTITY_BRANCH).clone().into()).is_err());
|
||||
assert!(Pattern::new((*SIGREFS_BRANCH).clone().into()).is_err());
|
||||
assert!(Pattern::new((*SIGREFS_PARENT).clone().into()).is_err());
|
||||
assert!(Pattern::new((*IDENTITY_ROOT).clone().into()).is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::git::canonical::{
|
||||
ValidRule,
|
||||
rules::{self, RawRules, Rules, ValidationError},
|
||||
};
|
||||
use crate::git::canonical::rules::{RawRules, Rules, ValidationError};
|
||||
|
||||
use super::doc::{Delegates, Payload};
|
||||
|
||||
|
|
@ -95,18 +92,6 @@ impl CanonicalRefs {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(rules::Pattern, ValidRule)> for CanonicalRefs {
|
||||
fn from_iter<T: IntoIterator<Item = (rules::Pattern, ValidRule)>>(iter: T) -> Self {
|
||||
Self::new(Rules::from_iter(iter))
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<(rules::Pattern, ValidRule)> for CanonicalRefs {
|
||||
fn extend<T: IntoIterator<Item = (rules::Pattern, ValidRule)>>(&mut self, iter: T) {
|
||||
self.rules.extend(iter)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum CanonicalRefsPayloadError {
|
||||
|
|
|
|||
|
|
@ -741,15 +741,19 @@ impl Doc {
|
|||
Ok(git::refs::branch(self.project()?.default_branch()))
|
||||
}
|
||||
|
||||
pub fn default_branch_rule(
|
||||
&self,
|
||||
) -> Result<(rules::Pattern, rules::ValidRule), DefaultBranchRuleError> {
|
||||
let pattern = rules::Pattern::refs_heads_exact(self.project()?.default_branch());
|
||||
pub fn default_branch_rule(&self) -> Result<rules::Rules, DefaultBranchRuleError> {
|
||||
let pattern = git::fmt::refspec::QualifiedPattern::from(git::refs::branch(
|
||||
self.project()?.default_branch(),
|
||||
));
|
||||
let rule = rules::Rule::new(
|
||||
rules::ResolvedDelegates::Delegates(self.delegates.clone()),
|
||||
self.threshold,
|
||||
);
|
||||
Ok((pattern, rule))
|
||||
Ok(rules::Rules::from_raw(
|
||||
rules::RawRules::from_iter([(pattern, rule.into())]),
|
||||
&mut || self.delegates.clone(),
|
||||
)
|
||||
.expect("default rules are valid"))
|
||||
}
|
||||
|
||||
/// Return the associated [`Visibility`] of this document.
|
||||
|
|
@ -923,21 +927,20 @@ impl crefs::GetCanonicalRefs for Doc {
|
|||
type Error = CanonicalRefsError;
|
||||
|
||||
fn canonical_refs(&self) -> Result<Option<CanonicalRefs>, Self::Error> {
|
||||
self.raw_canonical_refs().and_then(|raw| {
|
||||
raw.map(|raw| {
|
||||
raw.try_into_canonical_refs(&mut || self.delegates.clone())
|
||||
.map_err(CanonicalRefsError::from)
|
||||
.and_then(|mut crefs| {
|
||||
self.default_branch_rule()
|
||||
.map_err(CanonicalRefsError::from)
|
||||
.map(|rule| {
|
||||
crefs.extend([rule]);
|
||||
crefs
|
||||
})
|
||||
})
|
||||
})
|
||||
.transpose()
|
||||
})
|
||||
let Some(raw_crefs) = self.raw_canonical_refs()? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut raw_rules = raw_crefs.raw_rules().clone();
|
||||
|
||||
let default_branch_rule = self.default_branch_rule()?;
|
||||
raw_rules.extend(rules::RawRules::from(default_branch_rule));
|
||||
|
||||
let raw_crefs = RawCanonicalRefs::new(raw_rules);
|
||||
|
||||
Ok(Some(
|
||||
raw_crefs.try_into_canonical_refs(&mut || self.delegates.clone())?,
|
||||
))
|
||||
}
|
||||
|
||||
fn raw_canonical_refs(&self) -> Result<Option<RawCanonicalRefs>, Self::Error> {
|
||||
|
|
|
|||
|
|
@ -854,12 +854,13 @@ impl ReadRepository for Repository {
|
|||
fn canonical_head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError> {
|
||||
let doc = self.identity_doc()?;
|
||||
let refname = doc.default_branch()?.to_owned();
|
||||
let crefs = match doc.canonical_refs()? {
|
||||
Some(crefs) => crefs,
|
||||
// Fallback to constructing the default branch via the project
|
||||
// payload
|
||||
None => CanonicalRefs::from_iter([doc.default_branch_rule()?]),
|
||||
};
|
||||
|
||||
let crefs = doc.canonical_refs_or_default(|| {
|
||||
doc.default_branch_rule()
|
||||
.map_err(RepositoryError::from)
|
||||
.map(CanonicalRefs::new)
|
||||
})?;
|
||||
|
||||
Ok(crefs
|
||||
.rules()
|
||||
.canonical(refname, self)
|
||||
|
|
|
|||
Loading…
Reference in New Issue