use bstr::{BString, ByteSlice}; use either::Either; use radicle::crypto::PublicKey; use radicle::git::{self, Component, Namespaced, Oid, Qualified}; use thiserror::Error; pub use radicle::git::refs::storage::Special; use crate::git::refs::{Policy, Update}; pub(crate) use radicle::git::refs::storage::IDENTITY_BRANCH as REFS_RAD_ID; #[derive(Debug, Error)] #[non_exhaustive] pub enum Error { #[error("non-namespaced ref name '{0}' is not 'refs/rad/id'")] NotCanonicalRadID(Qualified<'static>), #[error("invalid remote peer id")] PublicKey(#[from] radicle::crypto::PublicKeyError), #[error(transparent)] Ref(#[from] radicle::git::RefError), #[error(transparent)] Utf8(#[from] bstr::Utf8Error), } pub(crate) fn unpack_ref<'a>( r: gix_protocol::handshake::Ref, ) -> Result<(ReceivedRefname<'a>, Oid), Error> { use crate::git::oid; use gix_protocol::handshake::Ref; match r { Ref::Peeled { full_ref_name, object, .. } | Ref::Direct { full_ref_name, object, } | Ref::Symbolic { full_ref_name, object, .. } => ReceivedRefname::try_from(full_ref_name).map(|name| (name, oid::to_oid(object))), Ref::Unborn { full_ref_name, .. } => { unreachable!("BUG: unborn ref {}", full_ref_name) } } } /// A reference name received during an exchange with another peer. The /// expected references are either namespaced references in the form /// of [`RemoteRef`] or the canonical `rad/id` reference. #[derive(Debug)] pub(crate) enum ReceivedRefname<'a> { /// A reference name under a `remote` namespace. /// /// # Examples /// /// * `refs/namespaces//refs/rad/id` /// * `refs/namespaces//refs/rad/sigrefs` /// * `refs/namespaces//refs/heads/main` /// * `refs/namespaces//refs/cobs/issue.rad.xyz` Namespaced { /// The namespace of the remote. remote: PublicKey, /// The reference is expected to either be a [`Special`] reference /// or a generic reference name. suffix: Either>, }, /// The canonical `refs/rad/id` reference RadId, } impl<'a> ReceivedRefname<'a> { pub fn remote(remote: PublicKey, suffix: Qualified<'a>) -> Self { Self::Namespaced { remote, suffix: Either::Right(suffix), } } pub fn to_qualified<'b>(&self) -> Qualified<'b> { match &self { Self::Namespaced { remote, suffix } => match suffix { Either::Left(s) => Qualified::from(*s) .with_namespace(Component::from(remote)) .into(), Either::Right(name) => { Qualified::from(name.with_namespace(Component::from(remote))).to_owned() } }, Self::RadId => REFS_RAD_ID.clone(), } } pub fn to_namespaced<'b>(&self) -> Option> { match self { Self::Namespaced { remote, suffix } => Some(match suffix { Either::Left(special) => special.namespaced(remote), Either::Right(refname) => { refname.with_namespace(Component::from(remote)).to_owned() } }), Self::RadId => None, } } } impl TryFrom for ReceivedRefname<'_> { type Error = Error; fn try_from(value: BString) -> Result { match git::parse_ref::(value.to_str()?)? { (None, name) => (name == *REFS_RAD_ID) .then_some(ReceivedRefname::RadId) .ok_or_else(|| Error::NotCanonicalRadID(name.to_owned())), (Some(remote), name) => Ok(ReceivedRefname::Namespaced { remote, suffix: match Special::from_qualified(&name) { None => Either::Right(name.to_owned()), Some(special) => Either::Left(special), }, }), } } } /// A reference name and the associated tip received during an /// exchange with another peer. #[derive(Debug)] pub(crate) struct ReceivedRef { pub tip: Oid, pub name: ReceivedRefname<'static>, } impl ReceivedRef { pub fn new(tip: Oid, name: ReceivedRefname<'static>) -> Self { Self { tip, name } } pub fn to_qualified(&self) -> Qualified<'static> { self.name.to_qualified() } pub fn as_special_ref_update(&self, is_delegate: F) -> Option<(PublicKey, Update<'static>)> where F: Fn(&PublicKey) -> bool, { match &self.name { ReceivedRefname::RadId => None, ReceivedRefname::Namespaced { remote, suffix } => { special_update(remote, suffix, self.tip, is_delegate).map(|up| (*remote, up)) } } } } pub(crate) fn special_update( remote: &PublicKey, suffix: &Either, tip: Oid, is_delegate: F, ) -> Option> where F: Fn(&PublicKey) -> bool, { suffix.as_ref().left().map(|special| Update::Direct { name: special.namespaced(remote).to_owned(), target: tip, // N.b. reject any updates if the remote is not a delegate, // since this is not fatal. no_ff: if is_delegate(remote) { Policy::Abort } else { Policy::Reject }, }) }