Skip to main content

crablock_core/
manifest.rs

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    // Commands are stored as argv parts instead of shell strings.
10    // This keeps runtime execution predictable and easier to validate.
11    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    // We store only metadata here.
23    // The secret `.env` bytes live in a separate encrypted payload section.
24    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    // These top fields identify the package and protect its integrity.
32    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    // These optional fields tell the runtime how to treat a directory-style app.
43    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        // New packages start with only the required metadata.
72        // Extra behavior is layered on with the `with_*` builder helpers below.
73        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}