use bstr::{BString, ByteSlice};
use either::Either;
use radicle::crypto::PublicKey;
use radicle::git::{
self, Oid,
fmt::{Component, Namespaced, 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 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, object.into())),
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/<remote>/refs/rad/id`
/// * `refs/namespaces/<remote>/refs/rad/sigrefs`
/// * `refs/namespaces/<remote>/refs/heads/main`
/// * `refs/namespaces/<remote>/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<Special, Qualified<'a>>,
},
/// 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<Namespaced<'b>> {
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<BString> for ReceivedRefname<'_> {
type Error = Error;
fn try_from(value: BString) -> Result<Self, Self::Error> {
match git::parse_ref::<PublicKey>(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<F>(&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<F>(
remote: &PublicKey,
suffix: &Either<Special, Qualified>,
tip: Oid,
is_delegate: F,
) -> Option<Update<'static>>
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
},
})
}