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