Skip to main content

pyro_artifacts/
artifacts.rs

1use cargo_toml::Dependency;
2use flate2::Compression;
3use flate2::read::GzDecoder;
4use flate2::write::GzEncoder;
5use pyro_spec::{InterfaceSpec, ModuleFunc};
6use sha2::{Digest, Sha256};
7use std::collections::{BTreeMap, HashMap};
8use std::future::Future;
9use std::io::{self, Read, Write};
10use std::ops::Deref;
11use std::path::Path;
12use tar::{Builder, Header};
13use tokio::fs;
14
15use crate::cargo::{CapabilityIdent, CapabilityManifest, ResolvedCapability};
16
17pub enum CapBinary {
18    Pe(Vec<u8>),
19    MachO(Vec<u8>),
20    Elf(Vec<u8>),
21}
22
23impl Deref for CapBinary {
24    type Target = [u8];
25
26    fn deref(&self) -> &Self::Target {
27        match self {
28            CapBinary::Pe(items) => &*items,
29            CapBinary::MachO(items) => &*items,
30            CapBinary::Elf(items) => &*items,
31        }
32    }
33}
34
35pub struct CapabilityBinary {
36    pub ident: CapabilityIdent,
37    pub libs: Vec<CapBinary>,
38    pub interface: InterfaceSpec<'static>,
39}
40
41pub struct CapabilitySource {
42    pub manifest: CapabilityManifest,
43    pub cargo_toml: String,
44    pub cargo_lock: String,
45    pub src_lib_rs: String,
46}
47
48pub struct Interface {
49    pub manifest: CapabilityManifest,
50    pub src_lib_rs: String,
51    pub interface: InterfaceSpec<'static>,
52}
53
54#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
55pub struct ModuleDependencies {
56    pub dependencies: BTreeMap<String, Dependency>,
57    pub capabilities: Vec<ResolvedCapability>,
58}
59
60#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
61pub struct ModuleSource {
62    pub dependencies: ModuleDependencies,
63    pub source: String,
64}
65
66#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
67pub struct ModuleSpec {
68    pub hash: String,
69    pub func: ModuleFunc<'static>,
70    pub capabilities: Vec<ResolvedCapability>,
71}
72
73#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
74pub struct ModuleBinary {
75    pub wasm: Vec<u8>,
76    pub spec: ModuleSpec,
77}
78
79#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
80pub enum Module {
81    Source(ModuleSource),
82    Binary(ModuleBinary),
83}
84
85/// A single wasm module in the pipeline intent.
86#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
87pub struct Playbook {
88    pub hash: String,
89    /// Per-class capability configuration. Keys are class names.
90    #[serde(default)]
91    pub configurations: HashMap<String, Option<serde_json::Value>>,
92}
93
94impl ModuleSource {
95    /// Computes a deterministic hash of the module's source and dependencies.
96    pub fn hash(&self) -> String {
97        Self::compute_hash(
98            &self.source,
99            &self.dependencies.dependencies,
100            &self.dependencies.capabilities,
101        )
102    }
103
104    pub fn compute_hash(
105        code: &str,
106        dependencies: &std::collections::BTreeMap<String, cargo_toml::Dependency>,
107        capabilities: &[crate::cargo::ResolvedCapability],
108    ) -> String {
109        let mut hasher = Sha256::new();
110
111        hasher.update(code.as_bytes());
112
113        if let Ok(deps_json) = serde_json::to_string(dependencies) {
114            hasher.update(deps_json.as_bytes());
115        }
116
117        let mut sorted_caps = capabilities.to_vec();
118        sorted_caps.sort_by(|a, b| {
119            a.package
120                .cmp(&b.package)
121                .then_with(|| a.author.cmp(&b.author))
122                .then_with(|| a.version.cmp(&b.version))
123        });
124
125        if let Ok(caps_json) = serde_json::to_string(&sorted_caps) {
126            hasher.update(caps_json.as_bytes());
127        }
128
129        format!("{:x}", hasher.finalize())
130    }
131}
132
133impl ModuleBinary {
134    pub fn hash(&self) -> String {
135        self.spec.hash.clone()
136    }
137}
138
139impl Module {
140    pub fn hash(&self) -> String {
141        match self {
142            Module::Source(m) => m.hash(),
143            Module::Binary(m) => m.hash(),
144        }
145    }
146}
147
148pub enum Artifacts {
149    CapabilityBinary(CapabilityBinary),
150    CapabilitySource(CapabilitySource),
151    Interface(Interface),
152    Module(Module),
153}
154
155impl From<CapabilityBinary> for Artifacts {
156    fn from(value: CapabilityBinary) -> Self {
157        Artifacts::CapabilityBinary(value)
158    }
159}
160
161impl From<CapabilitySource> for Artifacts {
162    fn from(value: CapabilitySource) -> Self {
163        Artifacts::CapabilitySource(value)
164    }
165}
166
167impl From<Interface> for Artifacts {
168    fn from(value: Interface) -> Self {
169        Artifacts::Interface(value)
170    }
171}
172
173impl From<Module> for Artifacts {
174    fn from(value: Module) -> Self {
175        Artifacts::Module(value)
176    }
177}
178
179impl From<ModuleBinary> for Artifacts {
180    fn from(value: ModuleBinary) -> Self {
181        Artifacts::Module(Module::Binary(value))
182    }
183}
184
185impl From<ModuleSource> for Artifacts {
186    fn from(value: ModuleSource) -> Self {
187        Artifacts::Module(Module::Source(value))
188    }
189}
190
191/// The common trait for all artifact types.
192/// Requires Sized so we can return Self for the constructors.
193pub trait Artifact: Sized {
194    fn write_to_directory(&self, path: &Path) -> impl Future<Output = io::Result<()>> + Send;
195    fn to_tarball(&self) -> Result<Vec<u8>, io::Error>;
196
197    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error>;
198    fn from_dir(path: &Path) -> impl Future<Output = Result<Self, io::Error>> + Send;
199}
200
201// --- Helper for appending files to a tarball ---
202pub(crate) fn append_file<W: Write>(
203    tar: &mut Builder<W>,
204    name: &str,
205    data: &[u8],
206) -> Result<(), io::Error> {
207    let mut header = Header::new_gnu();
208    header.set_size(data.len() as u64);
209    header.set_mode(0o644);
210    header.set_cksum();
211    tar.append_data(&mut header, name, data)
212}
213
214// ==========================================
215// Trait Implementations for Concrete Structs
216// ==========================================
217
218impl Artifact for CapabilityBinary {
219    async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
220        fs::create_dir_all(path).await?;
221        for lib in &self.libs {
222            match lib {
223                CapBinary::Pe(bytes) => fs::write(path.join("lib.dll"), bytes).await?,
224                CapBinary::MachO(bytes) => fs::write(path.join("lib.dylib"), bytes).await?,
225                CapBinary::Elf(bytes) => fs::write(path.join("lib.so"), bytes).await?,
226            }
227        }
228        fs::write(
229            path.join("ident.json"),
230            serde_json::to_string(&self.ident)
231                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?,
232        )
233        .await?;
234        let interface_json = serde_json::to_string_pretty(&self.interface)
235            .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
236        fs::write(path.join("interface.json"), interface_json).await?;
237        Ok(())
238    }
239
240    fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
241        let encoder = GzEncoder::new(Vec::new(), Compression::default());
242        let mut tar = Builder::new(encoder);
243
244        for lib in &self.libs {
245            match lib {
246                CapBinary::Pe(bytes) => append_file(&mut tar, "lib.dll", bytes)?,
247                CapBinary::MachO(bytes) => append_file(&mut tar, "lib.dylib", bytes)?,
248                CapBinary::Elf(bytes) => append_file(&mut tar, "lib.so", bytes)?,
249            }
250        }
251        append_file(
252            &mut tar,
253            "ident.json",
254            serde_json::to_string(&self.ident)
255                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
256                .as_bytes(),
257        )?;
258        append_file(
259            &mut tar,
260            "interface.json",
261            serde_json::to_string_pretty(&self.interface)
262                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?
263                .as_bytes(),
264        )?;
265
266        tar.into_inner()?.finish()
267    }
268
269    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
270        let tar = GzDecoder::new(bytes);
271        let mut archive = tar::Archive::new(tar);
272
273        let mut libs = Vec::new();
274        let mut ident = None;
275        let mut interface = None;
276
277        for file in archive.entries()? {
278            let mut file = file?;
279            let path = file.path()?.to_path_buf();
280            let mut content = Vec::new();
281            file.read_to_end(&mut content)?;
282
283            match path.to_string_lossy().as_ref() {
284                "lib.dll" => libs.push(CapBinary::Pe(content)),
285                "lib.dylib" => libs.push(CapBinary::MachO(content)),
286                "lib.so" => libs.push(CapBinary::Elf(content)),
287                "ident.json" => {
288                    ident = serde_json::from_slice(&content).map_err(|e| {
289                        io::Error::new(
290                            io::ErrorKind::InvalidData,
291                            format!("Unable to deserialize manifest: {}", e),
292                        )
293                    })?;
294                }
295                "interface.json" => {
296                    interface = serde_json::from_slice(&content).map_err(|e| {
297                        io::Error::new(
298                            io::ErrorKind::InvalidData,
299                            format!("Unable to deserialize interface: {}", e),
300                        )
301                    })?;
302                }
303                _ => {}
304            }
305        }
306
307        if libs.is_empty() {
308            return Err(io::Error::new(io::ErrorKind::NotFound, "Missing library"));
309        }
310
311        Ok(CapabilityBinary {
312            ident: ident
313                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing ident.json"))?,
314            libs,
315            interface: interface.ok_or_else(|| {
316                io::Error::new(io::ErrorKind::InvalidData, "Missing interface.json")
317            })?,
318        })
319    }
320
321    async fn from_dir(path: &Path) -> Result<Self, io::Error> {
322        let mut libs = Vec::new();
323        if let Ok(bytes) = fs::read(path.join("lib.dll")).await {
324            libs.push(CapBinary::Pe(bytes));
325        }
326        if let Ok(bytes) = fs::read(path.join("lib.dylib")).await {
327            libs.push(CapBinary::MachO(bytes));
328        }
329        if let Ok(bytes) = fs::read(path.join("lib.so")).await {
330            libs.push(CapBinary::Elf(bytes));
331        }
332
333        if libs.is_empty() {
334            return Err(io::Error::new(
335                io::ErrorKind::NotFound,
336                "Missing capability library",
337            ));
338        }
339
340        let ident_string = fs::read(path.join("ident.json")).await?;
341        let ident = serde_json::from_slice(&ident_string).map_err(|e| {
342            io::Error::new(
343                io::ErrorKind::InvalidData,
344                format!("Unable to deserialize ident: {}", e),
345            )
346        })?;
347
348        let interface_string = fs::read(path.join("interface.json")).await?;
349        let interface = serde_json::from_slice(&interface_string).map_err(|e| {
350            io::Error::new(
351                io::ErrorKind::InvalidData,
352                format!("Unable to deserialize interface: {}", e),
353            )
354        })?;
355
356        Ok(CapabilityBinary {
357            libs,
358            ident,
359            interface,
360        })
361    }
362}
363
364impl Artifact for CapabilitySource {
365    async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
366        fs::create_dir_all(path).await?;
367        let manifest = toml::to_string_pretty(&self.manifest).map_err(|e| {
368            io::Error::new(
369                io::ErrorKind::InvalidData,
370                format!("Unable to serialize manifest: {}", e),
371            )
372        })?;
373        fs::write(path.join("Capability.toml"), &manifest).await?;
374        fs::write(path.join("Cargo.toml"), &self.cargo_toml).await?;
375        fs::write(path.join("Cargo.lock"), &self.cargo_lock).await?;
376
377        let src_dir = path.join("src");
378        fs::create_dir_all(&src_dir).await?;
379        fs::write(src_dir.join("lib.rs"), &self.src_lib_rs).await?;
380        Ok(())
381    }
382
383    fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
384        let encoder = GzEncoder::new(Vec::new(), Compression::default());
385        let mut tar = Builder::new(encoder);
386
387        let manifest = toml::to_string_pretty(&self.manifest).map_err(|e| {
388            io::Error::new(
389                io::ErrorKind::InvalidData,
390                format!("Unable to serialize manifest: {}", e),
391            )
392        })?;
393        append_file(&mut tar, "Capability.toml", manifest.as_bytes())?;
394        append_file(&mut tar, "Cargo.toml", self.cargo_toml.as_bytes())?;
395        append_file(&mut tar, "Cargo.lock", self.cargo_lock.as_bytes())?;
396        append_file(&mut tar, "src/lib.rs", self.src_lib_rs.as_bytes())?;
397
398        tar.into_inner()?.finish()
399    }
400
401    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
402        let tar = GzDecoder::new(bytes);
403        let mut archive = tar::Archive::new(tar);
404
405        let mut manifest = None;
406        let mut cargo_toml = None;
407        let mut cargo_lock = None;
408        let mut src_lib_rs = None;
409
410        for file in archive.entries()? {
411            let mut file = file?;
412            let path = file.path()?.to_path_buf();
413            let mut content = Vec::new();
414            file.read_to_end(&mut content)?;
415
416            match path.to_string_lossy().as_ref() {
417                "Capability.toml" => {
418                    manifest = toml::from_slice(&content).map_err(|e| {
419                        io::Error::new(
420                            io::ErrorKind::InvalidData,
421                            format!("Unable to deserialize manifest: {}", e),
422                        )
423                    })?;
424                }
425                "Cargo.toml" => cargo_toml = String::from_utf8(content).ok(),
426                "Cargo.lock" => cargo_lock = String::from_utf8(content).ok(),
427                "src/lib.rs" => src_lib_rs = String::from_utf8(content).ok(),
428                _ => {}
429            }
430        }
431
432        Ok(CapabilitySource {
433            manifest: manifest.ok_or_else(|| {
434                io::Error::new(io::ErrorKind::InvalidData, "Missing Capability.toml")
435            })?,
436            cargo_toml: cargo_toml
437                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing Cargo.toml"))?,
438            cargo_lock: cargo_lock
439                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing Cargo.lock"))?,
440            src_lib_rs: src_lib_rs
441                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing src/lib.rs"))?,
442        })
443    }
444
445    async fn from_dir(path: &Path) -> Result<Self, io::Error> {
446        let manifest_string = fs::read(path.join("Capability.toml")).await?;
447        let manifest = toml::from_slice(&manifest_string).map_err(|e| {
448            io::Error::new(
449                io::ErrorKind::InvalidData,
450                format!("Unable to deserialize manifest: {}", e),
451            )
452        })?;
453
454        Ok(CapabilitySource {
455            manifest,
456            cargo_toml: fs::read_to_string(path.join("Cargo.toml")).await?,
457            cargo_lock: fs::read_to_string(path.join("Cargo.lock")).await?,
458            src_lib_rs: fs::read_to_string(path.join("src").join("lib.rs")).await?,
459        })
460    }
461}
462
463impl Artifact for Interface {
464    async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
465        let manifest = toml::to_string_pretty(&self.manifest).map_err(|e| {
466            io::Error::new(
467                io::ErrorKind::InvalidData,
468                format!("Unable to serialize manifest: {}", e),
469            )
470        })?;
471        fs::create_dir_all(path).await?;
472        fs::write(path.join("Capability.toml"), &manifest).await?;
473        let interface_str = serde_json::to_string_pretty(&self.interface).map_err(|e| {
474            io::Error::new(
475                io::ErrorKind::InvalidData,
476                format!("Unable to serialize manifest: {}", e),
477            )
478        })?;
479        fs::write(path.join("interface.json"), &interface_str).await?;
480
481        let src_dir = path.join("src");
482        fs::create_dir_all(&src_dir).await?;
483        fs::write(src_dir.join("lib.rs"), &self.src_lib_rs).await?;
484        Ok(())
485    }
486
487    fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
488        let encoder = GzEncoder::new(Vec::new(), Compression::default());
489        let mut tar = Builder::new(encoder);
490        let manifest = toml::to_string_pretty(&self.manifest).map_err(|e| {
491            io::Error::new(
492                io::ErrorKind::InvalidData,
493                format!("Unable to serialize manifest: {}", e),
494            )
495        })?;
496        let interface = serde_json::to_string_pretty(&self.interface).map_err(|e| {
497            io::Error::new(
498                io::ErrorKind::InvalidData,
499                format!("Unable to serialize interface: {}", e),
500            )
501        })?;
502
503        append_file(&mut tar, "Capability.toml", manifest.as_bytes())?;
504        append_file(&mut tar, "interface.json", interface.as_bytes())?;
505        append_file(&mut tar, "src/lib.rs", self.src_lib_rs.as_bytes())?;
506
507        tar.into_inner()?.finish()
508    }
509
510    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
511        let tar = GzDecoder::new(bytes);
512        let mut archive = tar::Archive::new(tar);
513
514        let mut manifest = None;
515        let mut src_lib_rs = None;
516        let mut interface = None;
517
518        for file in archive.entries()? {
519            let mut file = file?;
520            let path = file.path()?.to_path_buf();
521            let mut content = Vec::new();
522            file.read_to_end(&mut content)?;
523
524            match path.to_string_lossy().as_ref() {
525                "Capability.toml" => {
526                    manifest = toml::from_slice(&content).map_err(|e| {
527                        io::Error::new(
528                            io::ErrorKind::InvalidData,
529                            format!("Unable to deserialize manifest: {}", e),
530                        )
531                    })?;
532                }
533                "interface.json" => {
534                    interface = serde_json::from_slice(&content).map_err(|e| {
535                        io::Error::new(
536                            io::ErrorKind::InvalidData,
537                            format!("Unable to deserialize interface: {}", e),
538                        )
539                    })?;
540                }
541                "src/lib.rs" => src_lib_rs = String::from_utf8(content).ok(),
542                _ => {}
543            }
544        }
545
546        Ok(Interface {
547            manifest: manifest.ok_or_else(|| {
548                io::Error::new(io::ErrorKind::InvalidData, "Missing Capability.toml")
549            })?,
550            src_lib_rs: src_lib_rs
551                .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Missing src/lib.rs"))?,
552            interface: interface.ok_or_else(|| {
553                io::Error::new(io::ErrorKind::InvalidData, "Missing interface.json")
554            })?,
555        })
556    }
557
558    async fn from_dir(path: &Path) -> Result<Self, io::Error> {
559        let manifest_string = fs::read(path.join("Capability.toml")).await?;
560        let manifest = toml::from_slice(&manifest_string).map_err(|e| {
561            io::Error::new(
562                io::ErrorKind::InvalidData,
563                format!("Unable to deserialize manifest: {}", e),
564            )
565        })?;
566
567        let interface_string = fs::read(path.join("interface.json")).await?;
568        let interface = toml::from_slice(&interface_string).map_err(|e| {
569            io::Error::new(
570                io::ErrorKind::InvalidData,
571                format!("Unable to deserialize interface: {}", e),
572            )
573        })?;
574
575        Ok(Interface {
576            manifest,
577            src_lib_rs: fs::read_to_string(path.join("src").join("lib.rs")).await?,
578            interface,
579        })
580    }
581}
582
583impl Artifact for ModuleSource {
584    async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
585        fs::create_dir_all(path).await?;
586        let dependencies = serde_json::to_string_pretty(&self.dependencies).map_err(|e| {
587            io::Error::new(
588                io::ErrorKind::InvalidData,
589                format!("Unable to serialize dependencies: {}", e),
590            )
591        })?;
592        fs::write(path.join("source.rs"), &self.source).await?;
593        fs::write(path.join("dependencies.json"), &dependencies).await?;
594        Ok(())
595    }
596
597    fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
598        let encoder = GzEncoder::new(Vec::new(), Compression::default());
599        let mut tar = Builder::new(encoder);
600        let dependencies = serde_json::to_string_pretty(&self.dependencies).map_err(|e| {
601            io::Error::new(
602                io::ErrorKind::InvalidData,
603                format!("Unable to serialize dependencies: {}", e),
604            )
605        })?;
606        append_file(&mut tar, "source.rs", self.source.as_bytes())?;
607        append_file(&mut tar, "dependencies.json", dependencies.as_bytes())?;
608        tar.into_inner()?.finish()
609    }
610
611    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
612        let tar = GzDecoder::new(bytes);
613        let mut archive = tar::Archive::new(tar);
614        let mut source = None;
615        let mut dependencies = None;
616
617        for file in archive.entries()? {
618            let mut file = file?;
619            let path = file.path()?.to_path_buf();
620            let mut content = Vec::new();
621            file.read_to_end(&mut content)?;
622
623            match path.to_string_lossy().as_ref() {
624                "source.rs" => source = String::from_utf8(content).ok(),
625                "dependencies.json" => {
626                    dependencies = serde_json::from_slice(&content).map_err(|e| {
627                        io::Error::new(
628                            io::ErrorKind::InvalidData,
629                            format!("Unable to deserialize dependencies: {}", e),
630                        )
631                    })?;
632                }
633                _ => {}
634            }
635        }
636
637        Ok(ModuleSource {
638            source: source
639                .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Missing source.rs"))?,
640            dependencies: dependencies.ok_or_else(|| {
641                io::Error::new(io::ErrorKind::NotFound, "Missing dependencies.json")
642            })?,
643        })
644    }
645
646    async fn from_dir(path: &Path) -> Result<Self, io::Error> {
647        let dependencies_string = fs::read(path.join("dependencies.json")).await?;
648        let dependencies = serde_json::from_slice(&dependencies_string).map_err(|e| {
649            io::Error::new(
650                io::ErrorKind::InvalidData,
651                format!("Unable to deserialize dependencies: {}", e),
652            )
653        })?;
654        Ok(ModuleSource {
655            source: fs::read_to_string(path.join("source.rs")).await?,
656            dependencies,
657        })
658    }
659}
660
661impl Artifact for ModuleBinary {
662    async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
663        fs::create_dir_all(path).await?;
664        fs::write(path.join("mod.wasm"), &self.wasm).await?;
665        let spec = serde_json::to_string_pretty(&self.spec).map_err(|e| {
666            io::Error::new(
667                io::ErrorKind::InvalidData,
668                format!("Unable to serialize spec: {}", e),
669            )
670        })?;
671        fs::write(path.join("spec.json"), spec).await?;
672        Ok(())
673    }
674
675    fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
676        let encoder = GzEncoder::new(Vec::new(), Compression::default());
677        let mut tar = Builder::new(encoder);
678        append_file(&mut tar, "mod.wasm", &self.wasm)?;
679        let spec = serde_json::to_string_pretty(&self.spec).map_err(|e| {
680            io::Error::new(
681                io::ErrorKind::InvalidData,
682                format!("Unable to serialize spec: {}", e),
683            )
684        })?;
685        append_file(&mut tar, "spec.json", spec.as_bytes())?;
686        tar.into_inner()?.finish()
687    }
688
689    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
690        let tar = GzDecoder::new(bytes);
691        let mut archive = tar::Archive::new(tar);
692        let mut wasm = None;
693        let mut spec = None;
694
695        for file in archive.entries()? {
696            let mut file = file?;
697            let path = file.path()?.to_path_buf();
698            let mut content = Vec::new();
699            file.read_to_end(&mut content)?;
700
701            match path.to_string_lossy().as_ref() {
702                "mod.wasm" => wasm = Some(content),
703                "spec.json" => {
704                    spec = serde_json::from_slice(&content).map_err(|e| {
705                        io::Error::new(
706                            io::ErrorKind::InvalidData,
707                            format!("Unable to deserialize spec: {}", e),
708                        )
709                    })?;
710                }
711                _ => {}
712            }
713        }
714
715        Ok(ModuleBinary {
716            wasm: wasm
717                .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Missing mod.wasm"))?,
718            spec: spec
719                .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Missing spec.json"))?,
720        })
721    }
722
723    async fn from_dir(path: &Path) -> Result<Self, io::Error> {
724        let spec_string = fs::read(path.join("spec.json")).await?;
725        let spec = serde_json::from_slice(&spec_string).map_err(|e| {
726            io::Error::new(
727                io::ErrorKind::InvalidData,
728                format!("Unable to deserialize spec: {}", e),
729            )
730        })?;
731        Ok(ModuleBinary {
732            wasm: fs::read(path.join("mod.wasm")).await?,
733            spec,
734        })
735    }
736}
737
738impl Artifact for Module {
739    async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
740        match self {
741            Module::Source(module_source) => module_source.write_to_directory(path).await,
742            Module::Binary(module_binary) => module_binary.write_to_directory(path).await,
743        }
744    }
745
746    fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
747        match self {
748            Module::Source(module_source) => module_source.to_tarball(),
749            Module::Binary(module_binary) => module_binary.to_tarball(),
750        }
751    }
752
753    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
754        // Peek at the filenames inside the tarball to determine if it's source or binary.
755        let tar = GzDecoder::new(bytes);
756        let mut archive = tar::Archive::new(tar);
757
758        let mut has_source_rs = false;
759        let mut has_wasm = false;
760
761        for file in archive.entries()? {
762            let file = file?;
763            let path_str = file.path()?.to_string_lossy().into_owned();
764
765            match path_str.as_ref() {
766                "source.rs" => has_source_rs = true,
767                "mod.wasm" => has_wasm = true,
768                _ => {}
769            }
770        }
771
772        if has_source_rs {
773            Ok(Module::Source(ModuleSource::from_tarball(bytes)?))
774        } else if has_wasm {
775            Ok(Module::Binary(ModuleBinary::from_tarball(bytes)?))
776        } else {
777            Err(io::Error::new(
778                io::ErrorKind::InvalidData,
779                "Unknown module format in tarball: missing 'source.rs' or 'mod.wasm'",
780            ))
781        }
782    }
783
784    async fn from_dir(path: &Path) -> Result<Self, io::Error> {
785        if fs::try_exists(path.join("source.rs"))
786            .await
787            .unwrap_or(false)
788        {
789            Ok(Module::Source(ModuleSource::from_dir(path).await?))
790        } else if fs::try_exists(path.join("mod.wasm")).await.unwrap_or(false) {
791            Ok(Module::Binary(ModuleBinary::from_dir(path).await?))
792        } else {
793            Err(io::Error::new(
794                io::ErrorKind::InvalidData,
795                "Unknown module format in directory: missing 'source.rs' or 'mod.wasm'",
796            ))
797        }
798    }
799}
800
801impl Artifact for Artifacts {
802    async fn write_to_directory(&self, path: &Path) -> io::Result<()> {
803        match self {
804            Artifacts::CapabilityBinary(c) => c.write_to_directory(path).await,
805            Artifacts::CapabilitySource(c) => c.write_to_directory(path).await,
806            Artifacts::Interface(i) => i.write_to_directory(path).await,
807            Artifacts::Module(m) => m.write_to_directory(path).await,
808        }
809    }
810
811    fn to_tarball(&self) -> Result<Vec<u8>, io::Error> {
812        match self {
813            Artifacts::CapabilityBinary(c) => c.to_tarball(),
814            Artifacts::CapabilitySource(c) => c.to_tarball(),
815            Artifacts::Interface(i) => i.to_tarball(),
816            Artifacts::Module(m) => m.to_tarball(),
817        }
818    }
819
820    fn from_tarball(bytes: &[u8]) -> Result<Self, io::Error> {
821        // Peek at the filenames inside the tarball to determine what artifact this is.
822        let tar = GzDecoder::new(bytes);
823        let mut archive = tar::Archive::new(tar);
824
825        let mut has_source_rs = false;
826        let mut has_wasm = false;
827        let mut has_cap_toml = false;
828        let mut has_lib = false;
829
830        for file in archive.entries()? {
831            let file = file?;
832            let path_str = file.path()?.to_string_lossy().into_owned();
833
834            match path_str.as_ref() {
835                "source.rs" => has_source_rs = true,
836                "mod.wasm" => has_wasm = true,
837                "Capability.toml" => has_cap_toml = true,
838                "lib.dll" | "lib.dylib" | "lib.so" => has_lib = true,
839                _ => {}
840            }
841        }
842
843        if has_source_rs || has_wasm {
844            // Let the Module implementation handle the Source vs Binary split
845            Ok(Artifacts::Module(Module::from_tarball(bytes)?))
846        } else if has_cap_toml {
847            if has_lib {
848                // This shouldn't happen based on the split, but for backward compatibility or weird cases
849                Ok(Artifacts::CapabilitySource(CapabilitySource::from_tarball(
850                    bytes,
851                )?))
852            } else {
853                Ok(Artifacts::Interface(Interface::from_tarball(bytes)?))
854            }
855        } else if has_lib {
856            Ok(Artifacts::CapabilityBinary(CapabilityBinary::from_tarball(
857                bytes,
858            )?))
859        } else {
860            Err(io::Error::new(
861                io::ErrorKind::InvalidData,
862                "Unknown artifact format in tarball",
863            ))
864        }
865    }
866
867    async fn from_dir(path: &Path) -> Result<Self, io::Error> {
868        let has_source_rs = fs::try_exists(path.join("source.rs"))
869            .await
870            .unwrap_or(false);
871        let has_wasm = fs::try_exists(path.join("mod.wasm")).await.unwrap_or(false);
872
873        if has_source_rs || has_wasm {
874            // Let the Module implementation handle the Source vs Binary split
875            Ok(Artifacts::Module(Module::from_dir(path).await?))
876        } else if fs::try_exists(path.join("Capability.toml"))
877            .await
878            .unwrap_or(false)
879        {
880            let has_dll = fs::try_exists(path.join("lib.dll")).await.unwrap_or(false);
881            let has_dylib = fs::try_exists(path.join("lib.dylib"))
882                .await
883                .unwrap_or(false);
884            let has_so = fs::try_exists(path.join("lib.so")).await.unwrap_or(false);
885
886            if has_dll || has_dylib || has_so {
887                // This shouldn't happen based on the split, but for backward compatibility
888                Ok(Artifacts::CapabilitySource(
889                    CapabilitySource::from_dir(path).await?,
890                ))
891            } else {
892                Ok(Artifacts::Interface(Interface::from_dir(path).await?))
893            }
894        } else if fs::try_exists(path.join("lib.dll")).await.unwrap_or(false)
895            || fs::try_exists(path.join("lib.dylib"))
896                .await
897                .unwrap_or(false)
898            || fs::try_exists(path.join("lib.so")).await.unwrap_or(false)
899        {
900            Ok(Artifacts::CapabilityBinary(
901                CapabilityBinary::from_dir(path).await?,
902            ))
903        } else {
904            Err(io::Error::new(
905                io::ErrorKind::InvalidData,
906                "Unknown artifact format in directory",
907            ))
908        }
909    }
910}