Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle/crefs/protect: Module for protected refs
Lorenz Leutgeb committed 7 months ago
commit fbd95ff62dec916407a9fa3ad67765acf4dffade
parent 6397e8526ec69b51fb66e8e929281c921548937c
3 files changed +102 -81
modified crates/radicle/src/git/canonical.rs
@@ -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};
added crates/radicle/src/git/canonical/protect.rs
@@ -0,0 +1,79 @@
+
//! 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";
+

+
#[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 string is not protected.
+
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize)]
+
#[serde(transparent)]
+
pub struct Unprotected<T>(T);
+

+
impl<T: AsRef<str> + ToString> Unprotected<T> {
+
    /// Check that a reference-like string (reference name, reference pattern)
+
    /// is not protected.
+
    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)),
+
        }
+
    }
+
}
+

+
impl<'de, T: AsRef<str> + ToString + 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: std::fmt::Display> std::fmt::Display for Unprotected<T> {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        self.0.fmt(f)
+
    }
+
}
+

+
impl<T> AsRef<T> for Unprotected<T> {
+
    fn as_ref(&self) -> &T {
+
        &self.0
+
    }
+
}
+

+
/// For types that are commonly used in conjunction with [`Unprotected`]
+
/// have some infallible injections.
+
mod common {
+
    use super::*;
+
    use crate::git::{refs::branch, refspec::QualifiedPattern, RefStr};
+

+
    impl Unprotected<QualifiedPattern<'_>> {
+
        /// Construct a [`Unprotected`] pattern that matches a branch exactly.
+
        ///
+
        /// The resulting [`Unprotected`] pattern will match
+
        /// `refs/heads/<name>`.
+
        pub fn refs_heads_exact(name: &RefStr) -> Self {
+
            Self::new(QualifiedPattern::from(branch(name))).expect("branches are never protected")
+
        }
+
    }
+
}
modified crates/radicle/src/git/canonical/rules.rs
@@ -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};
@@ -22,14 +20,13 @@ use thiserror::Error;
use crate::git;
use crate::git::canonical;
use crate::git::canonical::Canonical;
-
use crate::git::fmt::{refname, RefString};
use crate::git::refspec::QualifiedPattern;
use crate::git::Qualified;
use crate::identity::{doc, Did};

-
const ASTERISK: char = '*';
+
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,57 +36,16 @@ 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>);
-

-
impl fmt::Display for Pattern {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        f.write_str(self.0.as_str())
-
    }
-
}
-

-
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()))
-
        }
-
    }
-
}
-

-
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))
-
    }
-
}
+
/// A pattern for a rule is an unprotected qualified reference pattern.
+
/// Requiring [`Unprotected`] makes rules that would create protected references
+
/// unrepresentable.
+
pub type Pattern = Unprotected<QualifiedPattern<'static>>;

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::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 {
+
        let pattern = self.as_ref();
+

        // 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.
@@ -97,8 +53,8 @@ impl Pattern {
        //
        //   - 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(),
+
        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();
@@ -117,12 +73,6 @@ impl Pattern {
    }
}

-
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))
@@ -248,8 +198,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.)
@@ -338,7 +288,7 @@ impl RawRules {
        let refname = refname.as_str();
        self.rules
            .iter()
-
            .any(|(pattern, _)| pattern.0.as_str() == refname)
+
            .any(|(pattern, _)| pattern.as_ref().as_str() == refname)
    }

    /// Check if the `refname` matches any existing rules, including glob
@@ -404,12 +354,8 @@ impl ValidRule {
    ///   "threshold": 1
    /// }
    /// ```
-
    ///
-
    /// # Errors
-
    ///
-
    /// If the `name` reference begins with `refs/rad`.
-
    pub fn default_branch(did: Did, name: &git::RefStr) -> Result<(Pattern, Self), PatternError> {
-
        let pattern = Pattern::try_from(git::refs::branch(name).to_owned())?;
+
    pub fn default_branch(did: Did, name: &git::RefStr) -> (Pattern, Self) {
+
        let pattern = Pattern::refs_heads_exact(name);
        let rule = Self {
            allow: ResolvedDelegates::Delegates(doc::Delegates::from(did)),
            // N.B. this needs to be the minimum since we only have one
@@ -417,7 +363,7 @@ impl ValidRule {
            threshold: doc::Threshold::MIN,
            extensions: json::Map::new(),
        };
-
        Ok((pattern, rule))
+
        (pattern, rule)
    }
}

@@ -719,11 +665,8 @@ 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>,
-
    },
+
    #[error("cannot create rule: {0}")]
+
    ProtectedRef(#[from] super::protect::Error),
}

#[derive(Debug, Error)]
@@ -732,8 +675,6 @@ pub enum ValidationError {
    Threshold(#[from] doc::ThresholdError),
    #[error(transparent)]
    Delegates(#[from] doc::DelegatesError),
-
    #[error("cannot create rule for reserved `rad` references '{pattern}'")]
-
    RadRef { pattern: QualifiedPattern<'static> },
}

#[derive(Debug, Error)]
@@ -780,7 +721,7 @@ mod tests {
    }

    fn pattern(qp: QualifiedPattern<'static>) -> Pattern {
-
        Pattern::try_from(qp).unwrap()
+
        Pattern::new(qp).unwrap()
    }

    fn resolve_from_doc(doc: &Doc) -> doc::Delegates {
@@ -1228,8 +1169,8 @@ 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((*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((*IDENTITY_ROOT).clone().into()).is_err());
    }
}