radicle_cob/change/
store.rs

1// Copyright © 2022 The Radicle Link Contributors
2
3use std::{error::Error, fmt, num::NonZeroUsize};
4
5use nonempty::NonEmpty;
6use radicle_git_ext::Oid;
7use serde::{Deserialize, Serialize};
8
9use crate::{signatures, TypeName};
10
11/// Change entry storage.
12pub trait Storage {
13    type StoreError: Error + Send + Sync + 'static;
14    type LoadError: Error + Send + Sync + 'static;
15
16    type ObjectId;
17    type Parent;
18    type Signatures;
19
20    /// Store a new change entry.
21    #[allow(clippy::type_complexity)]
22    fn store<G>(
23        &self,
24        resource: Option<Self::Parent>,
25        related: Vec<Self::Parent>,
26        signer: &G,
27        template: Template<Self::ObjectId>,
28    ) -> Result<Entry<Self::Parent, Self::ObjectId, Self::Signatures>, Self::StoreError>
29    where
30        G: crypto::Signer;
31
32    /// Load a change entry.
33    #[allow(clippy::type_complexity)]
34    fn load(
35        &self,
36        id: Self::ObjectId,
37    ) -> Result<Entry<Self::Parent, Self::ObjectId, Self::Signatures>, Self::LoadError>;
38
39    /// Returns the parents of the object with the specified ID.
40    fn parents_of(&self, id: &Oid) -> Result<Vec<Oid>, Self::LoadError>;
41}
42
43/// Change template, used to create a new change.
44pub struct Template<Id> {
45    pub type_name: TypeName,
46    pub tips: Vec<Id>,
47    pub message: String,
48    pub embeds: Vec<Embed<Oid>>,
49    pub contents: NonEmpty<Vec<u8>>,
50}
51
52/// Entry contents.
53/// This is the change payload.
54pub type Contents = NonEmpty<Vec<u8>>;
55
56/// Local time in seconds since epoch.
57pub type Timestamp = u64;
58
59/// A unique identifier for a history entry.
60pub type EntryId = Oid;
61
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct Entry<Resource, Id, Signature> {
64    /// The content address of the entry itself.
65    pub id: Id,
66    /// The content address of the tree of the entry.
67    pub revision: Id,
68    /// The cryptographic signature(s) and their public keys of the
69    /// authors.
70    pub signature: Signature,
71    /// The parent resource that this change lives under. For example,
72    /// this change could be for a patch of a project.
73    pub resource: Option<Resource>,
74    /// Parent changes.
75    pub parents: Vec<Resource>,
76    /// Other parents this change depends on.
77    pub related: Vec<Resource>,
78    /// The manifest describing the type of object as well as the type
79    /// of history for this entry.
80    pub manifest: Manifest,
81    /// The contents that describe entry.
82    pub contents: Contents,
83    /// Timestamp of change.
84    pub timestamp: Timestamp,
85}
86
87impl<Resource, Id, S> fmt::Display for Entry<Resource, Id, S>
88where
89    Id: fmt::Display,
90{
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        write!(f, "Entry {{ id: {} }}", self.id)
93    }
94}
95
96impl<Resource, Id, Signatures> Entry<Resource, Id, Signatures> {
97    pub fn id(&self) -> &Id {
98        &self.id
99    }
100
101    pub fn type_name(&self) -> &TypeName {
102        &self.manifest.type_name
103    }
104
105    pub fn contents(&self) -> &Contents {
106        &self.contents
107    }
108
109    pub fn resource(&self) -> Option<&Resource> {
110        self.resource.as_ref()
111    }
112}
113
114impl<R, Id> Entry<R, Id, signatures::Signatures>
115where
116    Id: AsRef<[u8]>,
117{
118    pub fn valid_signatures(&self) -> bool {
119        self.signature
120            .iter()
121            .all(|(key, sig)| key.verify(self.revision.as_ref(), sig).is_ok())
122    }
123}
124
125impl<R, Id> Entry<R, Id, signatures::ExtendedSignature>
126where
127    Id: AsRef<[u8]>,
128{
129    pub fn valid_signatures(&self) -> bool {
130        self.signature.verify(self.revision.as_ref())
131    }
132
133    pub fn author(&self) -> &crypto::PublicKey {
134        &self.signature.key
135    }
136}
137
138#[derive(Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct Manifest {
141    /// The name given to the type of collaborative object.
142    #[serde(alias = "typename")] // Deprecated name for compatibility reasons.
143    pub type_name: TypeName,
144    /// Version number.
145    #[serde(default)]
146    pub version: Version,
147}
148
149impl Manifest {
150    /// Create a new manifest.
151    pub fn new(type_name: TypeName, version: Version) -> Self {
152        Self { type_name, version }
153    }
154}
155
156/// COB version.
157#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
158pub struct Version(NonZeroUsize);
159
160impl Default for Version {
161    fn default() -> Self {
162        Version(NonZeroUsize::MIN)
163    }
164}
165
166impl From<Version> for usize {
167    fn from(value: Version) -> Self {
168        value.0.into()
169    }
170}
171
172impl From<NonZeroUsize> for Version {
173    fn from(value: NonZeroUsize) -> Self {
174        Self(value)
175    }
176}
177
178impl Version {
179    pub fn new(version: usize) -> Option<Self> {
180        NonZeroUsize::new(version).map(Self)
181    }
182}
183
184/// Embedded object.
185#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct Embed<T = Vec<u8>> {
188    /// File name.
189    pub name: String,
190    /// File content or content hash.
191    pub content: T,
192}
193
194impl<T: From<Oid>> Embed<T> {
195    /// Create a new embed.
196    pub fn store(
197        name: impl ToString,
198        content: &[u8],
199        repo: &git2::Repository,
200    ) -> Result<Self, git2::Error> {
201        let oid = repo.blob(content)?;
202
203        Ok(Self {
204            name: name.to_string(),
205            content: T::from(oid.into()),
206        })
207    }
208}
209
210impl Embed<Vec<u8>> {
211    /// Get the object id of the embedded content.
212    pub fn oid(&self) -> Oid {
213        // SAFETY: This should not fail since we are using a valid object type.
214        git2::Oid::hash_object(git2::ObjectType::Blob, &self.content)
215            .expect("Embed::oid: invalid object")
216            .into()
217    }
218
219    /// Return an embed where the content is replaced by a content hash.
220    pub fn hashed<T: From<Oid>>(&self) -> Embed<T> {
221        Embed {
222            name: self.name.clone(),
223            content: T::from(self.oid()),
224        }
225    }
226}
227
228impl Embed<Oid> {
229    /// Get the object id of the embedded content.
230    pub fn oid(&self) -> Oid {
231        self.content
232    }
233}