pub mod error;
mod iter;
#[cfg(test)]
mod test;
use std::collections::{BTreeMap, HashMap};
use std::num::NonZeroUsize;
use std::path::Path;
use crypto::{signature, PublicKey};
use nonempty::NonEmpty;
use radicle_core::{NodeId, RepoId};
use radicle_git_metadata::commit::CommitData;
use radicle_oid::Oid;
use crate::git;
use crate::identity::doc;
use crate::storage::refs::sigrefs::git::{object, reference};
use crate::storage::refs::{
FeatureLevel, Refs, SignedRefs, IDENTITY_ROOT, REFS_BLOB_PATH, SIGNATURE_BLOB_PATH,
SIGREFS_BRANCH,
};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct VerifiedCommit {
commit: Commit,
level: FeatureLevel,
}
impl VerifiedCommit {
pub(super) fn commit(&self) -> &Commit {
&self.commit
}
#[cfg(test)]
pub(super) fn level(&self) -> FeatureLevel {
self.level
}
pub(crate) fn into_sigrefs_at(self, id: PublicKey) -> SignedRefs {
SignedRefs {
refs: self.commit.refs,
signature: self.commit.signature,
id,
level: self.level,
parent: self.commit.parent,
at: self.commit.oid,
}
}
}
pub struct SignedRefsReader<'a, R, V> {
rid: RepoId,
tip: Tip,
repository: &'a R,
verifier: &'a V,
}
pub enum Tip {
Reference(NodeId),
Commit(Oid),
}
#[derive(Debug, PartialEq)]
pub struct FeatureLevels(BTreeMap<FeatureLevel, Oid>);
impl FeatureLevels {
fn new() -> Self {
Self(BTreeMap::new())
}
pub fn max(&self) -> FeatureLevel {
self.0.last_key_value().map(|(k, _)| *k).unwrap_or_default()
}
fn insert(&mut self, verified: &VerifiedCommit) {
if verified.level != FeatureLevel::None {
self.0.entry(verified.level).or_insert(verified.commit.oid);
}
}
#[cfg(any(test, feature = "test"))]
pub fn test(from: impl IntoIterator<Item = (FeatureLevel, Oid)>) -> Self {
Self(BTreeMap::from_iter(from))
}
}
impl std::fmt::Display for FeatureLevels {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(
&self
.0
.iter()
.map(|(level, at)| format!("{level}@{at}"))
.collect::<Vec<_>>()
.join(", "),
)
}
}
impl<'a, R, V> SignedRefsReader<'a, R, V>
where
R: object::Reader + reference::Reader,
V: signature::Verifier<crypto::Signature>,
{
pub fn new(rid: RepoId, tip: Tip, repository: &'a R, verifier: &'a V) -> Self {
Self {
rid,
tip,
repository,
verifier,
}
}
pub fn read(self) -> Result<VerifiedCommit, error::Read> {
const ONE: NonZeroUsize = NonZeroUsize::new(1).expect("one is non-zero");
const SIGNATURES_COLLECTED: &str = "all signatures were collected";
let mut head = CommitReader::new(self.resolve_tip()?, self.repository)
.read()
.map_err(error::Read::Commit)?
.verify(self.rid, self.verifier)
.map_err(error::Read::Verify)?;
if head.commit.parent.is_none() && head.level == FeatureLevel::Root {
head.level = FeatureLevel::Parent;
}
let head = head;
if head.level >= FeatureLevel::Parent {
return Ok(head);
}
let (seen, levels) = iter::Walk::new(head.commit.oid, self.repository).try_fold(
(
HashMap::<crypto::Signature, NonEmpty<Oid>>::new(),
FeatureLevels::new(),
),
|(mut seen, mut levels), commit| {
let commit = commit.map_err(error::Read::Commit)?;
seen.entry(commit.signature)
.and_modify(|value| value.push(commit.oid))
.or_insert_with(|| NonEmpty::new(commit.oid));
if levels.max() < FeatureLevel::LATEST {
let commit = commit
.verify(self.rid, self.verifier)
.map_err(error::Read::Verify)?;
if commit.level > FeatureLevel::None {
levels.insert(&commit);
}
}
Ok((seen, levels))
},
)?;
let level = levels.max();
if head.level < level {
return Err(error::Read::Downgrade {
levels,
actual: head.level,
commit: head.commit.oid,
});
}
if seen
.get(&head.commit.signature)
.expect(SIGNATURES_COLLECTED)
.len_nonzero()
== ONE
{
return Ok(head);
}
let parent = head.commit.parent.expect("parent must exist");
for commit in iter::Walk::new(parent, self.repository) {
let verified = commit
.map_err(error::Read::Commit)?
.verify(self.rid, self.verifier)
.map_err(error::Read::Verify)?;
if verified.level < level {
continue;
}
let commit = verified.commit();
let commits = seen.get(&commit.signature).expect(SIGNATURES_COLLECTED);
if commits.len_nonzero() == ONE {
return Ok(verified);
}
let id = &commit.oid;
if id == commits.last() {
return Ok(verified);
}
if id == commits.first() {
log::warn!("Duplicate signature found in commits {commits:?}");
}
}
unreachable!()
}
fn resolve_tip(&self) -> Result<Oid, error::Read> {
match self.tip {
Tip::Commit(oid) => Ok(oid),
Tip::Reference(namespace) => {
let reference =
SIGREFS_BRANCH.with_namespace(git::fmt::Component::from(&namespace));
let head = self
.repository
.find_reference(&reference)
.map_err(error::Read::FindReference)?
.ok_or_else(|| error::Read::MissingSigrefs { namespace })?;
Ok(head)
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct Commit {
oid: Oid,
parent: Option<Oid>,
refs: Refs,
signature: crypto::Signature,
identity_root: Option<IdentityRoot>,
}
impl Commit {
pub(super) fn oid(&self) -> &Oid {
&self.oid
}
#[cfg(test)]
pub(super) fn parent(&self) -> Option<&Oid> {
self.parent.as_ref()
}
pub(super) fn refs(&self) -> &Refs {
&self.refs
}
#[cfg(test)]
pub(super) fn signature(&self) -> &crypto::Signature {
&self.signature
}
pub(super) fn verify<V>(
mut self,
expected: RepoId,
verifier: &V,
) -> Result<VerifiedCommit, error::Verify>
where
V: signature::Verifier<crypto::Signature>,
{
verifier
.verify(&self.refs.canonical(), &self.signature)
.map_err(error::Verify::Signature)?;
let level = if let Some(IdentityRoot {
commit: identity_commit,
rid,
}) = self.identity_root
{
if rid != expected {
return Err(error::Verify::MismatchedIdentity {
identity_commit,
sigrefs_commit: self.oid,
expected,
found: rid,
});
} else {
FeatureLevel::Root
}
} else {
FeatureLevel::None
};
self.refs.remove_sigrefs();
let level = match (self.parent, self.refs.remove_parent()) {
(None, None) | (Some(_), None) => {
level
}
(None, Some(actual)) => {
return Err(error::Verify::DanglingParent {
sigrefs_commit: self.oid,
actual,
});
}
(Some(expected), Some(actual)) if expected == actual => {
if level == FeatureLevel::Root {
FeatureLevel::Parent
} else {
return Err(error::Verify::IdentityRootDowngrade {
sigrefs_commit: self.oid,
});
}
}
(Some(expected), Some(actual)) => {
return Err(error::Verify::MismatchedParent {
sigrefs_commit: self.oid,
expected,
actual,
})
}
};
Ok(VerifiedCommit {
commit: self,
level,
})
}
}
pub(super) struct CommitReader<'a, R> {
commit: Oid,
repository: &'a R,
}
impl<'a, R> CommitReader<'a, R>
where
R: object::Reader,
{
pub(super) fn new(commit: Oid, repository: &'a R) -> Self {
Self { commit, repository }
}
pub(super) fn read(self) -> Result<Commit, error::Commit> {
let commit = self.read_commit_data()?;
let Tree { refs, signature } = TreeReader::new(self.commit, self.repository)
.read()
.map_err(error::Commit::Tree)?;
let identity_root = IdentityRootReader::new(&refs, self.repository)
.read()
.map_err(error::Commit::IdentityRoot)?;
let parent = Self::get_parent(&commit).transpose()?;
Ok(Commit {
oid: self.commit,
parent,
refs,
signature,
identity_root,
})
}
fn read_commit_data(&self) -> Result<CommitData<Oid, Oid>, error::Commit> {
let bytes = self
.repository
.read_commit(&self.commit)
.map_err(error::Commit::Read)?
.ok_or(error::Commit::Missing { oid: self.commit })?;
CommitData::from_bytes(&bytes).map_err(|err| error::Commit::Parse {
oid: self.commit,
source: err,
})
}
fn get_parent(commit: &CommitData<Oid, Oid>) -> Option<Result<Oid, error::Commit>> {
let NonEmpty {
head: parent,
tail: mut rest,
} = NonEmpty::collect(commit.parents())?;
if rest.is_empty() {
Some(Ok(parent))
} else {
rest.insert(0, parent);
let err = error::Commit::TooManyParents(error::Parent { parents: rest });
Some(Err(err))
}
}
}
struct Tree {
refs: Refs,
signature: crypto::Signature,
}
struct TreeReader<'a, R> {
commit: Oid,
repository: &'a R,
}
impl<'a, R> TreeReader<'a, R>
where
R: object::Reader,
{
fn new(commit: Oid, repository: &'a R) -> Self {
Self { commit, repository }
}
fn read(self) -> Result<Tree, error::Tree> {
let (refs, signature) = self.try_handle_blobs()?;
let refs = Refs::from_canonical(&refs.bytes).map_err(error::Tree::ParseRefs)?;
let signature = crypto::Signature::try_from(signature.bytes.as_slice())
.map_err(error::Tree::ParseSignature)?;
Ok(Tree { refs, signature })
}
fn try_handle_blobs(&self) -> Result<(object::Blob, object::Blob), error::Tree> {
let commit = &self.commit;
let refs_path = Path::new(REFS_BLOB_PATH);
let sig_path = Path::new(SIGNATURE_BLOB_PATH);
let refs_bytes = self
.repository
.read_blob(commit, refs_path)
.map_err(error::Tree::Refs)?;
let sig_bytes = self
.repository
.read_blob(commit, sig_path)
.map_err(error::Tree::Signature)?;
let result = match (refs_bytes, sig_bytes) {
(None, None) => Err(error::MissingBlobs::Both {
commit: *commit,
refs: refs_path.to_path_buf(),
signature: sig_path.to_path_buf(),
}),
(None, Some(_)) => Err(error::MissingBlobs::Signature {
commit: *commit,
path: sig_path.to_path_buf(),
}),
(Some(_), None) => Err(error::MissingBlobs::Refs {
commit: *commit,
path: refs_path.to_path_buf(),
}),
(Some(refs), Some(sig)) => Ok((refs, sig)),
};
result.map_err(error::Tree::from)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct IdentityRoot {
commit: Oid,
rid: RepoId,
}
struct IdentityRootReader<'a, 'b, R> {
refs: &'a Refs,
repository: &'b R,
}
impl<'a, 'b, R> IdentityRootReader<'a, 'b, R>
where
R: object::Reader,
{
fn new(refs: &'a Refs, repository: &'b R) -> Self {
Self { refs, repository }
}
fn read(self) -> Result<Option<IdentityRoot>, error::IdentityRoot> {
match self.refs.get(&IDENTITY_ROOT) {
Some(commit) => self
.read_blob(&commit)
.map(|rid| Some(IdentityRoot { commit, rid })),
None => Ok(None),
}
}
fn read_blob(&self, commit: &Oid) -> Result<RepoId, error::IdentityRoot> {
let path = Path::new("embeds").join(*doc::PATH);
let object::Blob { oid, .. } = self
.repository
.read_blob(commit, &path)
.map_err(error::IdentityRoot::Blob)?
.ok_or_else(|| error::IdentityRoot::MissingIdentity { commit: *commit })?;
Ok(RepoId::from(oid))
}
}