1use crate::module_manifest::ModuleManifest;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::Path;
12
13const MAGIC: &[u8; 8] = b"SHAPEPKG";
14const FORMAT_VERSION: u32 = 2;
15const MIN_FORMAT_VERSION: u32 = 1;
17
18fn default_bundle_kind() -> String {
19 "portable-bytecode".to_string()
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct BundleMetadata {
25 pub name: String,
27 pub version: String,
29 pub compiler_version: String,
31 pub source_hash: String,
33 #[serde(default = "default_bundle_kind")]
36 pub bundle_kind: String,
37 #[serde(default)]
39 pub build_host: String,
40 #[serde(default = "default_native_portable")]
42 pub native_portable: bool,
43 pub entry_module: Option<String>,
45 pub built_at: u64,
47}
48
49fn default_native_portable() -> bool {
50 true
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct BundledModule {
56 pub module_path: String,
58 pub bytecode_bytes: Vec<u8>,
60 pub export_names: Vec<String>,
62 pub source_hash: String,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct PackageBundle {
69 pub metadata: BundleMetadata,
71 pub modules: Vec<BundledModule>,
73 pub dependencies: HashMap<String, String>,
75 #[serde(default)]
78 pub blob_store: HashMap<[u8; 32], Vec<u8>>,
79 #[serde(default)]
82 pub manifests: Vec<ModuleManifest>,
83 #[serde(default)]
86 pub native_dependency_scopes: Vec<BundledNativeDependencyScope>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct BundledNativeDependencyScope {
92 pub package_name: String,
94 pub package_version: String,
96 pub package_key: String,
98 pub dependencies: HashMap<String, crate::project::NativeDependencySpec>,
100}
101
102impl PackageBundle {
103 pub fn to_bytes(&self) -> Result<Vec<u8>, String> {
105 let payload =
106 rmp_serde::to_vec(self).map_err(|e| format!("Failed to serialize bundle: {}", e))?;
107
108 let mut buf = Vec::with_capacity(12 + payload.len());
109 buf.extend_from_slice(MAGIC);
110 buf.extend_from_slice(&FORMAT_VERSION.to_le_bytes());
111 buf.extend_from_slice(&payload);
112 Ok(buf)
113 }
114
115 pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
120 if data.len() < 12 {
121 return Err("Bundle too small: missing header".to_string());
122 }
123
124 if &data[..8] != MAGIC {
125 return Err("Invalid bundle: bad magic bytes".to_string());
126 }
127
128 let version = u32::from_le_bytes(
129 data[8..12]
130 .try_into()
131 .map_err(|_| "Invalid version bytes".to_string())?,
132 );
133 if version < MIN_FORMAT_VERSION || version > FORMAT_VERSION {
134 return Err(format!(
135 "Unsupported bundle format version: expected {}-{}, got {}",
136 MIN_FORMAT_VERSION, FORMAT_VERSION, version
137 ));
138 }
139
140 rmp_serde::from_slice(&data[12..])
141 .map_err(|e| format!("Failed to deserialize bundle: {}", e))
142 }
143
144 pub fn write_to_file(&self, path: &Path) -> Result<(), String> {
146 let bytes = self.to_bytes()?;
147 std::fs::write(path, bytes)
148 .map_err(|e| format!("Failed to write bundle to '{}': {}", path.display(), e))
149 }
150
151 pub fn read_from_file(path: &Path) -> Result<Self, String> {
153 let data = std::fs::read(path)
154 .map_err(|e| format!("Failed to read bundle from '{}': {}", path.display(), e))?;
155 Self::from_bytes(&data)
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 fn sample_bundle() -> PackageBundle {
164 PackageBundle {
165 metadata: BundleMetadata {
166 name: "test-pkg".to_string(),
167 version: "0.1.0".to_string(),
168 compiler_version: "0.5.0".to_string(),
169 source_hash: "abc123".to_string(),
170 bundle_kind: default_bundle_kind(),
171 build_host: "x86_64-linux".to_string(),
172 native_portable: true,
173 entry_module: Some("main".to_string()),
174 built_at: 1700000000,
175 },
176 modules: vec![
177 BundledModule {
178 module_path: "main".to_string(),
179 bytecode_bytes: vec![1, 2, 3, 4],
180 export_names: vec!["run".to_string()],
181 source_hash: "def456".to_string(),
182 },
183 BundledModule {
184 module_path: "utils::helpers".to_string(),
185 bytecode_bytes: vec![5, 6, 7],
186 export_names: vec!["helper".to_string(), "format".to_string()],
187 source_hash: "ghi789".to_string(),
188 },
189 ],
190 dependencies: {
191 let mut deps = HashMap::new();
192 deps.insert("my-lib".to_string(), "1.0.0".to_string());
193 deps
194 },
195 blob_store: HashMap::new(),
196 manifests: vec![],
197 native_dependency_scopes: vec![],
198 }
199 }
200
201 #[test]
202 fn test_roundtrip_serialize_deserialize() {
203 let bundle = sample_bundle();
204 let bytes = bundle.to_bytes().expect("serialization should succeed");
205 let restored = PackageBundle::from_bytes(&bytes).expect("deserialization should succeed");
206
207 assert_eq!(restored.metadata.name, "test-pkg");
208 assert_eq!(restored.metadata.version, "0.1.0");
209 assert_eq!(restored.modules.len(), 2);
210 assert_eq!(restored.modules[0].module_path, "main");
211 assert_eq!(restored.modules[0].bytecode_bytes, vec![1, 2, 3, 4]);
212 assert_eq!(restored.modules[1].module_path, "utils::helpers");
213 assert_eq!(restored.dependencies.get("my-lib").unwrap(), "1.0.0");
214 assert!(restored.blob_store.is_empty());
215 assert!(restored.manifests.is_empty());
216 }
217
218 #[test]
219 fn test_magic_bytes_validation() {
220 let mut bad_data = vec![0u8; 20];
221 bad_data[..8].copy_from_slice(b"BADMAGIC");
222 let result = PackageBundle::from_bytes(&bad_data);
223 assert!(result.is_err());
224 assert!(result.unwrap_err().contains("bad magic bytes"));
225 }
226
227 #[test]
228 fn test_version_validation() {
229 let mut data = vec![0u8; 20];
230 data[..8].copy_from_slice(MAGIC);
231 data[8..12].copy_from_slice(&99u32.to_le_bytes());
232 let result = PackageBundle::from_bytes(&data);
233 assert!(result.is_err());
234 assert!(
235 result
236 .unwrap_err()
237 .contains("Unsupported bundle format version")
238 );
239 }
240
241 #[test]
242 fn test_too_small_data() {
243 let result = PackageBundle::from_bytes(&[1, 2, 3]);
244 assert!(result.is_err());
245 assert!(result.unwrap_err().contains("too small"));
246 }
247
248 #[test]
249 fn test_empty_bundle() {
250 let bundle = PackageBundle {
251 metadata: BundleMetadata {
252 name: "empty".to_string(),
253 version: "0.0.1".to_string(),
254 compiler_version: "0.5.0".to_string(),
255 source_hash: "empty".to_string(),
256 bundle_kind: default_bundle_kind(),
257 build_host: "x86_64-linux".to_string(),
258 native_portable: true,
259 entry_module: None,
260 built_at: 0,
261 },
262 modules: vec![],
263 dependencies: HashMap::new(),
264 blob_store: HashMap::new(),
265 manifests: vec![],
266 native_dependency_scopes: vec![],
267 };
268
269 let bytes = bundle.to_bytes().expect("should serialize");
270 let restored = PackageBundle::from_bytes(&bytes).expect("should deserialize");
271 assert_eq!(restored.metadata.name, "empty");
272 assert!(restored.modules.is_empty());
273 assert!(restored.dependencies.is_empty());
274 }
275
276 #[test]
277 fn test_file_roundtrip() {
278 let tmp = tempfile::tempdir().expect("temp dir");
279 let path = tmp.path().join("test.shapec");
280
281 let bundle = sample_bundle();
282 bundle.write_to_file(&path).expect("write should succeed");
283 let restored = PackageBundle::read_from_file(&path).expect("read should succeed");
284
285 assert_eq!(restored.metadata.name, "test-pkg");
286 assert_eq!(restored.modules.len(), 2);
287 }
288
289 #[test]
290 fn test_bundle_with_blob_store_and_manifests() {
291 let blob_hash = [0xAB; 32];
292 let blob_data = vec![10, 20, 30, 40];
293
294 let mut manifest = ModuleManifest::new("mymod".into(), "1.0.0".into());
295 manifest.add_export("greet".into(), blob_hash);
296 manifest.finalize();
297
298 let bundle = PackageBundle {
299 metadata: BundleMetadata {
300 name: "ca-pkg".to_string(),
301 version: "2.0.0".to_string(),
302 compiler_version: "0.6.0".to_string(),
303 source_hash: "ca_hash".to_string(),
304 bundle_kind: default_bundle_kind(),
305 build_host: "x86_64-linux".to_string(),
306 native_portable: true,
307 entry_module: None,
308 built_at: 1700000001,
309 },
310 modules: vec![],
311 dependencies: HashMap::new(),
312 blob_store: {
313 let mut bs = HashMap::new();
314 bs.insert(blob_hash, blob_data.clone());
315 bs
316 },
317 manifests: vec![manifest],
318 native_dependency_scopes: vec![],
319 };
320
321 let bytes = bundle.to_bytes().expect("serialization should succeed");
322 let restored = PackageBundle::from_bytes(&bytes).expect("deserialization should succeed");
323
324 assert_eq!(restored.metadata.name, "ca-pkg");
325 assert_eq!(restored.manifests.len(), 1);
326 assert_eq!(restored.manifests[0].name, "mymod");
327 assert!(restored.manifests[0].verify_integrity());
328 assert_eq!(restored.blob_store.get(&blob_hash), Some(&blob_data));
329 assert!(restored.modules.is_empty());
330 }
331
332 #[test]
333 fn test_bundle_blob_deduplication() {
334 let shared_hash = [0x01; 32];
335 let shared_blob = vec![99, 88, 77];
336
337 let mut m1 = ModuleManifest::new("mod_a".into(), "1.0.0".into());
338 m1.add_export("fn_a".into(), shared_hash);
339 m1.finalize();
340
341 let mut m2 = ModuleManifest::new("mod_b".into(), "1.0.0".into());
342 m2.add_export("fn_b".into(), shared_hash);
343 m2.finalize();
344
345 let bundle = PackageBundle {
346 metadata: BundleMetadata {
347 name: "dedup-pkg".to_string(),
348 version: "1.0.0".to_string(),
349 compiler_version: "0.6.0".to_string(),
350 source_hash: "dedup".to_string(),
351 bundle_kind: default_bundle_kind(),
352 build_host: "x86_64-linux".to_string(),
353 native_portable: true,
354 entry_module: None,
355 built_at: 0,
356 },
357 modules: vec![],
358 dependencies: HashMap::new(),
359 blob_store: {
360 let mut bs = HashMap::new();
361 bs.insert(shared_hash, shared_blob.clone());
362 bs
363 },
364 manifests: vec![m1, m2],
365 native_dependency_scopes: vec![],
366 };
367
368 let bytes = bundle.to_bytes().expect("serialize");
369 let restored = PackageBundle::from_bytes(&bytes).expect("deserialize");
370
371 assert_eq!(restored.blob_store.len(), 1);
373 assert_eq!(restored.blob_store.get(&shared_hash), Some(&shared_blob));
374 assert_eq!(restored.manifests.len(), 2);
375 }
376}