Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
Handle errors correctly in storage
Alexis Sellier committed 3 years ago
commit fde0af09f76a607fa8173d31ecfe26aca7e686bf
parent d652df7c30787db00585c69da2f77be72dce6a94
7 files changed +151 -118
modified radicle-node/src/service.rs
@@ -851,7 +851,7 @@ pub trait ServiceState {
    /// Get the current inventory.
    fn inventory(&self) -> Result<Inventory, storage::Error>;
    /// Get a project from storage, using the local node's key.
-
    fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, storage::Error>;
+
    fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, 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<Option<Doc<Verified>>, storage::Error> {
+
    fn get(&self, proj: Id) -> Result<Option<Doc<Verified>>, 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.
modified 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<V> {
}

impl Doc<Verified> {
-
    pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), Error> {
+
    pub fn encode(&self) -> Result<(git::Oid, Vec<u8>), 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<Verified> {
        false
    }

-
    pub fn sign<G: crypto::Signer>(&self, signer: G) -> Result<(git::Oid, Signature), Error> {
+
    pub fn sign<G: crypto::Signer>(&self, signer: G) -> Result<(git::Oid, Signature), DocError> {
        let (oid, bytes) = self.encode()?;
        let sig = signer.sign(&bytes);

@@ -130,7 +146,7 @@ impl Doc<Verified> {
        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<Verified> {
        //
        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<Verified> {
        msg: &str,
        signatures: &[(&PublicKey, Signature)],
        repo: &R,
-
    ) -> Result<git::Oid, Error> {
+
    ) -> Result<git::Oid, DocError> {
        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<Verified> {
        msg: &str,
        parents: &[&git2::Commit],
        repo: &git2::Repository,
-
    ) -> Result<git::Oid, Error> {
+
    ) -> Result<git::Oid, DocError> {
        let sig = repo
            .signature()
            .or_else(|_| git2::Signature::now("radicle", remote.to_string().as_str()))?;
@@ -320,48 +336,30 @@ impl Doc<Unverified> {
        })
    }

-
    pub fn blob_at<R: ReadRepository>(
-
        commit: Oid,
-
        repo: &R,
-
    ) -> Result<Option<git2::Blob>, 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<R: ReadRepository>(commit: Oid, repo: &R) -> Result<git2::Blob, DocError> {
+
        repo.blob_at(commit, Path::new(&*PATH))
+
            .map_err(DocError::from)
    }

-
    pub fn load_at<R: ReadRepository>(
-
        commit: Oid,
-
        repo: &R,
-
    ) -> Result<Option<(Self, Oid)>, 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<R: ReadRepository>(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<R: ReadRepository>(
-
        remote: &RemoteId,
-
        repo: &R,
-
    ) -> Result<Option<(Self, Oid)>, git::Error> {
-
        if let Some(oid) = Self::head(remote, repo)? {
-
            Self::load_at(oid, repo)
-
        } else {
-
            Ok(None)
-
        }
+
    pub fn load<R: ReadRepository>(remote: &RemoteId, repo: &R) -> Result<(Self, Oid), DocError> {
+
        let oid = Self::head(remote, repo)?;
+

+
        Self::load_at(oid, repo)
    }
}

impl<V> Doc<V> {
-
    pub fn head<R: ReadRepository>(remote: &RemoteId, repo: &R) -> Result<Option<Oid>, git::Error> {
+
    pub fn head<R: ReadRepository>(remote: &RemoteId, repo: &R) -> Result<Oid, DocError> {
        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<Untrusted> {
        remote: &RemoteId,
        repo: &R,
    ) -> Result<Identity<Oid>, IdentityError> {
-
        if let Some(head) = Doc::<Untrusted>::head(remote, repo)? {
-
            let mut history = repo.revwalk(head)?.collect::<Vec<_>>();
-

-
            // 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;
-

-
            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();
-

-
                // 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));
+
        let head = Doc::<Untrusted>::head(remote, repo)?;
+
        let mut history = repo.revwalk(head)?.collect::<Vec<_>>();
+

+
        // 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();
+

+
        // 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));
                }
+
            }

-
                trusted = untrusted;
-
                current = blob.id().into();
+
            // 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));
            }

-
            return Ok(Identity {
-
                root,
-
                head,
-
                current,
-
                revision,
-
                doc: trusted,
-
                signatures: signatures.into_iter().collect(),
-
            });
+
            trusted = untrusted;
+
            current = blob.id().into();
        }
-
        Err(IdentityError::NotFound)
+

+
        Ok(Identity {
+
            root,
+
            head,
+
            current,
+
            revision,
+
            doc: trusted,
+
            signatures: signatures.into_iter().collect(),
+
        })
    }
}

modified radicle/src/lib.rs
@@ -1,3 +1,4 @@
+
#![allow(clippy::match_like_matches_macro)]
pub mod collections;
pub mod crypto;
pub mod git;
modified 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<git::RefString> = 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<P: AsRef<Path>, 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.
modified 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<Verified> {
pub trait ReadStorage {
    fn path(&self) -> &Path;
    fn url(&self, proj: &Id) -> Url;
-
    fn get(&self, remote: &RemoteId, proj: Id) -> Result<Option<identity::Doc<Verified>>, Error>;
+
    fn get(
+
        &self,
+
        remote: &RemoteId,
+
        proj: Id,
+
    ) -> Result<Option<identity::Doc<Verified>>, ProjectError>;
    fn inventory(&self) -> Result<Inventory, Error>;
}

@@ -281,7 +284,11 @@ where
        self.deref().inventory()
    }

-
    fn get(&self, remote: &RemoteId, proj: Id) -> Result<Option<identity::Doc<Verified>>, Error> {
+
    fn get(
+
        &self,
+
        remote: &RemoteId,
+
        proj: Id,
+
    ) -> Result<Option<identity::Doc<Verified>>, ProjectError> {
        self.deref().get(remote, proj)
    }
}
modified radicle/src/storage/git.rs
@@ -28,12 +28,19 @@ pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
    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<Option<Doc<Verified>>, Error> {
+
    fn get(&self, remote: &RemoteId, proj: Id) -> Result<Option<Doc<Verified>>, 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<Inventory, Error> {
@@ -97,7 +117,7 @@ impl WriteStorage for Storage {
    }

    fn fetch(&self, proj_id: Id, remote: &Url) -> Result<Vec<RefUpdate>, 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<Option<identity::Doc<Verified>>, 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<identity::Doc<Verified>, 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::<Unverified>::head(&remote, self)?.unwrap();
+
            let oid = Doc::<Unverified>::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)
    }
modified radicle/src/test/storage.rs
@@ -44,7 +44,11 @@ impl ReadStorage for MockStorage {
        }
    }

-
    fn get(&self, _remote: &RemoteId, proj: Id) -> Result<Option<Doc<Verified>>, Error> {
+
    fn get(
+
        &self,
+
        _remote: &RemoteId,
+
        proj: Id,
+
    ) -> Result<Option<Doc<Verified>>, git::ProjectError> {
        Ok(self.inventory.get(&proj).cloned())
    }