#![allow(clippy::doc_overindented_list_items)]
use std::collections::{BTreeMap, BTreeSet, HashSet};
use bstr::{BStr, BString};
use either::Either;
use gix_protocol::handshake::Ref;
use nonempty::NonEmpty;
use radicle::crypto::PublicKey;
use radicle::git::fmt::{Component, Namespaced, Qualified, refname};
use radicle::storage::ReadRepository;
use radicle::storage::git::Repository;
use radicle::storage::refs::{RefsAt, Special};
use crate::git::refs::{Policy, Update, Updates};
use crate::policy::BlockList;
use crate::refs::{ReceivedRef, ReceivedRefname};
use crate::sigrefs::RemoteRefs;
use crate::state::FetchState;
use crate::transport::WantsHaves;
use crate::{policy, refs};
pub mod error {
use radicle::crypto::PublicKey;
use radicle::git::fmt::RefString;
use thiserror::Error;
use crate::transport::WantsHavesError;
#[derive(Debug, Error)]
pub enum Layout {
#[error("missing required refs: {0:?}")]
MissingRequiredRefs(Vec<String>),
#[error("expected threshold of {threshold} of references, missing: {missing:?}")]
InsufficientRefs {
threshold: usize,
missing: Vec<String>,
},
}
#[derive(Debug, Error)]
pub enum Prepare {
#[error(transparent)]
References(#[from] radicle::storage::Error),
#[error("verification of rad/id for {remote} failed")]
Verification {
remote: PublicKey,
#[source]
err: Box<dyn std::error::Error + Send + Sync + 'static>,
},
}
#[derive(Debug, Error)]
pub enum WantsHaves {
#[error(transparent)]
WantsHavesAdd(#[from] WantsHavesError),
#[error("expected namespaced ref {0}")]
NotNamespaced(RefString),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum RefPrefix {
RadId,
NamespacedRadId { namespace: PublicKey },
NamespacedRadSigrefs { namespace: PublicKey },
AllNamespaces,
}
impl RefPrefix {
pub fn into_bstring(self) -> BString {
match self {
RefPrefix::RadId => refs::REFS_RAD_ID.as_bstr().into(),
RefPrefix::NamespacedRadId { namespace } => {
radicle::git::refs::storage::id(&namespace).as_bstr().into()
}
RefPrefix::NamespacedRadSigrefs { namespace } => {
radicle::git::refs::storage::sigrefs(&namespace)
.as_bstr()
.into()
}
RefPrefix::AllNamespaces => "refs/namespaces".into(),
}
}
pub fn as_refspec(&self) -> gix_refspec::RefSpec {
use gix_refspec::parse::Operation;
let parse = |spec: &BStr| -> gix_refspec::RefSpec {
gix_refspec::parse(spec, Operation::Fetch)
.expect("RefPrefix should be valid refspec")
.to_owned()
};
match self {
RefPrefix::RadId => parse(refs::REFS_RAD_ID.as_bstr()),
RefPrefix::NamespacedRadId { namespace } => {
parse(radicle::git::refs::storage::id(namespace).as_bstr())
}
RefPrefix::NamespacedRadSigrefs { namespace } => {
parse(radicle::git::refs::storage::sigrefs(namespace).as_bstr())
}
RefPrefix::AllNamespaces => parse(BStr::new("refs/namespaces")),
}
}
}
pub(crate) trait ProtocolStage {
fn ls_refs(&self) -> Option<NonEmpty<RefPrefix>>;
fn ref_filter(&self, r: Ref) -> Option<ReceivedRef>;
fn pre_validate(&self, refs: &[ReceivedRef]) -> Result<(), error::Layout>;
fn wants_haves(
&self,
refdb: &Repository,
refs: &[ReceivedRef],
) -> Result<WantsHaves, error::WantsHaves> {
let mut wants_haves = WantsHaves::default();
wants_haves.add(
refdb,
refs.iter().map(|recv| (recv.to_qualified(), recv.tip)),
)?;
Ok(wants_haves)
}
fn prepare_updates<'a>(
&self,
s: &FetchState,
repo: &Repository,
refs: &'a [ReceivedRef],
) -> Result<Updates<'a>, error::Prepare>;
}
#[derive(Debug)]
pub struct CanonicalId {
pub remote: PublicKey,
#[allow(dead_code)]
pub limit: u64,
}
impl ProtocolStage for CanonicalId {
fn ls_refs(&self) -> Option<NonEmpty<RefPrefix>> {
Some(NonEmpty::new(RefPrefix::RadId))
}
fn ref_filter(&self, r: Ref) -> Option<ReceivedRef> {
match refs::unpack_ref(r).ok()? {
(
refname @ ReceivedRefname::Namespaced {
suffix: Either::Left(_),
..
},
tip,
) => Some(ReceivedRef::new(tip, refname)),
(ReceivedRefname::RadId, tip) => Some(ReceivedRef::new(tip, ReceivedRefname::RadId)),
_ => None,
}
}
fn pre_validate(&self, refs: &[ReceivedRef]) -> Result<(), error::Layout> {
ensure_refs(
[BString::from(refs::REFS_RAD_ID.as_bstr())]
.into_iter()
.collect(),
refs.iter()
.map(|r| r.to_qualified().to_string().into())
.collect(),
)
}
fn prepare_updates<'a>(
&self,
s: &FetchState,
repo: &Repository,
refs: &'a [ReceivedRef],
) -> Result<Updates<'a>, error::Prepare> {
let verified = repo
.identity_doc_at(
*s.canonical_rad_id()
.expect("ensure we got canonical 'rad/id' ref"),
)
.map_err(|err| error::Prepare::Verification {
remote: self.remote,
err: Box::new(err),
})?;
if verified.is_delegate(&self.remote.into()) {
let is_delegate = |remote: &PublicKey| verified.is_delegate(&remote.into());
Ok(Updates::build(
refs.iter()
.filter_map(|r| r.as_special_ref_update(is_delegate)),
))
} else {
Ok(Updates::default())
}
}
}
#[derive(Debug)]
pub struct SpecialRefs {
pub blocked: BlockList,
#[allow(dead_code)]
pub remote: PublicKey,
pub followed: policy::Allowed,
pub delegates: BTreeSet<PublicKey>,
pub threshold: usize,
#[allow(dead_code)]
pub limit: u64,
}
impl ProtocolStage for SpecialRefs {
fn ls_refs(&self) -> Option<NonEmpty<RefPrefix>> {
match &self.followed {
policy::Allowed::All => Some(NonEmpty::new(RefPrefix::AllNamespaces)),
policy::Allowed::Followed { remotes } => NonEmpty::collect(
remotes
.iter()
.chain(self.delegates.iter())
.flat_map(|remote| {
[
RefPrefix::NamespacedRadSigrefs { namespace: *remote },
RefPrefix::NamespacedRadId { namespace: *remote },
]
}),
),
}
}
fn ref_filter(&self, r: Ref) -> Option<ReceivedRef> {
let (refname, tip) = refs::unpack_ref(r).ok()?;
match refname {
ReceivedRefname::Namespaced { remote, .. } if self.blocked.is_blocked(&remote) => None,
ReceivedRefname::Namespaced { ref suffix, .. } if suffix.is_left() => {
Some(ReceivedRef::new(tip, refname))
}
ReceivedRefname::Namespaced { .. } | ReceivedRefname::RadId => None,
}
}
fn pre_validate(&self, refs: &[ReceivedRef]) -> Result<(), error::Layout> {
ensure_threshold(
self.delegates
.iter()
.filter(|id| !self.blocked.is_blocked(id))
.map(|id| {
BString::from(radicle::git::refs::storage::sigrefs(id).to_string())
})
.collect(),
refs.iter()
.filter_map(|r| r.name.to_namespaced())
.map(|r| r.to_string().into())
.collect(),
self.threshold,
)
}
fn prepare_updates<'a>(
&self,
_s: &FetchState,
_repo: &Repository,
refs: &'a [ReceivedRef],
) -> Result<Updates<'a>, error::Prepare> {
special_refs_updates(&self.delegates, &self.blocked, refs)
}
}
#[derive(Debug)]
pub struct SigrefsAt {
pub blocked: BlockList,
#[allow(dead_code)]
pub remote: PublicKey,
pub refs_at: Vec<RefsAt>,
pub delegates: BTreeSet<PublicKey>,
#[allow(dead_code)]
pub limit: u64,
}
impl ProtocolStage for SigrefsAt {
fn ls_refs(&self) -> Option<NonEmpty<RefPrefix>> {
NonEmpty::collect(
self.refs_at
.iter()
.map(|refs_at| RefPrefix::NamespacedRadSigrefs {
namespace: refs_at.remote,
}),
)
}
fn ref_filter(&self, r: Ref) -> Option<ReceivedRef> {
let (refname, tip) = refs::unpack_ref(r).ok()?;
match refname {
ReceivedRefname::Namespaced { remote, .. } if self.blocked.is_blocked(&remote) => None,
ReceivedRefname::Namespaced {
suffix: Either::Left(Special::SignedRefs),
..
} => Some(ReceivedRef::new(tip, refname)),
ReceivedRefname::Namespaced { .. } | ReceivedRefname::RadId => None,
}
}
fn pre_validate(&self, _refs: &[ReceivedRef]) -> Result<(), error::Layout> {
Ok(())
}
fn wants_haves(
&self,
refdb: &Repository,
refs: &[ReceivedRef],
) -> Result<WantsHaves, error::WantsHaves> {
let mut wants_haves = WantsHaves::default();
let sigrefs = self
.refs_at
.iter()
.map(|RefsAt { remote, at }| (Special::SignedRefs.namespaced(remote), *at));
wants_haves.add(refdb, sigrefs)?;
wants_haves.add(
refdb,
refs.iter().map(|recv| (recv.to_qualified(), recv.tip)),
)?;
Ok(wants_haves)
}
fn prepare_updates<'a>(
&self,
_s: &FetchState,
_repo: &Repository,
_refs: &'a [ReceivedRef],
) -> Result<Updates<'a>, error::Prepare> {
let mut updates = Updates::default();
for RefsAt { remote, at } in self.refs_at.iter() {
if let Some(up) =
refs::special_update(remote, &Either::Left(Special::SignedRefs), *at, |remote| {
self.delegates.contains(remote)
})
{
updates.add(*remote, up);
}
}
Ok(updates)
}
}
#[derive(Debug)]
pub struct DataRefs {
remotes: RemoteRefs,
}
impl DataRefs {
pub(crate) fn new(remotes: RemoteRefs) -> Self {
Self { remotes }
}
pub(crate) fn into_inner(self) -> RemoteRefs {
self.remotes
}
}
impl ProtocolStage for DataRefs {
fn ls_refs(&self) -> Option<NonEmpty<RefPrefix>> {
None
}
fn ref_filter(&self, _: Ref) -> Option<ReceivedRef> {
None
}
fn pre_validate(&self, _refs: &[ReceivedRef]) -> Result<(), error::Layout> {
Ok(())
}
fn wants_haves(
&self,
refdb: &Repository,
_refs: &[ReceivedRef],
) -> Result<WantsHaves, error::WantsHaves> {
let mut wants_haves = WantsHaves::default();
for (remote, result) in self.remotes.iter() {
let Ok(Some(refs)) = result else {
continue;
};
wants_haves.add(
refdb,
refs.iter().filter_map(|(refname, tip)| {
let refname = Qualified::from_refstr(refname)
.map(|refname| refname.with_namespace(Component::from(remote)))?;
Some((refname, *tip))
}),
)?;
}
Ok(wants_haves)
}
fn prepare_updates<'a>(
&self,
_s: &FetchState,
repo: &Repository,
_refs: &'a [ReceivedRef],
) -> Result<Updates<'a>, error::Prepare> {
let mut updates = Updates::default();
for (remote, result) in &self.remotes {
let Ok(Some(refs)) = result else {
continue;
};
let mut signed = HashSet::with_capacity(refs.refs().len());
for (name, tip) in refs.iter() {
let tracking: Namespaced<'_> = Qualified::from_refstr(name)
.and_then(|q| refs::ReceivedRefname::remote(*remote, q).to_namespaced())
.expect("we checked sigrefs well-formedness in wants_refs already");
signed.insert(tracking.clone());
updates.add(
*remote,
Update::Direct {
name: tracking,
target: *tip,
no_ff: Policy::Allow,
},
);
}
let prefix_rad = refname!("refs/rad");
for (name, target) in repo.references_of(remote)? {
if name.starts_with(prefix_rad.as_str()) {
continue;
}
let name = Qualified::from_refstr(name)
.expect("BUG: reference is guaranteed to be Qualified")
.with_namespace(Component::from(remote));
if !signed.contains(&name) {
updates.add(
*remote,
Update::Prune {
name,
prev: either::Left(target),
},
);
}
}
}
Ok(updates)
}
}
fn special_refs_updates<'a>(
delegates: &BTreeSet<PublicKey>,
blocked: &BlockList,
refs: &'a [ReceivedRef],
) -> Result<Updates<'a>, error::Prepare> {
use either::Either::*;
let grouped = refs
.iter()
.filter_map(|r| match &r.name {
refs::ReceivedRefname::Namespaced { remote, suffix } => {
(!blocked.is_blocked(remote)).then_some((remote, r.tip, suffix.clone()))
}
refs::ReceivedRefname::RadId => None,
})
.fold(
BTreeMap::<PublicKey, Vec<_>>::new(),
|mut acc, (remote_id, tip, name)| {
acc.entry(*remote_id).or_default().push((tip, name));
acc
},
);
let mut updates = Updates::default();
for (remote_id, refs) in grouped {
let mut tips_inner = Vec::with_capacity(2);
for (tip, suffix) in &refs {
match &suffix {
Left(refs::Special::Id) => {
if let Some(u) = refs::special_update(&remote_id, suffix, *tip, |remote| {
delegates.contains(remote)
}) {
tips_inner.push(u);
}
}
Left(refs::Special::SignedRefs) => {
if let Some(u) = refs::special_update(&remote_id, suffix, *tip, |remote| {
delegates.contains(remote)
}) {
tips_inner.push(u);
}
}
Right(_) => continue,
}
}
updates.append(remote_id, tips_inner);
}
Ok(updates)
}
fn ensure_refs<T>(required: BTreeSet<T>, wants: BTreeSet<T>) -> Result<(), error::Layout>
where
T: Ord + ToString,
{
if wants.is_empty() {
return Ok(());
}
let diff = required.difference(&wants).collect::<Vec<_>>();
if diff.is_empty() {
Ok(())
} else {
Err(error::Layout::MissingRequiredRefs(
diff.into_iter().map(|ns| ns.to_string()).collect(),
))
}
}
fn ensure_threshold<T>(
wants: BTreeSet<T>,
haves: BTreeSet<T>,
threshold: usize,
) -> Result<(), error::Layout>
where
T: Ord + ToString,
T: std::fmt::Debug,
{
if threshold == 0 {
return Ok(());
}
if wants.is_empty() {
return Ok(());
}
if haves.len() < threshold {
let missing = wants
.difference(&haves)
.map(|ns| ns.to_string())
.collect::<Vec<_>>();
return Err(error::Layout::InsufficientRefs { threshold, missing });
}
Ok(())
}
#[cfg(test)]
mod test {
use super::RefPrefix;
#[test]
fn valid_refspecs() {
let namespace = "z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk"
.parse()
.unwrap();
let prefixes = [
RefPrefix::AllNamespaces,
RefPrefix::RadId,
RefPrefix::NamespacedRadId { namespace },
RefPrefix::NamespacedRadSigrefs { namespace },
];
for prefix in prefixes {
prefix.as_refspec();
}
}
}