1use anyhow::Error;
2use prost::Message;
3use thiserror::Error;
4use warg_crypto::{hash::AnyHash, Decode, Encode, Signable};
5use warg_protobuf::protocol as protobuf;
6
7use crate::{pbjson_to_prost_timestamp, prost_to_pbjson_timestamp, registry::RecordId};
8
9mod model;
10mod state;
11
12pub use model::{PackageEntry, PackageRecord, Permission};
13pub use state::{LogState, Release, ReleaseState, ValidationError};
14
15pub const PACKAGE_RECORD_VERSION: u32 = 0;
17
18impl Decode for model::PackageRecord {
19 fn decode(bytes: &[u8]) -> Result<Self, Error> {
20 protobuf::PackageRecord::decode(bytes)?.try_into()
21 }
22}
23
24impl TryFrom<protobuf::PackageRecord> for model::PackageRecord {
25 type Error = Error;
26
27 fn try_from(record: protobuf::PackageRecord) -> Result<Self, Self::Error> {
28 let prev: Option<RecordId> = match record.prev {
29 Some(hash_string) => {
30 let hash: AnyHash = hash_string.parse()?;
31 Some(hash.into())
32 }
33 None => None,
34 };
35 let version = record.version;
36 let pbjson_timestamp = record.time.ok_or(InvalidTimestampError)?;
37 let prost_timestamp = pbjson_to_prost_timestamp(pbjson_timestamp);
38 let timestamp = prost_timestamp.try_into()?;
39
40 let entries: Result<Vec<model::PackageEntry>, Error> = record
41 .entries
42 .into_iter()
43 .map(|proto_entry| proto_entry.try_into())
44 .collect();
45 let entries = entries?;
46
47 Ok(model::PackageRecord {
48 prev,
49 version,
50 timestamp,
51 entries,
52 })
53 }
54}
55
56#[derive(Error, Debug)]
57#[error("empty or invalid timestamp in record")]
58struct InvalidTimestampError;
59
60impl TryFrom<protobuf::PackageEntry> for model::PackageEntry {
61 type Error = Error;
62
63 fn try_from(entry: protobuf::PackageEntry) -> Result<Self, Self::Error> {
64 use protobuf::package_entry::Contents;
65 let output = match entry.contents.ok_or(EmptyContentError)? {
66 Contents::Init(init) => model::PackageEntry::Init {
67 hash_algorithm: init.hash_algorithm.parse()?,
68 key: init.key.parse()?,
69 },
70 Contents::GrantFlat(grant_flat) => model::PackageEntry::GrantFlat {
71 key: grant_flat.key.parse()?,
72 permissions: grant_flat
73 .permissions
74 .into_iter()
75 .map(TryInto::try_into)
76 .collect::<Result<_, _>>()?,
77 },
78 Contents::RevokeFlat(revoke_flat) => model::PackageEntry::RevokeFlat {
79 key_id: revoke_flat.key_id.into(),
80 permissions: revoke_flat
81 .permissions
82 .into_iter()
83 .map(TryInto::try_into)
84 .collect::<Result<_, _>>()?,
85 },
86 Contents::Release(release) => model::PackageEntry::Release {
87 version: release
88 .version
89 .parse()
90 .map_err(|error| Error::new(error) as Error)?,
91 content: release.content_hash.parse()?,
92 },
93 Contents::Yank(yank) => model::PackageEntry::Yank {
94 version: yank.version.parse()?,
95 },
96 };
97 Ok(output)
98 }
99}
100
101#[derive(Error, Debug)]
102#[error("no content in entry")]
103struct EmptyContentError;
104
105impl TryFrom<i32> for model::Permission {
106 type Error = Error;
107
108 fn try_from(permission: i32) -> Result<Self, Self::Error> {
109 let proto_perm = protobuf::PackagePermission::try_from(permission)
110 .map_err(|_| PermissionParseError { value: permission })?;
111 match proto_perm {
112 protobuf::PackagePermission::Unspecified => {
113 Err(Error::new(PermissionParseError { value: permission }))
114 }
115 protobuf::PackagePermission::Release => Ok(model::Permission::Release),
116 protobuf::PackagePermission::Yank => Ok(model::Permission::Yank),
117 }
118 }
119}
120
121#[derive(Error, Debug)]
122#[error("the value {value} could not be parsed as a permission")]
123struct PermissionParseError {
124 value: i32,
125}
126
127impl Signable for model::PackageRecord {
130 const PREFIX: &'static [u8] = b"WARG-PACKAGE-RECORD-SIGNATURE-V0";
131}
132
133impl Encode for model::PackageRecord {
134 fn encode(&self) -> Vec<u8> {
135 let proto_record: protobuf::PackageRecord = self.into();
136 proto_record.encode_to_vec()
137 }
138}
139
140impl<'a> From<&'a model::PackageRecord> for protobuf::PackageRecord {
141 fn from(record: &'a model::PackageRecord) -> Self {
142 protobuf::PackageRecord {
143 prev: record.prev.as_ref().map(|hash| hash.to_string()),
144 version: record.version,
145 time: Some(prost_to_pbjson_timestamp(record.timestamp.into())),
146 entries: record.entries.iter().map(|entry| entry.into()).collect(),
147 }
148 }
149}
150
151impl<'a> From<&'a model::PackageEntry> for protobuf::PackageEntry {
152 fn from(entry: &'a model::PackageEntry) -> Self {
153 use protobuf::package_entry::Contents;
154 let contents = match entry {
155 model::PackageEntry::Init {
156 hash_algorithm,
157 key,
158 } => Contents::Init(protobuf::PackageInit {
159 key: key.to_string(),
160 hash_algorithm: hash_algorithm.to_string(),
161 }),
162 model::PackageEntry::GrantFlat { key, permissions } => {
163 Contents::GrantFlat(protobuf::PackageGrantFlat {
164 key: key.to_string(),
165 permissions: permissions.iter().map(Into::into).collect(),
166 })
167 }
168 model::PackageEntry::RevokeFlat {
169 key_id,
170 permissions,
171 } => Contents::RevokeFlat(protobuf::PackageRevokeFlat {
172 key_id: key_id.to_string(),
173 permissions: permissions.iter().map(Into::into).collect(),
174 }),
175 model::PackageEntry::Release { version, content } => {
176 Contents::Release(protobuf::PackageRelease {
177 version: version.to_string(),
178 content_hash: content.to_string(),
179 })
180 }
181 model::PackageEntry::Yank { version } => Contents::Yank(protobuf::PackageYank {
182 version: version.to_string(),
183 }),
184 };
185 let contents = Some(contents);
186 protobuf::PackageEntry { contents }
187 }
188}
189
190impl<'a> From<&'a model::Permission> for i32 {
191 fn from(permission: &'a model::Permission) -> Self {
192 let proto_perm = match permission {
193 model::Permission::Release => protobuf::PackagePermission::Release,
194 model::Permission::Yank => protobuf::PackagePermission::Yank,
195 };
196 proto_perm.into()
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 use std::time::SystemTime;
205
206 use semver::Version;
207
208 use warg_crypto::hash::HashAlgorithm;
209
210 use crate::ProtoEnvelope;
211 use warg_crypto::signing::generate_p256_pair;
212
213 #[test]
214 fn test_envelope_roundtrip() {
215 let (alice_pub, alice_priv) = generate_p256_pair();
216 let (bob_pub, _bob_priv) = generate_p256_pair();
217
218 let record = model::PackageRecord {
219 prev: None,
220 version: PACKAGE_RECORD_VERSION,
221 timestamp: SystemTime::now(),
222 entries: vec![
223 model::PackageEntry::Init {
224 hash_algorithm: HashAlgorithm::Sha256,
225 key: alice_pub,
226 },
227 model::PackageEntry::GrantFlat {
228 key: bob_pub.clone(),
229 permissions: vec![model::Permission::Release, model::Permission::Yank],
230 },
231 model::PackageEntry::RevokeFlat {
232 key_id: bob_pub.fingerprint(),
233 permissions: vec![model::Permission::Release],
234 },
235 model::PackageEntry::Release {
236 version: Version::new(1, 0, 0),
237 content: HashAlgorithm::Sha256.digest(&[0, 1, 2, 3]),
238 },
239 ],
240 };
241
242 let first_envelope = match ProtoEnvelope::signed_contents(&alice_priv, record) {
243 Ok(value) => value,
244 Err(error) => panic!("Failed to sign envelope 1: {:?}", error),
245 };
246
247 let bytes = first_envelope.to_protobuf();
248
249 let second_envelope: ProtoEnvelope<model::PackageRecord> =
250 match ProtoEnvelope::from_protobuf(&bytes) {
251 Ok(value) => value,
252 Err(error) => panic!("Failed to create envelope 2: {:?}", error),
253 };
254
255 assert_eq!(first_envelope, second_envelope);
256 }
257}