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 8c65efec8c08053364c22207dd0b69e3a90bcdcc
parent ab735e5778161853a930eb09a919fadbd43c1407
3 files changed +98 -74
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,73 @@
+
//! Some reference names are protected and cannot be used with canonical
+
//! references. This module contains checks for these cases.
+

+
static REFS_RAD: &str = "refs/rad";
+

+
#[derive(Debug, thiserror::Error)]
+
pub enum Error {
+
    #[error("reference-like string '{reflike}' is protected because it starts with '{REFS_RAD}'")]
+
    RefsRad { reflike: String },
+
}
+

+
/// Check that a reference-like string (reference name, reference pattern)
+
/// is not protected.
+
fn check(reflike: &impl AsRef<str>) -> Result<(), Error> {
+
    let s = reflike.as_ref();
+
    if s.starts_with(REFS_RAD) {
+
        Err(Error::RefsRad {
+
            reflike: s.to_string(),
+
        })
+
    } else {
+
        Ok(())
+
    }
+
}
+

+
/// 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<'de, T: AsRef<str> + 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<str>> Unprotected<T> {
+
    pub fn new(value: T) -> Result<Self, Error> {
+
        check(&value)?;
+
        Ok(Self(value))
+
    }
+
}
+

+
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::{
+
        refname, refs::branch, refspec::QualifiedPattern, Qualified, RefStr, RefString,
+
    };
+

+
    impl Unprotected<QualifiedPattern<'_>> {
+
        /// Construct a qualified reference name pattern that matches a branch
+
        /// exactly, i.e., return `/refs/heads/<name>`.
+
        pub fn branch_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,56 +36,20 @@ 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;
+
// #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+
// #[serde(into = "QualifiedPattern", try_from = "QualifiedPattern")]
+
// pub struct Pattern(QualifiedPattern<'static>);

-
    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 qualified reference name pattern that matches a branch
-
    /// exactly, i.e., return `/refs/heads/<name>`.
-
    pub fn branch_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.
@@ -96,8 +57,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();
@@ -116,12 +77,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))
@@ -247,8 +202,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.)
@@ -337,7 +292,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
@@ -406,9 +361,9 @@ impl ValidRule {
    ///
    /// # Errors
    ///
-
    /// If the `name` reference begins with `refs/rad`.
+
    /// If the `name` reference is protected (see [`crate::git::canonical::protect`]).
    pub fn default_branch(did: Did, name: &git::RefStr) -> Result<(Pattern, Self), PatternError> {
-
        let pattern = Pattern::try_from(git::refs::branch(name).to_owned())?;
+
        let pattern = Pattern::branch_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
@@ -718,11 +673,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)]
@@ -731,8 +683,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)]
@@ -779,7 +729,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 {
@@ -1227,8 +1177,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());
    }
}