1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5use crate::crypto::EncryptionAlgorithm;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
8pub struct CommandSpec {
9 pub run: Vec<String>,
12}
13
14impl CommandSpec {
15 pub fn new(run: Vec<String>) -> Self {
16 Self { run }
17 }
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub struct EmbeddedEnv {
22 pub target_path: String,
25 pub nonce: String,
26 pub payload_hash_sha256: String,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct Manifest {
31 pub package_id: Uuid,
33 pub artifact_name: String,
34 pub artifact_size: u64,
35 pub created_at: DateTime<Utc>,
36 pub encryption_algorithm: EncryptionAlgorithm,
37 pub nonce: String,
38 pub artifact_hash_sha256: String,
39 pub payload_hash_sha256: String,
40 pub version: Option<String>,
41 pub profile: Option<String>,
42 pub artifact_kind: Option<String>,
44 pub archive_format: Option<String>,
45 pub runtime: Option<String>,
46 pub framework: Option<String>,
47 pub app_root: Option<String>,
48 pub start_command: Option<Vec<String>>,
49 pub setup_commands: Option<Vec<CommandSpec>>,
50 pub required_runtimes: Option<Vec<String>>,
51 pub writable_paths: Option<Vec<String>>,
52 pub entrypoint: Option<String>,
53 pub args: Option<Vec<String>>,
54 pub env: Option<Vec<(String, String)>>,
55 pub embedded_env: Option<EmbeddedEnv>,
56 pub signature_algorithm: Option<String>,
57 pub signing_pubkey_fingerprint: Option<String>,
58 pub signature_created_at: Option<String>,
59 pub require_signature: Option<bool>,
60}
61
62impl Manifest {
63 pub fn new(
64 artifact_name: String,
65 artifact_size: u64,
66 encryption_algorithm: EncryptionAlgorithm,
67 nonce: &[u8],
68 artifact_hash: &str,
69 payload_hash: &str,
70 ) -> Self {
71 Self {
74 package_id: Uuid::new_v4(),
75 artifact_name,
76 artifact_size,
77 created_at: Utc::now(),
78 encryption_algorithm,
79 nonce: hex::encode(nonce),
80 artifact_hash_sha256: artifact_hash.to_string(),
81 payload_hash_sha256: payload_hash.to_string(),
82 version: None,
83 profile: None,
84 artifact_kind: None,
85 archive_format: None,
86 runtime: None,
87 framework: None,
88 app_root: None,
89 start_command: None,
90 setup_commands: None,
91 required_runtimes: None,
92 writable_paths: None,
93 entrypoint: None,
94 args: None,
95 env: None,
96 embedded_env: None,
97 signature_algorithm: None,
98 signing_pubkey_fingerprint: None,
99 signature_created_at: None,
100 require_signature: None,
101 }
102 }
103
104 pub fn with_version(mut self, version: String) -> Self {
105 self.version = Some(version);
106 self
107 }
108
109 pub fn with_profile(mut self, profile: String) -> Self {
110 self.profile = Some(profile);
111 self
112 }
113
114 pub fn with_artifact_kind(mut self, artifact_kind: String) -> Self {
115 self.artifact_kind = Some(artifact_kind);
116 self
117 }
118
119 pub fn with_archive_format(mut self, archive_format: String) -> Self {
120 self.archive_format = Some(archive_format);
121 self
122 }
123
124 pub fn with_runtime(mut self, runtime: String) -> Self {
125 self.runtime = Some(runtime);
126 self
127 }
128
129 pub fn with_framework(mut self, framework: String) -> Self {
130 self.framework = Some(framework);
131 self
132 }
133
134 pub fn with_app_root(mut self, app_root: String) -> Self {
135 self.app_root = Some(app_root);
136 self
137 }
138
139 pub fn with_start_command(mut self, start_command: Vec<String>) -> Self {
140 self.start_command = Some(start_command);
141 self
142 }
143
144 pub fn with_setup_commands(mut self, setup_commands: Vec<CommandSpec>) -> Self {
145 self.setup_commands = Some(setup_commands);
146 self
147 }
148
149 pub fn with_required_runtimes(mut self, required_runtimes: Vec<String>) -> Self {
150 self.required_runtimes = Some(required_runtimes);
151 self
152 }
153
154 pub fn with_writable_paths(mut self, writable_paths: Vec<String>) -> Self {
155 self.writable_paths = Some(writable_paths);
156 self
157 }
158
159 pub fn with_entrypoint(mut self, entrypoint: String) -> Self {
160 self.entrypoint = Some(entrypoint);
161 self
162 }
163
164 pub fn with_args(mut self, args: Vec<String>) -> Self {
165 self.args = Some(args);
166 self
167 }
168
169 pub fn with_env(mut self, env: Vec<(String, String)>) -> Self {
170 self.env = Some(env);
171 self
172 }
173
174 pub fn with_embedded_env(mut self, embedded_env: EmbeddedEnv) -> Self {
175 self.embedded_env = Some(embedded_env);
176 self
177 }
178
179 pub fn with_signature_metadata(
180 mut self,
181 algorithm: String,
182 fingerprint: String,
183 created_at: String,
184 ) -> Self {
185 self.signature_algorithm = Some(algorithm);
186 self.signing_pubkey_fingerprint = Some(fingerprint);
187 self.signature_created_at = Some(created_at);
188 self
189 }
190
191 pub fn with_require_signature(mut self, require_signature: bool) -> Self {
192 self.require_signature = Some(require_signature);
193 self
194 }
195
196 pub fn to_json(&self) -> crate::error::Result<String> {
197 serde_json::to_string_pretty(self)
198 .map_err(|e| crate::error::CrablockError::Serialization(format!("JSON: {e}")))
199 }
200
201 pub fn from_json(json: &str) -> crate::error::Result<Self> {
202 serde_json::from_str(json)
203 .map_err(|e| crate::error::CrablockError::Serialization(format!("JSON: {e}")))
204 }
205
206 pub fn to_cbor(&self) -> crate::error::Result<Vec<u8>> {
207 let mut buf = Vec::new();
208 ciborium::into_writer(self, &mut buf)
209 .map_err(|e| crate::error::CrablockError::Serialization(format!("CBOR: {e}")))?;
210 Ok(buf)
211 }
212
213 pub fn from_cbor(data: &[u8]) -> crate::error::Result<Self> {
214 ciborium::from_reader(data)
215 .map_err(|e| crate::error::CrablockError::Serialization(format!("CBOR: {e}")))
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn test_manifest_serialization() {
225 let manifest = Manifest::new(
226 "test_app".to_string(),
227 1024,
228 EncryptionAlgorithm::Aes256Gcm,
229 &[0u8; 12],
230 "abc123",
231 "def456",
232 )
233 .with_version("1.0.0".to_string())
234 .with_profile("laravel".to_string())
235 .with_artifact_kind("directory_archive".to_string())
236 .with_archive_format("tar.gz".to_string())
237 .with_runtime("php".to_string())
238 .with_framework("laravel".to_string())
239 .with_app_root(".".to_string())
240 .with_start_command(vec![
241 "php".to_string(),
242 "artisan".to_string(),
243 "serve".to_string(),
244 ])
245 .with_setup_commands(vec![CommandSpec::new(vec![
246 "composer".to_string(),
247 "install".to_string(),
248 ])])
249 .with_required_runtimes(vec!["php".to_string()])
250 .with_writable_paths(vec!["storage".to_string(), "bootstrap/cache".to_string()])
251 .with_entrypoint("/bin/app".to_string())
252 .with_args(vec!["--port".to_string(), "3000".to_string()])
253 .with_env(vec![
254 ("ENV".to_string(), "production".to_string()),
255 ("DEBUG".to_string(), "false".to_string()),
256 ])
257 .with_embedded_env(EmbeddedEnv {
258 target_path: ".env".to_string(),
259 nonce: "00112233445566778899aabb".to_string(),
260 payload_hash_sha256: "feedface".to_string(),
261 });
262
263 let json = manifest.to_json().unwrap();
264 let deserialized = Manifest::from_json(&json).unwrap();
265
266 assert_eq!(manifest.package_id, deserialized.package_id);
267 assert_eq!(manifest.artifact_name, deserialized.artifact_name);
268 assert_eq!(manifest.version, deserialized.version);
269 assert_eq!(manifest.profile, deserialized.profile);
270 assert_eq!(manifest.artifact_kind, deserialized.artifact_kind);
271 assert_eq!(manifest.runtime, deserialized.runtime);
272 assert_eq!(manifest.framework, deserialized.framework);
273 assert_eq!(manifest.app_root, deserialized.app_root);
274 assert_eq!(manifest.start_command, deserialized.start_command);
275 assert_eq!(manifest.setup_commands, deserialized.setup_commands);
276 assert_eq!(manifest.required_runtimes, deserialized.required_runtimes);
277 assert_eq!(manifest.writable_paths, deserialized.writable_paths);
278 assert_eq!(manifest.entrypoint, deserialized.entrypoint);
279 assert_eq!(manifest.embedded_env, deserialized.embedded_env);
280 }
281
282 #[test]
283 fn test_manifest_cbor() {
284 let manifest = Manifest::new(
285 "test_app".to_string(),
286 1024,
287 EncryptionAlgorithm::ChaCha20Poly1305,
288 &[1u8; 12],
289 "hash1",
290 "hash2",
291 );
292
293 let cbor = manifest.to_cbor().unwrap();
294 let deserialized = Manifest::from_cbor(&cbor).unwrap();
295
296 assert_eq!(manifest.package_id, deserialized.package_id);
297 assert_eq!(
298 manifest.encryption_algorithm,
299 deserialized.encryption_algorithm
300 );
301 }
302}