diff --git a/radicle-node/src/service.rs b/radicle-node/src/service.rs index d3be30e4..8388373c 100644 --- a/radicle-node/src/service.rs +++ b/radicle-node/src/service.rs @@ -851,7 +851,7 @@ pub trait ServiceState { /// Get the current inventory. fn inventory(&self) -> Result; /// Get a project from storage, using the local node's key. - fn get(&self, proj: Id) -> Result>, storage::Error>; + fn get(&self, proj: Id) -> Result>, storage::ProjectError>; /// Get the clock. fn clock(&self) -> &RefClock; /// Get service configuration. @@ -874,7 +874,7 @@ where self.storage.inventory() } - fn get(&self, proj: Id) -> Result>, storage::Error> { + fn get(&self, proj: Id) -> Result>, storage::ProjectError> { self.storage.get(&self.node_id(), proj) } @@ -944,6 +944,8 @@ pub enum LookupError { Storage(#[from] storage::Error), #[error(transparent)] Routing(#[from] routing::Error), + #[error(transparent)] + Project(#[from] storage::ProjectError), } /// Information on a peer, that we may or may not be connected to. diff --git a/radicle/src/identity/project.rs b/radicle/src/identity/project.rs index f666e435..60ea9119 100644 --- a/radicle/src/identity/project.rs +++ b/radicle/src/identity/project.rs @@ -17,6 +17,7 @@ use crate::crypto; use crate::crypto::{Signature, Unverified, Verified}; use crate::git; use crate::identity::Did; +use crate::storage; use crate::storage::git::trailers; use crate::storage::{BranchName, ReadRepository, RemoteId, WriteRepository, WriteStorage}; @@ -36,7 +37,7 @@ pub const MAX_STRING_LENGTH: usize = 255; pub const MAX_DELEGATES: usize = 255; #[derive(Error, Debug)] -pub enum Error { +pub enum DocError { #[error("json: {0}")] Json(#[from] serde_json::Error), #[error("i/o: {0}")] @@ -47,6 +48,21 @@ pub enum Error { Git(#[from] git::Error), #[error("git: {0}")] RawGit(#[from] git2::Error), + #[error("storage: {0}")] + Storage(#[from] storage::Error), + #[error("git: reference `{0}` was not found")] + NotFound(git::RefString), +} + +impl DocError { + /// Whether this error is caused by the document not being found. + pub fn is_not_found(&self) -> bool { + match self { + Self::NotFound(_) => true, + Self::Git(git::Error::NotFound(_)) => true, + _ => false, + } + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -93,7 +109,7 @@ pub struct Doc { } impl Doc { - pub fn encode(&self) -> Result<(git::Oid, Vec), Error> { + pub fn encode(&self) -> Result<(git::Oid, Vec), DocError> { let mut buf = Vec::new(); let mut serializer = serde_json::Serializer::with_formatter(&mut buf, olpc_cjson::CanonicalFormatter::new()); @@ -118,7 +134,7 @@ impl Doc { false } - pub fn sign(&self, signer: G) -> Result<(git::Oid, Signature), Error> { + pub fn sign(&self, signer: G) -> Result<(git::Oid, Signature), DocError> { let (oid, bytes) = self.encode()?; let sig = signer.sign(&bytes); @@ -130,7 +146,7 @@ impl Doc { remote: &RemoteId, msg: &str, storage: &S, - ) -> Result<(Id, git::Oid, S::Repository), Error> { + ) -> Result<(Id, git::Oid, S::Repository), DocError> { // You can checkout this branch in your working copy with: // // git fetch rad @@ -138,7 +154,7 @@ impl Doc { // let (doc_oid, doc) = self.encode()?; let id = Id::from(doc_oid); - let repo = storage.repository(id).unwrap(); + let repo = storage.repository(id)?; let tree = git::write_tree(*PATH, doc.as_slice(), repo.raw())?; let oid = Doc::commit(remote, &tree, msg, &[], repo.raw())?; @@ -153,7 +169,7 @@ impl Doc { msg: &str, signatures: &[(&PublicKey, Signature)], repo: &R, - ) -> Result { + ) -> Result { let mut msg = format!("{msg}\n\n"); for (key, sig) in signatures { writeln!(&mut msg, "{}: {key} {sig}", trailers::SIGNATURE_TRAILER) @@ -175,7 +191,7 @@ impl Doc { msg: &str, parents: &[&git2::Commit], repo: &git2::Repository, - ) -> Result { + ) -> Result { let sig = repo .signature() .or_else(|_| git2::Signature::now("radicle", remote.to_string().as_str()))?; @@ -320,48 +336,30 @@ impl Doc { }) } - pub fn blob_at( - commit: Oid, - repo: &R, - ) -> Result, git::Error> { - match repo.blob_at(commit, Path::new(&*PATH)) { - Err(git::ext::Error::NotFound(_)) => Ok(None), - Err(e) => Err(e), - Ok(blob) => Ok(Some(blob)), - } + pub fn blob_at(commit: Oid, repo: &R) -> Result { + repo.blob_at(commit, Path::new(&*PATH)) + .map_err(DocError::from) } - pub fn load_at( - commit: Oid, - repo: &R, - ) -> Result, git::Error> { - if let Some(blob) = Self::blob_at(commit, repo)? { - let doc = Doc::from_json(blob.content()).unwrap(); - return Ok(Some((doc, blob.id().into()))); - } - Ok(None) + pub fn load_at(commit: Oid, repo: &R) -> Result<(Self, Oid), DocError> { + let blob = Self::blob_at(commit, repo)?; + let doc = Doc::from_json(blob.content())?; + + Ok((doc, blob.id().into())) } - pub fn load( - remote: &RemoteId, - repo: &R, - ) -> Result, git::Error> { - if let Some(oid) = Self::head(remote, repo)? { - Self::load_at(oid, repo) - } else { - Ok(None) - } + pub fn load(remote: &RemoteId, repo: &R) -> Result<(Self, Oid), DocError> { + let oid = Self::head(remote, repo)?; + + Self::load_at(oid, repo) } } impl Doc { - pub fn head(remote: &RemoteId, repo: &R) -> Result, git::Error> { + pub fn head(remote: &RemoteId, repo: &R) -> Result { let head = &git::refname!("heads").join(&*git::refs::IDENTITY_BRANCH); - if let Some(oid) = repo.reference_oid(remote, head)? { - Ok(Some(oid)) - } else { - Ok(None) - } + repo.reference_oid(remote, head)? + .ok_or_else(|| DocError::NotFound(head.to_owned())) } } @@ -379,8 +377,8 @@ pub enum IdentityError { InvalidSignature(PublicKey, crypto::Error), #[error("quorum not reached: {0} signatures for a threshold of {1}")] QuorumNotReached(usize, usize), - #[error("the identity branch was not found")] - NotFound, + #[error("identity document error: {0}")] + Doc(#[from] DocError), } #[derive(Clone, Debug, PartialEq, Eq)] @@ -424,60 +422,58 @@ impl Identity { remote: &RemoteId, repo: &R, ) -> Result, IdentityError> { - if let Some(head) = Doc::::head(remote, repo)? { - let mut history = repo.revwalk(head)?.collect::>(); + let head = Doc::::head(remote, repo)?; + let mut history = repo.revwalk(head)?.collect::>(); - // Retrieve root document. - let root_oid = history.pop().unwrap()?.into(); - let root_blob = Doc::blob_at(root_oid, repo)?.unwrap(); - let root: git::Oid = root_blob.id().into(); - let trusted = Doc::from_json(root_blob.content()).unwrap(); - let revision = history.len() as u32; + // Retrieve root document. + let root_oid = history.pop().unwrap()?.into(); + let root_blob = Doc::blob_at(root_oid, repo)?; + let root: git::Oid = root_blob.id().into(); + let trusted = Doc::from_json(root_blob.content()).unwrap(); + let revision = history.len() as u32; - let mut trusted = trusted.verified()?; - let mut current = root; - let mut signatures = Vec::new(); + let mut trusted = trusted.verified()?; + let mut current = root; + let mut signatures = Vec::new(); - // Traverse the history chronologically. - for oid in history.into_iter().rev() { - let oid = oid?; - let blob = Doc::blob_at(oid.into(), repo)?.unwrap(); - let untrusted = Doc::from_json(blob.content()).unwrap(); - let untrusted = untrusted.verified()?; - let commit = repo.commit(oid.into())?.unwrap(); - let msg = commit.message_raw().unwrap(); + // Traverse the history chronologically. + for oid in history.into_iter().rev() { + let oid = oid?; + let blob = Doc::blob_at(oid.into(), repo)?; + let untrusted = Doc::from_json(blob.content()).map_err(DocError::from)?; + let untrusted = untrusted.verified()?; + let commit = repo.commit(oid.into())?.unwrap(); + let msg = commit.message_raw().unwrap(); - // Keys that signed the *current* document version. - signatures = trailers::parse_signatures(msg).unwrap(); - for (pk, sig) in &signatures { - if let Err(err) = pk.verify(blob.content(), sig) { - return Err(IdentityError::InvalidSignature(*pk, err)); - } + // Keys that signed the *current* document version. + signatures = trailers::parse_signatures(msg).unwrap(); + for (pk, sig) in &signatures { + if let Err(err) = pk.verify(blob.content(), sig) { + return Err(IdentityError::InvalidSignature(*pk, err)); } - - // Check that enough delegates signed this next version. - let quorum = signatures - .iter() - .filter(|(key, _)| trusted.delegates.iter().any(|d| d.matches(key))) - .count(); - if quorum < trusted.threshold { - return Err(IdentityError::QuorumNotReached(quorum, trusted.threshold)); - } - - trusted = untrusted; - current = blob.id().into(); } - return Ok(Identity { - root, - head, - current, - revision, - doc: trusted, - signatures: signatures.into_iter().collect(), - }); + // Check that enough delegates signed this next version. + let quorum = signatures + .iter() + .filter(|(key, _)| trusted.delegates.iter().any(|d| d.matches(key))) + .count(); + if quorum < trusted.threshold { + return Err(IdentityError::QuorumNotReached(quorum, trusted.threshold)); + } + + trusted = untrusted; + current = blob.id().into(); } - Err(IdentityError::NotFound) + + Ok(Identity { + root, + head, + current, + revision, + doc: trusted, + signatures: signatures.into_iter().collect(), + }) } } diff --git a/radicle/src/lib.rs b/radicle/src/lib.rs index 8abd882a..1bb8cefa 100644 --- a/radicle/src/lib.rs +++ b/radicle/src/lib.rs @@ -1,3 +1,4 @@ +#![allow(clippy::match_like_matches_macro)] pub mod collections; pub mod crypto; pub mod git; diff --git a/radicle/src/rad.rs b/radicle/src/rad.rs index 2f7f7ab0..8f85e3ca 100644 --- a/radicle/src/rad.rs +++ b/radicle/src/rad.rs @@ -8,8 +8,10 @@ use thiserror::Error; use crate::crypto::{Signer, Verified}; use crate::git; +use crate::identity::project::DocError; use crate::identity::Id; use crate::node; +use crate::storage::git::ProjectError; use crate::storage::refs::SignedRefs; use crate::storage::{BranchName, ReadRepository as _, RemoteId, WriteRepository as _}; use crate::{identity, storage}; @@ -19,7 +21,7 @@ pub static REMOTE_NAME: Lazy = Lazy::new(|| git::refname!("rad") #[derive(Error, Debug)] pub enum InitError { #[error("doc: {0}")] - Doc(#[from] identity::project::Error), + Doc(#[from] identity::project::DocError), #[error("doc: {0}")] DocVerification(#[from] identity::project::VerificationError), #[error("git: {0}")] @@ -95,6 +97,8 @@ pub enum ForkError { NotFound(Id), #[error("project identity error: {0}")] InvalidIdentity(#[from] storage::git::ProjectError), + #[error("project identity document error: {0}")] + Doc(#[from] DocError), #[error("git: invalid reference")] InvalidReference, } @@ -206,6 +210,8 @@ pub enum CloneError { Fork(#[from] ForkError), #[error("checkout: {0}")] Checkout(#[from] CheckoutError), + #[error("identity document error: {0}")] + Doc(#[from] DocError), } pub fn clone, G: Signer, S: storage::WriteStorage, H: node::Handle>( @@ -258,6 +264,8 @@ pub enum CheckoutError { Storage(#[from] storage::Error), #[error("project `{0}` was not found in storage")] NotFound(Id), + #[error("project error: {0}")] + Project(#[from] ProjectError), } /// Checkout a project from storage as a working copy. diff --git a/radicle/src/storage.rs b/radicle/src/storage.rs index b3d761e1..0cca4fa1 100644 --- a/radicle/src/storage.rs +++ b/radicle/src/storage.rs @@ -9,7 +9,7 @@ use std::{fmt, io}; use thiserror::Error; -pub use git::VerifyError; +pub use git::{ProjectError, VerifyError}; pub use radicle_git_ext::Oid; use crate::collections::HashMap; @@ -20,7 +20,6 @@ use crate::git::Url; use crate::git::{RefError, RefStr, RefString}; use crate::identity; use crate::identity::{Id, IdError}; -use crate::storage::git::ProjectError; use crate::storage::refs::Refs; use self::refs::SignedRefs; @@ -43,8 +42,6 @@ pub enum Error { Id(#[from] IdError), #[error("i/o: {0}")] Io(#[from] io::Error), - #[error("doc: {0}")] - Doc(#[from] identity::project::Error), #[error("invalid repository head")] InvalidHead, } @@ -59,6 +56,8 @@ pub enum FetchError { Io(#[from] io::Error), #[error("verify: {0}")] Verify(#[from] git::VerifyError), + #[error(transparent)] + Storage(#[from] Error), } pub type RemoteId = PublicKey; @@ -219,7 +218,11 @@ impl Remote { pub trait ReadStorage { fn path(&self) -> &Path; fn url(&self, proj: &Id) -> Url; - fn get(&self, remote: &RemoteId, proj: Id) -> Result>, Error>; + fn get( + &self, + remote: &RemoteId, + proj: Id, + ) -> Result>, ProjectError>; fn inventory(&self) -> Result; } @@ -281,7 +284,11 @@ where self.deref().inventory() } - fn get(&self, remote: &RemoteId, proj: Id) -> Result>, Error> { + fn get( + &self, + remote: &RemoteId, + proj: Id, + ) -> Result>, ProjectError> { self.deref().get(remote, proj) } } diff --git a/radicle/src/storage/git.rs b/radicle/src/storage/git.rs index 76b48b38..f63590ed 100644 --- a/radicle/src/storage/git.rs +++ b/radicle/src/storage/git.rs @@ -28,12 +28,19 @@ pub static REMOTES_GLOB: Lazy = pub static SIGNATURES_GLOB: Lazy = Lazy::new(|| refspec::pattern!("refs/remotes/*/radicle/signature")); +// FIXME: Should this be here? #[derive(Error, Debug)] pub enum ProjectError { #[error("identity branches diverge from each other")] BranchesDiverge, #[error("identity branches are in an invalid state")] InvalidState, + #[error("storage error: {0}")] + Storage(#[from] Error), + #[error("identity document error: {0}")] + Doc(#[from] identity::project::DocError), + #[error("identity verification error: {0}")] + Verify(#[from] identity::project::VerificationError), #[error("git: {0}")] Git(#[from] git2::Error), #[error("git: {0}")] @@ -42,6 +49,16 @@ pub enum ProjectError { Refs(#[from] refs::Error), } +impl ProjectError { + /// Whether this error is caused by the project not being found. + pub fn is_not_found(&self) -> bool { + match self { + Self::Doc(doc) => doc.is_not_found(), + _ => false, + } + } +} + pub struct Storage { path: PathBuf, } @@ -68,12 +85,15 @@ impl ReadStorage for Storage { } } - fn get(&self, remote: &RemoteId, proj: Id) -> Result>, Error> { + fn get(&self, remote: &RemoteId, proj: Id) -> Result>, ProjectError> { // TODO: Don't create a repo here if it doesn't exist? // Perhaps for checking we could have a `contains` method? - self.repository(proj)? - .project_of(remote) - .map_err(Error::from) + match self.repository(proj)?.project_of(remote) { + Ok(doc) => Ok(Some(doc)), + + Err(err) if err.is_not_found() => Ok(None), + Err(err) => Err(err), + } } fn inventory(&self) -> Result { @@ -97,7 +117,7 @@ impl WriteStorage for Storage { } fn fetch(&self, proj_id: Id, remote: &Url) -> Result, FetchError> { - let mut repo = self.repository(proj_id).unwrap(); + let mut repo = self.repository(proj_id)?; let mut path = remote.path.clone(); path.push(b'/'); @@ -272,15 +292,11 @@ impl Repository { Identity::load(remote, self) } - pub fn project_of( - &self, - remote: &RemoteId, - ) -> Result>, refs::Error> { - if let Some((doc, _)) = identity::Doc::load(remote, self)? { - Ok(Some(doc.verified().unwrap())) - } else { - Ok(None) - } + pub fn project_of(&self, remote: &RemoteId) -> Result, ProjectError> { + let (doc, _) = identity::Doc::load(remote, self)?; + let verified = doc.verified()?; + + Ok(verified) } /// Return the canonical identity [`git::Oid`] and document. @@ -288,7 +304,7 @@ impl Repository { let mut heads = Vec::new(); for remote in self.remote_ids()? { let remote = remote?; - let oid = Doc::::head(&remote, self)?.unwrap(); + let oid = Doc::::head(&remote, self)?; heads.push(oid.into()); } @@ -329,8 +345,7 @@ impl Repository { } } - Doc::load_at(longest.into(), self)? - .ok_or(refs::Error::NotFound) + Doc::load_at(longest.into(), self) .map(|(doc, _)| (longest.into(), doc)) .map_err(ProjectError::from) } diff --git a/radicle/src/test/storage.rs b/radicle/src/test/storage.rs index a4b37568..248fd6d2 100644 --- a/radicle/src/test/storage.rs +++ b/radicle/src/test/storage.rs @@ -44,7 +44,11 @@ impl ReadStorage for MockStorage { } } - fn get(&self, _remote: &RemoteId, proj: Id) -> Result>, Error> { + fn get( + &self, + _remote: &RemoteId, + proj: Id, + ) -> Result>, git::ProjectError> { Ok(self.inventory.get(&proj).cloned()) }