use std::marker::PhantomData; use std::ops::Deref; use nonempty::NonEmpty; use radicle_crypto::ssh::ExtendedSignature; use serde::{Deserialize, Serialize}; use crate::cob::op::Op; use crate::cob::patch::Patch; use crate::cob::store::encoding; use crate::cob::{Entry, History, Manifest, Timestamp, Version}; use crate::cob::{Title, patch}; use crate::crypto::Signer; use crate::git::{self, Oid}; use crate::node::device::Device; use crate::prelude::Did; use crate::profile::env; use crate::storage::ReadRepository; use crate::test::arbitrary; use super::store::{Cob, CobWithType}; use super::thread; /// Convenience type for building histories. #[derive(Debug, Clone, PartialEq, Eq)] pub struct HistoryBuilder { history: History, resource: Option, time: Timestamp, witness: PhantomData, } impl AsRef for HistoryBuilder { fn as_ref(&self) -> &History { &self.history } } impl HistoryBuilder { pub fn comment( &mut self, body: impl ToString, reply_to: Option, signer: &G, ) -> Oid { let action = thread::Action::Comment { body: body.to_string(), reply_to, }; self.commit(&action, signer) } } impl HistoryBuilder where T: CobWithType, T::Action: for<'de> Deserialize<'de> + Serialize + Eq + 'static, { pub fn new(actions: &[T::Action], time: Timestamp, signer: &G) -> HistoryBuilder { let resource = Some(arbitrary::oid()); let revision = arbitrary::oid(); let (contents, oids): (Vec>, Vec) = actions .iter() .map(|a| encoded::(a, time, [], signer)) .unzip(); let contents = NonEmpty::from_vec(contents).unwrap(); let root = oids.first().unwrap(); let manifest = Manifest::new(T::type_name().clone(), Version::default()); let signature = signer.sign(&[0]); let signature = ExtendedSignature::new(*signer.public_key(), signature); let change = Entry { id: *root, signature, resource, contents, timestamp: time.as_secs(), revision, parents: vec![], related: vec![], manifest, }; Self { history: History::new_from_root(change), time, resource, witness: PhantomData, } } pub fn root(&self) -> &Entry { self.history.root() } pub fn merge(&mut self, other: Self) { self.history.merge(other.history); } pub fn commit(&mut self, action: &T::Action, signer: &G) -> crate::git::Oid { let timestamp = self.time; let tips = self.tips(); let revision = arbitrary::oid(); let (data, oid) = encoded::(action, timestamp, tips, signer); let manifest = Manifest::new(T::type_name().clone(), Version::default()); let signature = signer.sign(data.as_slice()); let signature = ExtendedSignature::new(*signer.public_key(), signature); let change = Entry { id: oid, signature, resource: self.resource, contents: NonEmpty::new(data), timestamp: timestamp.as_secs(), revision, parents: vec![], related: vec![], manifest, }; self.history.extend(change); oid } } impl Deref for HistoryBuilder { type Target = History; fn deref(&self) -> &Self::Target { &self.history } } /// Create a new test history. pub fn history( actions: &[T::Action], time: Timestamp, signer: &G, ) -> HistoryBuilder where T: Cob + CobWithType, T::Action: Serialize + Eq + 'static, { HistoryBuilder::new(actions, time, signer) } /// An object that can be used to create and sign operations. pub struct Actor { pub signer: Device, } impl Default for Actor { fn default() -> Self { Self::new(Device::default()) } } impl Actor { pub fn new(signer: Device) -> Self { Self { signer } } } impl Actor { /// Create a new operation. pub fn op_with( &mut self, actions: impl IntoIterator, identity: Option, timestamp: Timestamp, ) -> Op where T: Cob + CobWithType, T::Action: Clone + Serialize, { let actions = actions.into_iter().collect::>(); let data = encoding::encode(serde_json::json!({ "action": actions, "nonce": fastrand::u64(..), })) .unwrap(); let oid = crate::git::raw::Oid::hash_object(crate::git::raw::ObjectType::Blob, &data).unwrap(); let id = oid.into(); let author = *self.signer.public_key(); let actions = NonEmpty::from_vec(actions).unwrap(); let manifest = Manifest::new(T::type_name().clone(), Version::default()); let parents = vec![]; let related = vec![]; Op { id, actions, author, parents, related, timestamp, identity, manifest, } } /// Create a new operation. pub fn op(&mut self, actions: impl IntoIterator) -> Op where T: Cob + CobWithType, T::Action: Clone + Serialize, { let identity = arbitrary::oid(); let timestamp = env::commit_time(); self.op_with::(actions, Some(identity), timestamp.into()) } /// Get the actor's DID. pub fn did(&self) -> Did { self.signer.public_key().into() } } impl Actor { /// Create a patch. pub fn patch( &mut self, title: Title, description: impl ToString, base: crate::git::Oid, oid: crate::git::Oid, repo: &R, ) -> Result { Patch::from_root( self.op::([ patch::Action::Revision { description: description.to_string(), base, oid, resolves: Default::default(), }, patch::Action::Edit { title, target: patch::MergeTarget::default(), }, ]), repo, ) } } /// Encode an action and return its hash. /// /// Doesn't encode in the same way as we do in production, but attempts to include the same data /// that feeds into the hash entropy, so that changing any input will change the resulting oid. fn encoded( action: &T::Action, timestamp: Timestamp, parents: impl IntoIterator, signer: &G, ) -> (Vec, crate::git::Oid) { use radicle_git_metadata::{ author::{Author, Time}, commit::{CommitData, headers::Headers, trailers::OwnedTrailer}, }; let data = encoding::encode(action).unwrap(); let oid = crate::git::raw::Oid::hash_object(crate::git::raw::ObjectType::Blob, &data).unwrap(); let parents = parents.into_iter().map(|o| o.into()); let author = Author { name: "radicle".to_owned(), email: signer.public_key().to_human(), time: Time::new(timestamp.as_secs() as i64, 0), }; let commit = CommitData::::new::<_, _, OwnedTrailer>( oid, parents, author.clone(), author, Headers::new(), String::default(), [], ) .to_string(); let hash = crate::git::raw::Oid::hash_object(crate::git::raw::ObjectType::Commit, commit.as_bytes()) .unwrap(); (data, hash.into()) }