provider_archive/
archive.rs

1use crate::Result;
2use async_compression::{
3    tokio::{bufread::GzipDecoder, write::GzipEncoder},
4    Level,
5};
6use data_encoding::HEXUPPER;
7use ring::digest::{Context, Digest, SHA256};
8use std::{
9    collections::HashMap,
10    io::{Cursor, Read},
11    path::{Path, PathBuf},
12};
13use tokio::{
14    fs::File,
15    io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt, BufReader},
16};
17use tokio_stream::StreamExt;
18use tokio_tar::Archive;
19use wascap::{
20    jwt::{CapabilityProvider, Claims, Token},
21    prelude::KeyPair,
22};
23
24const CLAIMS_JWT_FILE: &str = "claims.jwt";
25const WIT_WORLD_FILE: &str = "world.wasm";
26
27const GZIP_MAGIC: [u8; 2] = [0x1f, 0x8b];
28
29/// A provider archive is a specialized ZIP file that contains a set of embedded and signed claims
30/// (a .JWT file) as well as a list of binary files, one plugin library for each supported
31/// target architecture and OS combination
32pub struct ProviderArchive {
33    libraries: HashMap<String, Vec<u8>>,
34    name: String,
35    vendor: String,
36    rev: Option<i32>,
37    ver: Option<String>,
38    token: Option<Token<CapabilityProvider>>,
39    json_schema: Option<serde_json::Value>,
40    wit: Option<Vec<u8>>,
41}
42
43impl ProviderArchive {
44    /// Creates a new provider archive in memory, to which native library files can be added.
45    #[must_use]
46    pub fn new(name: &str, vendor: &str, rev: Option<i32>, ver: Option<String>) -> ProviderArchive {
47        ProviderArchive {
48            libraries: HashMap::new(),
49            name: name.to_string(),
50            vendor: vendor.to_string(),
51            rev,
52            ver,
53            token: None,
54            json_schema: None,
55            wit: None,
56        }
57    }
58
59    /// Adds a native library file (.so, .dylib, .dll) to the archive for a given target string
60    pub fn add_library(&mut self, target: &str, input: &[u8]) -> Result<()> {
61        self.libraries.insert(target.to_string(), input.to_vec());
62
63        Ok(())
64    }
65
66    /// Adds a WIT file encoded as a wasm module to the archive
67    pub fn add_wit_world(&mut self, world: &[u8]) -> Result<()> {
68        self.wit = Some(world.to_vec());
69
70        Ok(())
71    }
72
73    /// Sets a JSON schema for this provider's link definition specification. This will be injected
74    /// into the claims written to a provider's PAR file, so you'll need to do this after instantiation
75    /// and prior to writing
76    pub fn set_schema(&mut self, schema: serde_json::Value) -> Result<()> {
77        self.json_schema = Some(schema);
78
79        Ok(())
80    }
81
82    /// Gets the list of architecture/OS targets within the archive
83    #[must_use]
84    pub fn targets(&self) -> Vec<String> {
85        self.libraries.keys().cloned().collect()
86    }
87
88    /// Retrieves the raw bytes for a given target
89    #[must_use]
90    pub fn target_bytes(&self, target: &str) -> Option<Vec<u8>> {
91        self.libraries.get(target).cloned()
92    }
93
94    /// Returns the embedded claims associated with this archive. Note that claims are not available
95    /// while building a new archive. They are only available after the archive has been written
96    /// or if the archive was loaded from an existing file
97    #[must_use]
98    pub fn claims(&self) -> Option<Claims<CapabilityProvider>> {
99        self.token.as_ref().map(|t| t.claims.clone())
100    }
101
102    /// Returns the embedded claims token associated with this archive.
103    #[must_use]
104    pub fn claims_token(&self) -> Option<Token<CapabilityProvider>> {
105        self.token.clone()
106    }
107
108    /// Obtains the JSON schema if one was either set explicitly on the structure or loaded from
109    /// claims in the PAR
110    #[must_use]
111    pub fn schema(&self) -> Option<serde_json::Value> {
112        self.json_schema.clone()
113    }
114
115    /// Returns the WIT embedded in this provider archive.
116    #[must_use]
117    pub fn wit_world(&self) -> Option<&[u8]> {
118        self.wit.as_deref()
119    }
120
121    /// Attempts to read a Provider Archive (PAR) file's bytes to analyze and verify its contents.
122    ///
123    /// The embedded claims in this archive will be validated, and the file hashes contained in
124    /// those claims will be compared and verified against hashes computed at load time. This
125    /// prevents the contents of the archive from being modified without the embedded claims being
126    /// re-signed. This will load all binaries into memory in the returned `ProviderArchive`.
127    ///
128    /// Please note that this method requires that you have _all_ of the provider archive bytes in
129    /// memory, which will likely be really hefty if you are just trying to load a specific binary
130    /// to run
131    pub async fn try_load(input: &[u8]) -> Result<ProviderArchive> {
132        let mut cursor = Cursor::new(input);
133        Self::load(&mut cursor, None).await
134    }
135
136    /// Attempts to read a Provider Archive (PAR) file's bytes to analyze and verify its contents,
137    /// loading _only_ the specified target.
138    ///
139    /// This is useful when loading a provider archive for consumption and you know the target OS
140    /// you need. The embedded claims in this archive will be validated, and the file hashes
141    /// contained in those claims will be compared and verified against hashes computed at load
142    /// time. This prevents the contents of the archive from being modified without the embedded
143    /// claims being re-signed
144    ///
145    /// Please note that this method requires that you have _all_ of the provider archive bytes in
146    /// memory, which will likely be really hefty if you are just trying to load a specific binary
147    /// to run
148    pub async fn try_load_target(input: &[u8], target: &str) -> Result<ProviderArchive> {
149        let mut cursor = Cursor::new(input);
150        Self::load(&mut cursor, Some(target)).await
151    }
152
153    /// Attempts to read a Provider Archive (PAR) file to analyze and verify its contents.
154    ///
155    /// The embedded claims in this archive will be validated, and the file hashes contained in
156    /// those claims will be compared and verified against hashes computed at load time. This
157    /// prevents the contents of the archive from being modified without the embedded claims being
158    /// re-signed. This will load all binaries into memory in the returned `ProviderArchive`. Use
159    /// [`load`] or [`try_load_target_from_file`]  methods if you only want to load a single binary
160    /// into memory.
161    pub async fn try_load_file(path: impl AsRef<Path>) -> Result<ProviderArchive> {
162        let mut file = File::open(&path).await.map_err(|e| {
163            std::io::Error::new(
164                e.kind(),
165                format!(
166                    "failed to load PAR from file [{}]: {e}",
167                    path.as_ref().display()
168                ),
169            )
170        })?;
171        Self::load(&mut file, None).await
172    }
173
174    /// Attempts to read a Provider Archive (PAR) file to analyze and verify its contents.
175    ///
176    /// The embedded claims in this archive will be validated, and the file hashes contained in
177    /// those claims will be compared and verified against hashes computed at load time. This
178    /// prevents the contents of the archive from being modified without the embedded claims being
179    /// re-signed. This will only read a single binary into memory.
180    ///
181    /// It is recommended to use this method or the [`load`] method when consuming a provider
182    /// archive. Otherwise all binaries will be loaded into memory
183    pub async fn try_load_target_from_file(
184        path: impl AsRef<Path>,
185        target: &str,
186    ) -> Result<ProviderArchive> {
187        let mut file = File::open(&path).await.map_err(|e| {
188            std::io::Error::new(
189                e.kind(),
190                format!(
191                    "failed to load target [{target}] from PAR from file [{}]: {e}",
192                    path.as_ref().display()
193                ),
194            )
195        })?;
196        Self::load(&mut file, Some(target)).await
197    }
198
199    /// Attempts to read a Provider Archive (PAR) from a Reader to analyze and verify its contents.
200    /// The optional `target` parameter allows you to select a single binary to load
201    ///
202    /// The embedded claims in this archive will be validated, and the file hashes contained in
203    /// those claims will be compared and verified against hashes computed at load time. This
204    /// prevents the contents of the archive from being modified without the embedded claims being
205    /// re-signed. If a `target` is specified, this will only read a single binary into memory.
206    ///
207    /// This is the most generic loading option available and allows you to load from anything that
208    /// implements `AsyncRead` and `AsyncSeek`
209    pub async fn load<R: AsyncRead + AsyncSeek + Unpin + Send + Sync>(
210        input: &mut R,
211        target: Option<&str>,
212    ) -> Result<ProviderArchive> {
213        let mut libraries = HashMap::new();
214        let mut wit_world = None;
215
216        let mut magic = [0; 2];
217        if let Err(e) = input.read_exact(&mut magic).await {
218            // If we can't fill the buffer, it isn't a valid par file
219            if matches!(e.kind(), std::io::ErrorKind::UnexpectedEof) {
220                return Err("Not enough bytes to be a valid PAR file".into());
221            }
222            return Err(e.into());
223        }
224
225        // Seek back to beginning
226        input.rewind().await?;
227
228        let mut par = Archive::new(if magic == GZIP_MAGIC {
229            Box::new(GzipDecoder::new(BufReader::new(input)))
230                as Box<dyn AsyncRead + Unpin + Sync + Send>
231        } else {
232            Box::new(input) as Box<dyn AsyncRead + Unpin + Sync + Send>
233        });
234
235        let mut token: Option<Token<CapabilityProvider>> = None;
236
237        let mut entries = par.entries()?;
238
239        while let Some(res) = entries.next().await {
240            let mut entry = res?;
241            let mut bytes = Vec::new();
242            let file_target = PathBuf::from(entry.path()?)
243                .file_stem()
244                .unwrap()
245                .to_str()
246                .unwrap()
247                .to_string();
248            if file_target == "claims" {
249                tokio::io::copy(&mut entry, &mut bytes).await?;
250                let jwt = std::str::from_utf8(&bytes)?;
251                let claims = Some(Claims::<CapabilityProvider>::decode(jwt)?);
252                token = claims.map(|claims| Token {
253                    jwt: jwt.to_string(),
254                    claims,
255                });
256            } else if file_target == "world" {
257                tokio::io::copy(&mut entry, &mut bytes).await?;
258                wit_world = Some(bytes);
259            } else if let Some(t) = target {
260                // If loading only a specific target, only copy in bytes if it is the target. We still
261                // need to iterate through the rest so we can be sure to find the claims
262                if file_target == t {
263                    tokio::io::copy(&mut entry, &mut bytes).await?;
264                    libraries.insert(file_target.to_string(), bytes);
265                }
266                continue;
267            } else {
268                tokio::io::copy(&mut entry, &mut bytes).await?;
269                libraries.insert(file_target.to_string(), bytes);
270            }
271        }
272
273        if token.is_none() || libraries.is_empty() {
274            // we need at least claims.jwt and one plugin binary
275            libraries.clear();
276            return Err(
277                "Not enough files found in provider archive. Is this a complete archive?".into(),
278            );
279        }
280
281        if let Some(ref claims_token) = token {
282            let cl = &claims_token.claims;
283            let metadata = cl.metadata.as_ref().unwrap();
284            let name = cl.name();
285            let vendor = metadata.vendor.to_string();
286            let rev = metadata.rev;
287            let ver = metadata.ver.clone();
288            let json_schema = metadata.config_schema.clone();
289
290            validate_hashes(&libraries, &wit_world, cl)?;
291
292            Ok(ProviderArchive {
293                libraries,
294                name,
295                vendor,
296                rev,
297                ver,
298                token,
299                json_schema,
300                wit: wit_world,
301            })
302        } else {
303            Err("No claims found embedded in provider archive.".into())
304        }
305    }
306
307    /// Generates a Provider Archive (PAR) file with all of the library files and a signed set of claims in an embedded JWT
308    pub async fn write(
309        &mut self,
310        destination: impl AsRef<Path>,
311        issuer: &KeyPair,
312        subject: &KeyPair,
313        compress_par: bool,
314    ) -> Result<()> {
315        let file = File::create(
316            if compress_par && destination.as_ref().extension().unwrap_or_default() != "gz" {
317                let mut file_name = destination
318                    .as_ref()
319                    .file_name()
320                    .ok_or("Destination is not a file")?
321                    .to_owned();
322                file_name.push(".gz");
323                destination.as_ref().with_file_name(file_name)
324            } else {
325                destination.as_ref().to_owned()
326            },
327        )
328        .await?;
329
330        let mut par = tokio_tar::Builder::new(if compress_par {
331            Box::new(GzipEncoder::with_quality(file, Level::Best))
332                as Box<dyn AsyncWrite + Send + Sync + Unpin>
333        } else {
334            Box::new(file) as Box<dyn AsyncWrite + Send + Sync + Unpin>
335        });
336
337        let mut claims = Claims::<CapabilityProvider>::new(
338            self.name.to_string(),
339            issuer.public_key(),
340            subject.public_key(),
341            self.vendor.to_string(),
342            self.rev,
343            self.ver.clone(),
344            generate_hashes(&self.libraries, &self.wit),
345        );
346        if let Some(schema) = self.json_schema.clone() {
347            claims.metadata.as_mut().unwrap().config_schema = Some(schema);
348        }
349
350        let claims_jwt = claims.encode(issuer)?;
351        self.token = Some(Token {
352            jwt: claims_jwt.clone(),
353            claims,
354        });
355
356        let mut header = tokio_tar::Header::new_gnu();
357        header.set_path(CLAIMS_JWT_FILE)?;
358        header.set_size(claims_jwt.len() as u64);
359        header.set_cksum();
360        par.append_data(&mut header, CLAIMS_JWT_FILE, Cursor::new(claims_jwt))
361            .await?;
362
363        if let Some(world) = &self.wit {
364            let mut header = tokio_tar::Header::new_gnu();
365            header.set_path(WIT_WORLD_FILE)?;
366            header.set_size(world.len() as u64);
367            header.set_cksum();
368            par.append_data(&mut header, WIT_WORLD_FILE, Cursor::new(world))
369                .await?;
370        }
371
372        for (tgt, lib) in &self.libraries {
373            let mut header = tokio_tar::Header::new_gnu();
374            let path = format!("{tgt}.bin");
375            header.set_path(&path)?;
376            header.set_size(lib.len() as u64);
377            header.set_cksum();
378            par.append_data(&mut header, &path, Cursor::new(lib))
379                .await?;
380        }
381
382        // Completes the process of packing a .par archive
383        let mut inner = par.into_inner().await?;
384        // Make sure everything is flushed to disk, otherwise we might miss closing data block
385        inner.flush().await?;
386        inner.shutdown().await?;
387
388        Ok(())
389    }
390}
391
392fn validate_hashes(
393    libraries: &HashMap<String, Vec<u8>>,
394    wit: &Option<Vec<u8>>,
395    claims: &Claims<CapabilityProvider>,
396) -> Result<()> {
397    let file_hashes = claims.metadata.as_ref().unwrap().target_hashes.clone();
398
399    for (tgt, library) in libraries {
400        let file_hash = file_hashes.get(tgt).cloned().unwrap();
401        let check_hash = hash_bytes(library);
402        if file_hash != check_hash {
403            return Err(format!("File hash and verify hash do not match for '{tgt}'").into());
404        }
405    }
406
407    if let Some(interface) = wit {
408        if let Some(wit_hash) = file_hashes.get(WIT_WORLD_FILE) {
409            let check_hash = hash_bytes(interface);
410            if wit_hash != &check_hash {
411                return Err("WIT interface hash does not match".into());
412            }
413        } else if wit.is_some() {
414            return Err("WIT interface present but no hash found in claims".into());
415        }
416    }
417    Ok(())
418}
419
420fn generate_hashes(
421    libraries: &HashMap<String, Vec<u8>>,
422    wit: &Option<Vec<u8>>,
423) -> HashMap<String, String> {
424    let mut hm = HashMap::new();
425    for (target, lib) in libraries {
426        let hash = hash_bytes(lib);
427        hm.insert(target.to_string(), hash);
428    }
429
430    if let Some(interface) = wit {
431        let hash = hash_bytes(interface);
432        hm.insert(WIT_WORLD_FILE.to_string(), hash);
433    }
434
435    hm
436}
437
438fn hash_bytes(bytes: &[u8]) -> String {
439    let digest = sha256_digest(bytes).unwrap();
440    HEXUPPER.encode(digest.as_ref())
441}
442
443fn sha256_digest<R: Read>(mut reader: R) -> Result<Digest> {
444    let mut context = Context::new(&SHA256);
445    let mut buffer = [0; 1024];
446
447    loop {
448        let count = reader.read(&mut buffer)?;
449        if count == 0 {
450            break;
451        }
452        context.update(&buffer[..count]);
453    }
454
455    Ok(context.finish())
456}
457
458#[cfg(test)]
459mod test {
460    use super::*;
461    use serde_json::json;
462    use wascap::prelude::KeyPair;
463
464    #[tokio::test]
465    async fn write_par() -> Result<()> {
466        let tempdir = tempfile::tempdir()?;
467        let mut arch =
468            ProviderArchive::new("Testing", "wasmCloud", Some(1), Some("0.0.1".to_string()));
469        arch.add_library("aarch64-linux", b"blahblah")?;
470
471        let issuer = KeyPair::new_account();
472        let subject = KeyPair::new_service();
473
474        let outpath = tempdir.path().join("writetest.par");
475        arch.write(&outpath, &issuer, &subject, false).await?;
476        tokio::fs::metadata(outpath)
477            .await
478            .expect("Unable to locate newly created par file");
479
480        Ok(())
481    }
482
483    #[tokio::test]
484    async fn error_on_no_providers() -> Result<()> {
485        let mut arch =
486            ProviderArchive::new("Testing", "wasmCloud", Some(2), Some("0.0.2".to_string()));
487
488        let tempdir = tempfile::tempdir()?;
489
490        let issuer = KeyPair::new_account();
491        let subject = KeyPair::new_service();
492
493        let outpath = tempdir.path().join("shoulderr.par");
494        arch.write(&outpath, &issuer, &subject, false).await?;
495
496        let mut buf2 = Vec::new();
497        let mut f2 = File::open(outpath).await?;
498        f2.read_to_end(&mut buf2).await?;
499
500        let arch2 = ProviderArchive::try_load(&buf2).await;
501
502        match arch2 {
503            Ok(_notok) => panic!("Loading an archive without any libraries should fail"),
504            Err(_e) => (),
505        }
506
507        Ok(())
508    }
509
510    #[tokio::test]
511    async fn round_trip() -> Result<()> {
512        // Build an archive in memory the way a CLI wrapper might...
513        let mut arch =
514            ProviderArchive::new("Testing", "wasmCloud", Some(3), Some("0.0.3".to_string()));
515        arch.add_library("aarch64-linux", b"blahblah")?;
516        arch.add_library("x86_64-linux", b"bloobloo")?;
517        arch.add_library("x86_64-macos", b"blarblar")?;
518        arch.set_schema(json!({"property":"foo"}))?;
519
520        let issuer = KeyPair::new_account();
521        let subject = KeyPair::new_service();
522
523        let tempdir = tempfile::tempdir()?;
524
525        let firstpath = tempdir.path().join("firstarchive.par");
526        let secondpath = tempdir.path().join("secondarchive.par");
527
528        // Generate the .par file with embedded claims.jwt file (needs a service and an account key)
529        arch.write(&firstpath, &issuer, &subject, false).await?;
530
531        // Try loading from file
532        let arch2 = ProviderArchive::try_load_file(&firstpath).await?;
533        assert_eq!(
534            arch.libraries.get("aarch64-linux"),
535            arch2.libraries.get("aarch64-linux")
536        );
537        assert_eq!(
538            arch.libraries.get("x86_64-macos"),
539            arch2.libraries.get("x86_64-macos")
540        );
541        assert_eq!(arch.claims().unwrap().subject, subject.public_key());
542
543        // Load just one of the binaries
544        let arch2 = ProviderArchive::try_load_target_from_file(&firstpath, "aarch64-linux").await?;
545        assert_eq!(
546            arch.libraries.get("aarch64-linux"),
547            arch2.libraries.get("aarch64-linux")
548        );
549        assert!(
550            !arch2.libraries.contains_key("x86_64-macos"),
551            "Should have loaded only one binary"
552        );
553        assert_eq!(
554            arch2.claims().unwrap().subject,
555            subject.public_key(),
556            "Claims should still load"
557        );
558
559        let json = arch2
560            .claims()
561            .unwrap()
562            .metadata
563            .unwrap()
564            .config_schema
565            .unwrap();
566        assert_eq!(json, json!({"property":"foo"}));
567
568        let mut buf2 = Vec::new();
569        let mut f2 = File::open(&firstpath).await?;
570        f2.read_to_end(&mut buf2).await?;
571
572        // Make sure the file we wrote can be read back in with no data loss
573        let mut arch2 = ProviderArchive::try_load(&buf2).await?;
574        assert_eq!(
575            arch.libraries.get("aarch64-linux"),
576            arch2.libraries.get("aarch64-linux")
577        );
578        assert_eq!(arch.claims().unwrap().subject, subject.public_key());
579
580        // Another common task - read an existing archive and add another library file to it
581        arch2.add_library("mips-linux", b"bluhbluh")?;
582        arch2.write(&secondpath, &issuer, &subject, false).await?;
583
584        let mut buf3 = Vec::new();
585        let mut f3 = File::open(&secondpath).await?;
586        f3.read_to_end(&mut buf3).await?;
587
588        // Make sure the re-written/modified archive looks the way we expect
589        let arch3 = ProviderArchive::try_load(&buf3).await?;
590        assert_eq!(
591            arch3.libraries[&"aarch64-linux".to_string()],
592            arch2.libraries[&"aarch64-linux".to_string()]
593        );
594        assert_eq!(arch3.claims().unwrap().subject, subject.public_key());
595        assert_eq!(arch3.targets().len(), 4);
596
597        Ok(())
598    }
599
600    #[tokio::test]
601    async fn compression_roundtrip() -> Result<()> {
602        let mut arch =
603            ProviderArchive::new("Testing", "wasmCloud", Some(4), Some("0.0.4".to_string()));
604        arch.add_library("aarch64-linux", b"heylookimaraspberrypi")?;
605        arch.add_library("x86_64-linux", b"system76")?;
606        arch.add_library("x86_64-macos", b"16inchmacbookpro")?;
607
608        let issuer = KeyPair::new_account();
609        let subject = KeyPair::new_service();
610
611        let filename = "computers";
612
613        let tempdir = tempfile::tempdir()?;
614
615        let parpath = tempdir.path().join(format!("{filename}.par"));
616        let cheezypath = tempdir.path().join(format!("{filename}.par.gz"));
617
618        arch.write(&parpath, &issuer, &subject, false).await?;
619        arch.write(&cheezypath, &issuer, &subject, true).await?;
620
621        let mut buf2 = Vec::new();
622        let mut f2 = File::open(&parpath).await?;
623        f2.read_to_end(&mut buf2).await?;
624
625        let mut buf3 = Vec::new();
626        let mut f3 = File::open(&cheezypath).await?;
627        f3.read_to_end(&mut buf3).await?;
628
629        // Make sure the file we wrote compressed can be read back in with no data loss
630        let arch2 = ProviderArchive::try_load(&buf3).await?;
631        assert_eq!(
632            arch.libraries[&"aarch64-linux".to_string()],
633            arch2.libraries[&"aarch64-linux".to_string()]
634        );
635        assert_eq!(arch.claims().unwrap().subject, subject.public_key());
636
637        // Try loading from file as well
638        let arch2 = ProviderArchive::try_load_file(&cheezypath).await?;
639        assert_eq!(
640            arch.libraries.get("aarch64-linux"),
641            arch2.libraries.get("aarch64-linux")
642        );
643        assert_eq!(arch.claims().unwrap().subject, subject.public_key());
644
645        Ok(())
646    }
647
648    #[tokio::test]
649    async fn wit_compression_roundtrip() -> Result<()> {
650        let mut arch =
651            ProviderArchive::new("Testing", "wasmCloud", Some(4), Some("0.0.4".to_string()));
652
653        // Add both libraries and WIT data
654        arch.add_library("aarch64-linux", b"heylookimaraspberrypi")?;
655        arch.add_library("x86_64-linux", b"system76")?;
656        arch.add_library("x86_64-macos", b"16inchmacbookpro")?;
657        arch.add_wit_world(b"interface world example { resource config {} }")?;
658
659        let issuer = KeyPair::new_account();
660        let subject = KeyPair::new_service();
661
662        let filename = "wit_test";
663        let tempdir = tempfile::tempdir()?;
664
665        let parpath = tempdir.path().join(format!("{filename}.par"));
666        let cheezypath = tempdir.path().join(format!("{filename}.par.gz"));
667
668        // Write both compressed and uncompressed
669        arch.write(&parpath, &issuer, &subject, false).await?;
670        arch.write(&cheezypath, &issuer, &subject, true).await?;
671
672        // Read both files into memory
673        let mut buf2 = Vec::new();
674        let mut f2 = File::open(&parpath).await?;
675        f2.read_to_end(&mut buf2).await?;
676
677        let mut buf3 = Vec::new();
678        let mut f3 = File::open(&cheezypath).await?;
679        f3.read_to_end(&mut buf3).await?;
680
681        // Test uncompressed archive
682        let arch2 = ProviderArchive::try_load(&buf2).await?;
683        assert_eq!(
684            arch.libraries[&"aarch64-linux".to_string()],
685            arch2.libraries[&"aarch64-linux".to_string()]
686        );
687        assert_eq!(arch.wit_world(), arch2.wit_world());
688        assert_eq!(arch.claims().unwrap().subject, subject.public_key());
689
690        // Test compressed archive
691        let arch3 = ProviderArchive::try_load(&buf3).await?;
692        assert_eq!(
693            arch.libraries[&"aarch64-linux".to_string()],
694            arch3.libraries[&"aarch64-linux".to_string()]
695        );
696        assert_eq!(arch.wit_world(), arch3.wit_world());
697        assert_eq!(arch.claims().unwrap().subject, subject.public_key());
698
699        // Test loading directly from files
700        let arch4 = ProviderArchive::try_load_file(&parpath).await?;
701        assert_eq!(arch.wit_world(), arch4.wit_world());
702
703        let arch5 = ProviderArchive::try_load_file(&cheezypath).await?;
704        assert_eq!(arch.wit_world(), arch5.wit_world());
705
706        // Verify WIT hash in claims
707        let claims = arch5.claims().unwrap();
708        let hashes = claims.metadata.unwrap().target_hashes;
709        assert!(hashes.contains_key("world.wasm"));
710
711        Ok(())
712    }
713
714    #[tokio::test]
715    async fn valid_write_compressed() -> Result<()> {
716        let mut arch =
717            ProviderArchive::new("Testing", "wasmCloud", Some(6), Some("0.0.6".to_string()));
718        arch.add_library("x86_64-linux", b"linux")?;
719        arch.add_library("arm-macos", b"macos")?;
720        arch.add_library("mips64-freebsd", b"freebsd")?;
721
722        let filename = "multi-os";
723
724        let issuer = KeyPair::new_account();
725        let subject = KeyPair::new_service();
726
727        let tempdir = tempfile::tempdir()?;
728
729        arch.write(
730            tempdir.path().join(format!("{filename}.par")),
731            &issuer,
732            &subject,
733            true,
734        )
735        .await?;
736
737        let arch2 =
738            ProviderArchive::try_load_file(tempdir.path().join(format!("{filename}.par.gz")))
739                .await?;
740
741        assert_eq!(
742            arch.libraries[&"x86_64-linux".to_string()],
743            arch2.libraries[&"x86_64-linux".to_string()]
744        );
745        assert_eq!(
746            arch.libraries[&"arm-macos".to_string()],
747            arch2.libraries[&"arm-macos".to_string()]
748        );
749        assert_eq!(
750            arch.libraries[&"mips64-freebsd".to_string()],
751            arch2.libraries[&"mips64-freebsd".to_string()]
752        );
753        assert_eq!(arch.claims(), arch2.claims());
754
755        Ok(())
756    }
757
758    #[tokio::test]
759    async fn valid_write_compressed_with_wit() -> Result<()> {
760        let mut arch =
761            ProviderArchive::new("Testing", "wasmCloud", Some(6), Some("0.0.6".to_string()));
762
763        // Add libraries and WIT
764        arch.add_library("x86_64-linux", b"linux")?;
765        arch.add_library("arm-macos", b"macos")?;
766        arch.add_library("mips64-freebsd", b"freebsd")?;
767        arch.add_wit_world(b"interface world capability { resource handler {} }")?;
768
769        let filename = "multi-os-wit";
770        let issuer = KeyPair::new_account();
771        let subject = KeyPair::new_service();
772
773        let tempdir = tempfile::tempdir()?;
774        arch.write(
775            tempdir.path().join(format!("{filename}.par")),
776            &issuer,
777            &subject,
778            true,
779        )
780        .await?;
781
782        let arch2 =
783            ProviderArchive::try_load_file(tempdir.path().join(format!("{filename}.par.gz")))
784                .await?;
785
786        // Verify libraries
787        assert_eq!(
788            arch.libraries[&"x86_64-linux".to_string()],
789            arch2.libraries[&"x86_64-linux".to_string()]
790        );
791        assert_eq!(
792            arch.libraries[&"arm-macos".to_string()],
793            arch2.libraries[&"arm-macos".to_string()]
794        );
795        assert_eq!(
796            arch.libraries[&"mips64-freebsd".to_string()],
797            arch2.libraries[&"mips64-freebsd".to_string()]
798        );
799
800        // Verify WIT and claims
801        assert_eq!(arch.wit_world(), arch2.wit_world());
802        assert_eq!(arch.claims(), arch2.claims());
803
804        Ok(())
805    }
806
807    #[tokio::test]
808    async fn valid_write_compressed_with_suffix() -> Result<()> {
809        let mut arch =
810            ProviderArchive::new("Testing", "wasmCloud", Some(7), Some("0.0.7".to_string()));
811        arch.add_library("x86_64-linux", b"linux")?;
812        arch.add_library("arm-macos", b"macos")?;
813        arch.add_library("mips64-freebsd", b"freebsd")?;
814
815        let filename = "suffix-test";
816
817        let issuer = KeyPair::new_account();
818        let subject = KeyPair::new_service();
819
820        let tempdir = tempfile::tempdir()?;
821        let cheezypath = tempdir.path().join(format!("{filename}.par.gz"));
822
823        // the gz suffix is explicitly provided to write
824        arch.write(&cheezypath, &issuer, &subject, true)
825            .await
826            .expect("Unable to write parcheezy");
827
828        let arch2 = ProviderArchive::try_load_file(&cheezypath)
829            .await
830            .expect("Unable to load parcheezy from file");
831
832        assert_eq!(
833            arch.libraries[&"x86_64-linux".to_string()],
834            arch2.libraries[&"x86_64-linux".to_string()]
835        );
836        assert_eq!(
837            arch.libraries[&"arm-macos".to_string()],
838            arch2.libraries[&"arm-macos".to_string()]
839        );
840        assert_eq!(
841            arch.libraries[&"mips64-freebsd".to_string()],
842            arch2.libraries[&"mips64-freebsd".to_string()]
843        );
844        assert_eq!(arch.claims(), arch2.claims());
845
846        Ok(())
847    }
848
849    #[tokio::test]
850    async fn preserved_claims() -> Result<()> {
851        // Build an archive in memory the way a CLI wrapper might...
852        let name = "Testing";
853        let vendor = "wasmCloud";
854        let rev = 8;
855        let ver = "0.0.8".to_string();
856        let mut arch = ProviderArchive::new(name, vendor, Some(rev), Some(ver.clone()));
857        arch.add_library("aarch64-linux", b"blahblah")?;
858        arch.add_library("x86_64-linux", b"bloobloo")?;
859        arch.add_library("x86_64-macos", b"blarblar")?;
860
861        let issuer = KeyPair::new_account();
862        let subject = KeyPair::new_service();
863
864        let tempdir = tempfile::tempdir()?;
865        let originalpath = tempdir.path().join("original.par.gz");
866        let addedpath = tempdir.path().join("linuxadded.par.gz");
867
868        arch.write(&originalpath, &issuer, &subject, true).await?;
869
870        // Make sure the file we wrote can be read back in with no claims loss
871        let mut arch2 = ProviderArchive::try_load_file(&originalpath).await?;
872
873        assert_eq!(
874            arch.libraries[&"aarch64-linux".to_string()],
875            arch2.libraries[&"aarch64-linux".to_string()]
876        );
877        assert_eq!(arch2.claims().unwrap().subject, subject.public_key());
878        assert_eq!(arch2.claims().unwrap().issuer, issuer.public_key());
879        assert_eq!(arch2.claims().unwrap().name(), name);
880        assert_eq!(arch2.claims().unwrap().metadata.unwrap().ver.unwrap(), ver);
881        assert_eq!(arch2.claims().unwrap().metadata.unwrap().rev.unwrap(), rev);
882        assert_eq!(arch2.claims().unwrap().metadata.unwrap().vendor, vendor);
883
884        // Another common task - read an existing archive and add another library file to it
885        arch2.add_library("mips-linux", b"bluhbluh")?;
886        arch2.write(&addedpath, &issuer, &subject, true).await?;
887
888        // Make sure the re-written/modified archive looks the way we expect
889        let arch3 = ProviderArchive::try_load_file(&addedpath).await?;
890        assert_eq!(
891            arch3.libraries[&"aarch64-linux".to_string()],
892            arch2.libraries[&"aarch64-linux".to_string()]
893        );
894        assert_eq!(arch3.claims().unwrap().subject, subject.public_key());
895        assert_eq!(arch3.claims().unwrap().issuer, issuer.public_key());
896        assert_eq!(arch3.claims().unwrap().name(), name);
897        assert_eq!(arch3.claims().unwrap().metadata.unwrap().ver.unwrap(), ver);
898        assert_eq!(arch3.claims().unwrap().metadata.unwrap().rev.unwrap(), rev);
899        assert_eq!(arch3.claims().unwrap().metadata.unwrap().vendor, vendor);
900        assert_eq!(arch3.targets().len(), 4);
901
902        Ok(())
903    }
904
905    /// Ensures backwards compatibility with PAR that do not contain WIT
906    #[tokio::test]
907    async fn witless_archive() -> Result<()> {
908        // First create an "old style" archive without WIT
909        let mut old_arch =
910            ProviderArchive::new("OldStyle", "wasmCloud", Some(1), Some("0.0.1".to_string()));
911        old_arch.add_library("x86_64-linux", b"oldbin")?;
912
913        let issuer = KeyPair::new_account();
914        let subject = KeyPair::new_service();
915
916        let tempdir = tempfile::tempdir()?;
917        let old_path = tempdir.path().join("old_style.par");
918
919        // Write old archive
920        old_arch.write(&old_path, &issuer, &subject, false).await?;
921
922        let loaded_arch = ProviderArchive::try_load_file(&old_path).await?;
923        assert_eq!(loaded_arch.wit_world(), None); // No WIT data
924        assert_eq!(
925            loaded_arch.libraries.get("x86_64-linux"),
926            old_arch.libraries.get("x86_64-linux")
927        );
928
929        Ok(())
930    }
931}