wasmer_package/package/
package.rs

1#![allow(
2    clippy::result_large_err,
3    reason = "WasmerPackageError is large, but not often used"
4)]
5
6use std::{
7    borrow::Cow,
8    collections::{BTreeMap, BTreeSet},
9    fmt::Debug,
10    fs::File,
11    io::{BufRead, BufReader},
12    path::{Path, PathBuf},
13    sync::Arc,
14};
15
16use anyhow::{Context, Error};
17use bytes::Bytes;
18use flate2::bufread::GzDecoder;
19use shared_buffer::OwnedBuffer;
20use tar::Archive;
21use tempfile::TempDir;
22use wasmer_config::package::Manifest as WasmerManifest;
23
24use webc::{
25    AbstractVolume, AbstractWebc, Container, ContainerError, DetectError, PathSegment, Version,
26    Volume,
27    metadata::{Manifest as WebcManifest, annotations::Wapm},
28    v3::{
29        ChecksumAlgorithm, Timestamps,
30        write::{FileEntry, Writer},
31    },
32};
33
34use super::{
35    ManifestError, MemoryVolume, Strictness,
36    manifest::wasmer_manifest_to_webc,
37    volume::{WasmerPackageVolume, fs::FsVolume},
38};
39
40/// Errors that may occur while loading a Wasmer package from disk.
41#[derive(Debug, thiserror::Error)]
42#[allow(clippy::result_large_err)]
43#[non_exhaustive]
44pub enum WasmerPackageError {
45    /// Unable to create a temporary directory.
46    #[error("Unable to create a temporary directory")]
47    TempDir(#[source] std::io::Error),
48    /// Unable to open a file.
49    #[error("Unable to open \"{}\"", path.display())]
50    FileOpen {
51        /// The file being opened.
52        path: PathBuf,
53        /// The underlying error.
54        #[source]
55        error: std::io::Error,
56    },
57    /// Unable to read a file.
58    #[error("Unable to read \"{}\"", path.display())]
59    FileRead {
60        /// The file being opened.
61        path: PathBuf,
62        /// The underlying error.
63        #[source]
64        error: std::io::Error,
65    },
66
67    /// Generic IO error.
68    #[error("IO Error: {0:?}")]
69    IoError(#[from] std::io::Error),
70
71    /// Unexpected path format
72    #[error("Malformed path format: {0:?}")]
73    MalformedPath(PathBuf),
74
75    /// Unable to extract the tarball.
76    #[error("Unable to extract the tarball")]
77    Tarball(#[source] std::io::Error),
78    /// Unable to deserialize the `wasmer.toml` file.
79    #[error("Unable to deserialize \"{}\"", path.display())]
80    TomlDeserialize {
81        /// The file being deserialized.
82        path: PathBuf,
83        /// The underlying error.
84        #[source]
85        error: toml::de::Error,
86    },
87    /// Unable to deserialize a json file.
88    #[error("Unable to deserialize \"{}\"", path.display())]
89    JsonDeserialize {
90        /// The file being deserialized.
91        path: PathBuf,
92        /// The underlying error.
93        #[source]
94        error: serde_json::Error,
95    },
96    /// Unable to find the `wasmer.toml` file.
97    #[error("Unable to find the \"wasmer.toml\"")]
98    MissingManifest,
99    /// Unable to canonicalize a path.
100    #[error("Unable to get the absolute path for \"{}\"", path.display())]
101    Canonicalize {
102        /// The path being canonicalized.
103        path: PathBuf,
104        /// The underlying error.
105        #[source]
106        error: std::io::Error,
107    },
108    /// Unable to load the `wasmer.toml` manifest.
109    #[error("Unable to load the \"wasmer.toml\" manifest")]
110    Manifest(#[from] ManifestError),
111    /// A manifest validation error.
112    #[error("The manifest is invalid")]
113    Validation(#[from] wasmer_config::package::ValidationError),
114    /// A path in the fs mapping does not exist
115    #[error("Path: \"{}\" does not exist", path.display())]
116    PathNotExists {
117        /// Path entry in fs mapping
118        path: PathBuf,
119    },
120    /// Any error happening when populating the volumes tree map of a package
121    #[error("Volume creation failed: {0:?}")]
122    VolumeCreation(#[from] anyhow::Error),
123
124    /// Error when serializing or deserializing
125    #[error("serde error: {0:?}")]
126    SerdeError(#[from] ciborium::value::Error),
127
128    /// Container Error
129    #[error("container error: {0:?}")]
130    ContainerError(#[from] ContainerError),
131
132    /// Detect Error
133    #[error("detect error: {0:?}")]
134    DetectError(#[from] DetectError),
135}
136
137// Serious Java vibes from this one! Still better than repeating the
138// function type everywhere.
139// I'm opting not to use a trait object here to avoid generics and boxing.
140#[derive(Clone, Copy)]
141pub struct WalkBuilderFactory {
142    pub walker_factory: fn(&Path) -> ignore::WalkBuilder,
143}
144
145impl Debug for WalkBuilderFactory {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.debug_struct("WalkBuilderFactory").finish()
148    }
149}
150
151impl WalkBuilderFactory {
152    pub fn new(walker_factory: fn(&Path) -> ignore::WalkBuilder) -> Self {
153        Self { walker_factory }
154    }
155
156    pub fn create_walk_builder(&self, path: &Path) -> ignore::WalkBuilder {
157        (self.walker_factory)(path)
158    }
159}
160
161fn is_wasmer_ignore_present(path: &Path) -> bool {
162    path.ancestors().any(|p| p.join(".wasmerignore").is_file())
163}
164
165/// Create and configure a default ignore walker for building packages that:
166///   * If a `.wasmerignore` file is present, it will be used to ignore files.
167///   * Otherwise, only `.git` folders will be ignored.
168pub fn wasmer_ignore_walker() -> WalkBuilderFactory {
169    fn walker_factory(root_dir: &Path) -> ignore::WalkBuilder {
170        let mut builder = ignore::WalkBuilder::new(root_dir);
171        // We always add .wasmerignore because it could exist in a sub-directory.
172        builder
173            .standard_filters(false)
174            .follow_links(false)
175            .parents(true)
176            .add_custom_ignore_filename(".wasmerignore");
177
178        // However, if there is no .wasmerignore in the root directory,
179        // we want to at least ignore .git folders. This setup creates the
180        // very niche edge case of having .wasmerignore in a subdirectory
181        // causing .git folders to be ignored in other directories, but
182        // that seems acceptable given how rare that would be. The proper
183        // solution would be to do this logic per-directory, but that's
184        // not supported by `ignore`.
185        if !is_wasmer_ignore_present(root_dir) {
186            let mut overrides = ignore::overrides::OverrideBuilder::new(root_dir);
187            overrides.add("!.git").expect("valid override");
188            builder.overrides(overrides.build().expect("valid overrides"));
189        }
190
191        builder
192    }
193    WalkBuilderFactory::new(walker_factory)
194}
195
196pub fn include_everything_walker() -> WalkBuilderFactory {
197    fn walker_factory(root_dir: &Path) -> ignore::WalkBuilder {
198        let mut builder = ignore::WalkBuilder::new(root_dir);
199        builder.standard_filters(false).follow_links(false);
200        builder
201    }
202    WalkBuilderFactory::new(walker_factory)
203}
204
205/// A Wasmer package that will be lazily loaded from disk.
206#[derive(Debug)]
207pub struct Package {
208    // base dir could be a temp dir, so we keep it around to prevent the directory
209    // from being deleted
210    #[allow(dead_code)]
211    base_dir: BaseDir,
212    manifest: WebcManifest,
213    atoms: BTreeMap<String, OwnedBuffer>,
214    strictness: Strictness,
215    volumes: BTreeMap<String, Arc<dyn WasmerPackageVolume + Send + Sync + 'static>>,
216}
217
218impl Package {
219    /// Load a [`Package`] from a `*.tar.gz` file on disk.
220    ///
221    /// # Implementation Details
222    ///
223    /// This will unpack the tarball to a temporary directory on disk and use
224    /// memory-mapped files in order to reduce RAM usage.
225    pub fn from_tarball_file(path: impl AsRef<Path>) -> Result<Self, WasmerPackageError> {
226        Package::from_tarball_file_with_walker(path.as_ref(), include_everything_walker())
227    }
228
229    /// Load a [`Package`] from a `*.tar.gz` file on disk.
230    ///
231    /// # Implementation Details
232    ///
233    /// This will unpack the tarball to a temporary directory on disk and use
234    /// memory-mapped files in order to reduce RAM usage.
235    pub fn from_tarball_file_with_walker(
236        path: impl AsRef<Path>,
237        walker_factory: WalkBuilderFactory,
238    ) -> Result<Self, WasmerPackageError> {
239        Package::from_tarball_file_with_strictness(
240            path.as_ref(),
241            Strictness::default(),
242            walker_factory,
243        )
244    }
245
246    /// Load a [`Package`] from a `*.tar.gz` file on disk.
247    ///
248    /// # Implementation Details
249    ///
250    /// This will unpack the tarball to a temporary directory on disk and use
251    /// memory-mapped files in order to reduce RAM usage.
252    pub fn from_tarball_file_with_strictness(
253        path: impl AsRef<Path>,
254        strictness: Strictness,
255        walker_factory: WalkBuilderFactory,
256    ) -> Result<Self, WasmerPackageError> {
257        let path = path.as_ref();
258        let f = File::open(path).map_err(|error| WasmerPackageError::FileOpen {
259            path: path.to_path_buf(),
260            error,
261        })?;
262
263        Package::from_tarball_with_strictness(BufReader::new(f), strictness, walker_factory)
264    }
265
266    /// Load a package from a `*.tar.gz` archive.
267    pub fn from_tarball(tarball: impl BufRead) -> Result<Self, WasmerPackageError> {
268        Package::from_tarball_with_walker(tarball, include_everything_walker())
269    }
270
271    /// Load a package from a `*.tar.gz` archive.
272    pub fn from_tarball_with_walker(
273        tarball: impl BufRead,
274        walker_factory: WalkBuilderFactory,
275    ) -> Result<Self, WasmerPackageError> {
276        Package::from_tarball_with_strictness(tarball, Strictness::default(), walker_factory)
277    }
278
279    /// Load a package from a `*.tar.gz` archive.
280    pub fn from_tarball_with_strictness(
281        tarball: impl BufRead,
282        strictness: Strictness,
283        walker_factory: WalkBuilderFactory,
284    ) -> Result<Self, WasmerPackageError> {
285        let tarball = GzDecoder::new(tarball);
286        let temp = tempdir().map_err(WasmerPackageError::TempDir)?;
287        let archive = Archive::new(tarball);
288        unpack_archive(archive, temp.path()).map_err(WasmerPackageError::Tarball)?;
289
290        let (_manifest_path, manifest) = read_manifest(temp.path())?;
291
292        Package::load(manifest, temp, strictness, walker_factory)
293    }
294
295    /// Load a package from a `wasmer.toml` manifest on disk.
296    pub fn from_manifest(wasmer_toml: impl AsRef<Path>) -> Result<Self, WasmerPackageError> {
297        Package::from_manifest_with_walker(wasmer_toml, wasmer_ignore_walker())
298    }
299
300    /// Load a package from a `wasmer.toml` manifest on disk.
301    pub fn from_manifest_with_walker(
302        wasmer_toml: impl AsRef<Path>,
303        walker_factory: WalkBuilderFactory,
304    ) -> Result<Self, WasmerPackageError> {
305        Package::from_manifest_with_strictness(wasmer_toml, Strictness::default(), walker_factory)
306    }
307
308    /// Load a package from a `wasmer.toml` manifest on disk.
309    pub fn from_manifest_with_strictness(
310        wasmer_toml: impl AsRef<Path>,
311        strictness: Strictness,
312        walker_factory: WalkBuilderFactory,
313    ) -> Result<Self, WasmerPackageError> {
314        let path = wasmer_toml.as_ref();
315        let path = path
316            .canonicalize()
317            .map_err(|error| WasmerPackageError::Canonicalize {
318                path: path.to_path_buf(),
319                error,
320            })?;
321
322        let wasmer_toml =
323            std::fs::read_to_string(&path).map_err(|error| WasmerPackageError::FileRead {
324                path: path.to_path_buf(),
325                error,
326            })?;
327        let wasmer_toml: WasmerManifest =
328            toml::from_str(&wasmer_toml).map_err(|error| WasmerPackageError::TomlDeserialize {
329                path: path.to_path_buf(),
330                error,
331            })?;
332
333        let base_dir = path
334            .parent()
335            .expect("Canonicalizing should always result in a file with a parent directory")
336            .to_path_buf();
337
338        for path in wasmer_toml.fs.values() {
339            if !base_dir.join(path).exists() {
340                return Err(WasmerPackageError::PathNotExists { path: path.clone() });
341            }
342        }
343
344        Package::load(wasmer_toml, base_dir, strictness, walker_factory)
345    }
346
347    /// (Re)loads a package from a `manifest.json` file produced by unpacking a
348    /// Wasmer package archive.
349    pub fn from_json_manifest(manifest: PathBuf) -> Result<Self, WasmerPackageError> {
350        Self::from_json_manifest_with_strictness(manifest, Strictness::default())
351    }
352
353    /// (Re)loads a package from a `manifest.json` file produced by unpacking a
354    /// Wasmer package archive.
355    pub fn from_json_manifest_with_strictness(
356        manifest: PathBuf,
357        strictness: Strictness,
358    ) -> Result<Self, WasmerPackageError> {
359        let base_dir = manifest
360            .parent()
361            .expect("Canonicalizing should always result in a file with a parent directory")
362            .to_path_buf();
363
364        let base_dir: BaseDir = base_dir.into();
365
366        let contents = std::fs::read(&manifest)?;
367        let manifest: WebcManifest =
368            serde_json::from_slice(&contents).map_err(|e| WasmerPackageError::JsonDeserialize {
369                path: manifest.clone(),
370                error: e,
371            })?;
372
373        let mut atoms = BTreeMap::<String, OwnedBuffer>::new();
374        for atom in manifest.atoms.keys() {
375            let path = base_dir.path().join(atom);
376
377            let contents = std::fs::read(&path)
378                .map_err(|e| WasmerPackageError::FileRead { path, error: e })?;
379
380            atoms.insert(atom.clone(), contents.into());
381        }
382
383        let mut volumes: BTreeMap<String, Arc<dyn WasmerPackageVolume + Send + Sync + 'static>> =
384            BTreeMap::new();
385        if let Some(fs_mappings) = manifest.filesystem()? {
386            for entry in fs_mappings.iter() {
387                let mut dirs = BTreeSet::new();
388                let path = entry.volume_name.strip_prefix('/').ok_or_else(|| {
389                    WasmerPackageError::MalformedPath(PathBuf::from(&entry.volume_name))
390                })?;
391                let path = base_dir.path().join(path);
392                dirs.insert(path);
393
394                volumes.insert(
395                    entry.volume_name.clone(),
396                    Arc::new(FsVolume::new(
397                        entry.volume_name.clone(),
398                        base_dir.path().to_owned(),
399                        BTreeSet::new(),
400                        dirs,
401                        include_everything_walker(),
402                    )),
403                );
404            }
405        }
406
407        let mut files = BTreeSet::new();
408        for entry in std::fs::read_dir(base_dir.path().join(FsVolume::METADATA))? {
409            let entry = entry?;
410
411            files.insert(entry.path());
412        }
413
414        if let Some(wapm) = manifest.wapm().unwrap() {
415            if let Some(license_file) = wapm.license_file.as_ref() {
416                let path = license_file.path.strip_prefix('/').ok_or_else(|| {
417                    WasmerPackageError::MalformedPath(PathBuf::from(&license_file.path))
418                })?;
419                let path = base_dir.path().join(FsVolume::METADATA).join(path);
420
421                files.insert(path);
422            }
423
424            if let Some(readme_file) = wapm.readme.as_ref() {
425                let path = readme_file.path.strip_prefix('/').ok_or_else(|| {
426                    WasmerPackageError::MalformedPath(PathBuf::from(&readme_file.path))
427                })?;
428                let path = base_dir.path().join(FsVolume::METADATA).join(path);
429
430                files.insert(path);
431            }
432        }
433
434        volumes.insert(
435            FsVolume::METADATA.to_string(),
436            Arc::new(FsVolume::new_with_intermediate_dirs(
437                FsVolume::METADATA.to_string(),
438                base_dir.path().join(FsVolume::METADATA).to_owned(),
439                files,
440                BTreeSet::new(),
441                include_everything_walker(),
442            )),
443        );
444
445        Ok(Package {
446            base_dir,
447            manifest,
448            atoms,
449            strictness,
450            volumes,
451        })
452    }
453
454    /// Create a [`Package`] from an in-memory representation.
455    pub fn from_in_memory(
456        manifest: WasmerManifest,
457        volumes: BTreeMap<String, MemoryVolume>,
458        atoms: BTreeMap<String, (Option<String>, OwnedBuffer)>,
459        metadata: MemoryVolume,
460        strictness: Strictness,
461    ) -> Result<Self, WasmerPackageError> {
462        let mut new_volumes = BTreeMap::new();
463
464        for (k, v) in volumes.into_iter() {
465            new_volumes.insert(k, Arc::new(v) as _);
466        }
467
468        new_volumes.insert(MemoryVolume::METADATA.to_string(), Arc::new(metadata) as _);
469
470        let volumes = new_volumes;
471
472        let mut annotated_atoms = BTreeMap::new();
473
474        for (atom_name, (a, b)) in atoms {
475            let annotations =
476                if let Some(module) = manifest.modules.iter().find(|v| v.name == atom_name) {
477                    module.annotations.as_ref()
478                } else {
479                    None
480                };
481
482            annotated_atoms.insert(atom_name, (a, b, annotations));
483        }
484
485        let (mut manifest, atoms) =
486            super::manifest::in_memory_wasmer_manifest_to_webc(&manifest, &annotated_atoms)?;
487
488        if let Some(entry) = manifest.package.get_mut(Wapm::KEY) {
489            let mut wapm: Wapm = entry.deserialized()?;
490
491            wapm.name.take();
492            wapm.version.take();
493            wapm.description.take();
494
495            *entry = ciborium::value::Value::serialized(&wapm)?;
496        };
497
498        Ok(Package {
499            base_dir: BaseDir::Path(Path::new("/").to_path_buf()),
500            manifest,
501            atoms,
502            strictness,
503            volumes,
504        })
505    }
506
507    fn load(
508        wasmer_toml: WasmerManifest,
509        base_dir: impl Into<BaseDir>,
510        strictness: Strictness,
511        walker_factory: WalkBuilderFactory,
512    ) -> Result<Self, WasmerPackageError> {
513        let base_dir = base_dir.into();
514
515        if strictness.is_strict() {
516            wasmer_toml.validate()?;
517        }
518
519        let (mut manifest, atoms) =
520            wasmer_manifest_to_webc(&wasmer_toml, base_dir.path(), strictness)?;
521
522        // remove name, description, and version before creating the webc file
523        if let Some(entry) = manifest.package.get_mut(Wapm::KEY) {
524            let mut wapm: Wapm = entry.deserialized()?;
525
526            wapm.name.take();
527            wapm.version.take();
528            wapm.description.take();
529
530            *entry = ciborium::value::Value::serialized(&wapm)?;
531        };
532
533        // Create volumes
534        let base_dir_path = base_dir.path().to_path_buf();
535        // Create metadata volume
536        let metadata_volume = FsVolume::new_metadata(&wasmer_toml, base_dir_path.clone())?;
537        // Create assets volume
538        let mut volumes: BTreeMap<String, Arc<dyn WasmerPackageVolume + Send + Sync + 'static>> = {
539            let old = FsVolume::new_assets(&wasmer_toml, &base_dir_path, walker_factory)?;
540            let mut new = BTreeMap::new();
541
542            for (k, v) in old.into_iter() {
543                new.insert(k, Arc::new(v) as _);
544            }
545
546            new
547        };
548        volumes.insert(
549            metadata_volume.name().to_string(),
550            Arc::new(metadata_volume),
551        );
552
553        Ok(Package {
554            base_dir,
555            manifest,
556            atoms,
557            strictness,
558            volumes,
559        })
560    }
561
562    /// Returns the Sha256 has of the webc represented by this Package
563    pub fn webc_hash(&self) -> Option<[u8; 32]> {
564        None
565    }
566
567    /// Get the WEBC manifest.
568    pub fn manifest(&self) -> &WebcManifest {
569        &self.manifest
570    }
571
572    /// Get all atoms in this package.
573    pub fn atoms(&self) -> &BTreeMap<String, OwnedBuffer> {
574        &self.atoms
575    }
576
577    /// Returns all volumes in this package
578    pub fn volumes(
579        &self,
580    ) -> impl Iterator<Item = &Arc<dyn WasmerPackageVolume + Sync + Send + 'static>> {
581        self.volumes.values()
582    }
583
584    /// Serialize the package to a `*.webc` v2 file, ignoring errors due to
585    /// missing files.
586    pub fn serialize(&self) -> Result<Bytes, Error> {
587        let mut w = Writer::new(ChecksumAlgorithm::Sha256)
588            .write_manifest(self.manifest())?
589            .write_atoms(self.atom_entries()?)?;
590
591        for (name, volume) in &self.volumes {
592            w.write_volume(name.as_str(), volume.as_directory_tree(self.strictness)?)?;
593        }
594
595        let serialized = w.finish(webc::v3::SignatureAlgorithm::None)?;
596
597        Ok(serialized)
598    }
599
600    fn atom_entries(&self) -> Result<BTreeMap<PathSegment, FileEntry<'_>>, Error> {
601        self.atoms()
602            .iter()
603            .map(|(key, value)| {
604                let filename = PathSegment::parse(key)
605                    .with_context(|| format!("\"{key}\" isn't a valid atom name"))?;
606                // FIXME: maybe?
607                Ok((filename, FileEntry::borrowed(value, Timestamps::default())))
608            })
609            .collect()
610    }
611
612    pub(crate) fn get_volume(
613        &self,
614        name: &str,
615    ) -> Option<Arc<dyn WasmerPackageVolume + Sync + Send + 'static>> {
616        self.volumes.get(name).cloned()
617    }
618
619    pub(crate) fn volume_names(&self) -> Vec<Cow<'_, str>> {
620        self.volumes
621            .keys()
622            .map(|name| Cow::Borrowed(name.as_str()))
623            .collect()
624    }
625}
626
627impl AbstractWebc for Package {
628    fn version(&self) -> Version {
629        Version::V3
630    }
631
632    fn manifest(&self) -> &WebcManifest {
633        self.manifest()
634    }
635
636    fn atom_names(&self) -> Vec<Cow<'_, str>> {
637        self.atoms()
638            .keys()
639            .map(|s| Cow::Borrowed(s.as_str()))
640            .collect()
641    }
642
643    fn get_atom(&self, name: &str) -> Option<OwnedBuffer> {
644        self.atoms().get(name).cloned()
645    }
646
647    fn get_webc_hash(&self) -> Option<[u8; 32]> {
648        self.webc_hash()
649    }
650
651    fn get_atoms_hash(&self) -> Option<[u8; 32]> {
652        None
653    }
654
655    fn volume_names(&self) -> Vec<Cow<'_, str>> {
656        self.volume_names()
657    }
658
659    fn get_volume(&self, name: &str) -> Option<Volume> {
660        self.get_volume(name).map(|v| {
661            let a: Arc<dyn AbstractVolume + Send + Sync + 'static> = v.as_volume();
662
663            Volume::from(a)
664        })
665    }
666}
667
668impl From<Package> for Container {
669    fn from(value: Package) -> Self {
670        Container::new(value)
671    }
672}
673
674const IS_WASI: bool = cfg!(all(target_family = "wasm", target_os = "wasi"));
675
676/// A polyfill for [`TempDir::new()`] that will work when compiling to
677/// WASI-based targets.
678///
679/// This works around [`std::env::temp_dir()`][tempdir] panicking
680/// unconditionally on WASI.
681///
682/// [tempdir]: https://github.com/wasix-org/rust/blob/ef19cdcdff77047f1e5ea4d09b4869d6fa456cc7/library/std/src/sys/wasi/os.rs#L228-L230
683fn tempdir() -> Result<TempDir, std::io::Error> {
684    if !IS_WASI {
685        // The happy path.
686        return TempDir::new();
687    }
688
689    // Note: When compiling to wasm32-wasip1, we can't use TempDir::new()
690    // because std::env::temp_dir() will unconditionally panic.
691    let temp_dir: PathBuf = std::env::var("TMPDIR")
692        .unwrap_or_else(|_| "/tmp".to_string())
693        .into();
694
695    if temp_dir.exists() {
696        TempDir::new_in(temp_dir)
697    } else {
698        // The temporary directory doesn't exist. A naive create_dir_all()
699        // doesn't work when running with "wasmer run" because the root
700        // directory is immutable, so let's try to use the current exe's
701        // directory as our tempdir.
702        // See also: https://github.com/wasmerio/wasmer/blob/482b78890b789f6867a91be9f306385e6255b260/lib/wasix/src/syscalls/wasi/path_create_directory.rs#L30-L32
703        if let Ok(current_exe) = std::env::current_exe() {
704            if let Some(parent) = current_exe.parent() {
705                if let Ok(temp) = TempDir::new_in(parent) {
706                    return Ok(temp);
707                }
708            }
709        }
710
711        // Oh well, this will probably fail, but at least we tried.
712        std::fs::create_dir_all(&temp_dir)?;
713        TempDir::new_in(temp_dir)
714    }
715}
716
717/// A polyfill for [`Archive::unpack()`] that is WASI-compatible.
718///
719/// This works around `canonicalize()` being [unsupported][github] on
720/// `wasm32-wasip1`.
721///
722/// [github]: https://github.com/rust-lang/rust/blob/5b1dc9de77106cb08ce9a1a8deaa14f52751d7e4/library/std/src/sys/wasi/fs.rs#L654-L658
723fn unpack_archive(
724    mut archive: Archive<impl std::io::Read>,
725    dest: &Path,
726) -> Result<(), std::io::Error> {
727    cfg_if::cfg_if! {
728        if #[cfg(all(target_family = "wasm", target_os = "wasi"))]
729        {
730            // A naive version of unpack() that should be good enough for WASI
731            // https://github.com/alexcrichton/tar-rs/blob/c77f47cb1b4b47fc4404a170d9d91cb42cc762ea/src/archive.rs#L216-L247
732            for entry in archive.entries()? {
733                let mut entry = entry?;
734                let item_path = entry.path()?;
735                let full_path = resolve_archive_path(dest, &item_path);
736
737                match entry.header().entry_type() {
738                    tar::EntryType::Directory => {
739                        std::fs::create_dir_all(&full_path)?;
740                    }
741                    tar::EntryType::Regular => {
742                        if let Some(parent) = full_path.parent() {
743                            std::fs::create_dir_all(parent)?;
744                        }
745                        let mut f = File::create(&full_path)?;
746                        std::io::copy(&mut entry, &mut f)?;
747
748                        let mtime = entry.header().mtime().unwrap_or_default();
749                        if let Err(e) = set_timestamp(full_path.as_path(), mtime) {
750                            println!("WARN: {e:?}");
751                        }
752                    }
753                    _ => {}
754                }
755            }
756            Ok(())
757
758        } else {
759            archive.unpack(dest)
760        }
761    }
762}
763
764#[cfg(all(target_family = "wasm", target_os = "wasi"))]
765fn set_timestamp(path: &Path, timestamp: u64) -> Result<(), anyhow::Error> {
766    let fd = unsafe {
767        libc::open(
768            path.as_os_str().as_encoded_bytes().as_ptr() as _,
769            libc::O_RDONLY,
770        )
771    };
772
773    if fd < 0 {
774        anyhow::bail!(format!("failed to open: {}", path.display()));
775    }
776
777    let timespec = [
778        // accessed
779        libc::timespec {
780            tv_sec: unsafe { libc::time(std::ptr::null_mut()) }, // now
781            tv_nsec: 0,
782        },
783        // modified
784        libc::timespec {
785            tv_sec: timestamp as i64,
786            tv_nsec: 0,
787        },
788    ];
789
790    let res = unsafe { libc::futimens(fd, timespec.as_ptr() as _) };
791
792    if res < 0 {
793        anyhow::bail!("failed to set timestamp for: {}", path.display());
794    }
795
796    Ok(())
797}
798
799#[cfg(all(target_family = "wasm", target_os = "wasi"))]
800fn resolve_archive_path(base_dir: &Path, path: &Path) -> PathBuf {
801    let mut buffer = base_dir.to_path_buf();
802
803    for component in path.components() {
804        match component {
805            std::path::Component::Prefix(_)
806            | std::path::Component::RootDir
807            | std::path::Component::CurDir => continue,
808            std::path::Component::ParentDir => {
809                buffer.pop();
810            }
811            std::path::Component::Normal(segment) => {
812                buffer.push(segment);
813            }
814        }
815    }
816
817    buffer
818}
819
820fn read_manifest(base_dir: &Path) -> Result<(PathBuf, WasmerManifest), WasmerPackageError> {
821    for path in ["wasmer.toml", "wapm.toml"] {
822        let path = base_dir.join(path);
823
824        match std::fs::read_to_string(&path) {
825            Ok(s) => {
826                let toml_file = toml::from_str(&s).map_err({
827                    let path = path.clone();
828                    |error| WasmerPackageError::TomlDeserialize { path, error }
829                })?;
830
831                return Ok((path, toml_file));
832            }
833            Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
834            Err(error) => {
835                return Err(WasmerPackageError::FileRead { path, error });
836            }
837        }
838    }
839
840    Err(WasmerPackageError::MissingManifest)
841}
842
843#[derive(Debug)]
844enum BaseDir {
845    /// An existing directory.
846    Path(PathBuf),
847    /// A temporary directory that will be deleted on drop.
848    Temp(TempDir),
849}
850
851impl BaseDir {
852    fn path(&self) -> &Path {
853        match self {
854            BaseDir::Path(p) => p.as_path(),
855            BaseDir::Temp(t) => t.path(),
856        }
857    }
858}
859
860impl From<TempDir> for BaseDir {
861    fn from(v: TempDir) -> Self {
862        Self::Temp(v)
863    }
864}
865
866impl From<PathBuf> for BaseDir {
867    fn from(v: PathBuf) -> Self {
868        Self::Path(v)
869    }
870}
871
872#[cfg(test)]
873mod tests {
874    use std::{
875        collections::BTreeMap,
876        fs::File,
877        path::{Path, PathBuf},
878        str::FromStr,
879        time::SystemTime,
880    };
881
882    use flate2::{Compression, write::GzEncoder};
883    use sha2::Digest;
884    use shared_buffer::OwnedBuffer;
885    use tempfile::TempDir;
886    use webc::{
887        PathSegment, PathSegments,
888        metadata::{
889            Binding, BindingsExtended, WaiBindings, WitBindings,
890            annotations::{FileSystemMapping, VolumeSpecificPath},
891        },
892    };
893
894    use crate::{package::*, utils::from_bytes};
895
896    #[test]
897    fn nonexistent_files() {
898        let temp = TempDir::new().unwrap();
899
900        assert!(Package::from_manifest(temp.path().join("nonexistent.toml")).is_err());
901        assert!(Package::from_tarball_file(temp.path().join("nonexistent.tar.gz")).is_err());
902    }
903
904    #[test]
905    fn load_a_tarball() {
906        let coreutils = Path::new(env!("CARGO_MANIFEST_DIR"))
907            .join("..")
908            .join("..")
909            .join("tests")
910            .join("old-tar-gz")
911            .join("coreutils-1.0.11.tar.gz");
912        assert!(coreutils.exists());
913
914        let package = Package::from_tarball_file(coreutils).unwrap();
915
916        let wapm = package.manifest().wapm().unwrap().unwrap();
917        assert!(wapm.name.is_none());
918        assert!(wapm.version.is_none());
919        assert!(wapm.description.is_none());
920    }
921
922    #[test]
923    fn tarball_with_no_manifest() {
924        let temp = TempDir::new().unwrap();
925        let empty_tarball = temp.path().join("empty.tar.gz");
926        let mut f = File::create(&empty_tarball).unwrap();
927        tar::Builder::new(GzEncoder::new(&mut f, Compression::fast()))
928            .finish()
929            .unwrap();
930
931        assert!(Package::from_tarball_file(&empty_tarball).is_err());
932    }
933
934    #[test]
935    fn empty_package_on_disk() {
936        let temp = TempDir::new().unwrap();
937        let manifest = temp.path().join("wasmer.toml");
938        std::fs::write(
939            &manifest,
940            r#"
941                [package]
942                name = "some/package"
943                version = "0.0.0"
944                description = "A dummy package"
945            "#,
946        )
947        .unwrap();
948
949        let package = Package::from_manifest(&manifest).unwrap();
950
951        let wapm = package.manifest().wapm().unwrap().unwrap();
952        assert!(wapm.name.is_none());
953        assert!(wapm.version.is_none());
954        assert!(wapm.description.is_none());
955    }
956
957    #[test]
958    fn load_old_cowsay() {
959        let tarball = Path::new(env!("CARGO_MANIFEST_DIR"))
960            .join("..")
961            .join("..")
962            .join("tests")
963            .join("old-tar-gz")
964            .join("cowsay-0.3.0.tar.gz");
965
966        let pkg = Package::from_tarball_file(tarball).unwrap();
967
968        insta::assert_yaml_snapshot!(pkg.manifest());
969        assert_eq!(
970            pkg.manifest.commands.keys().collect::<Vec<_>>(),
971            ["cowsay", "cowthink"],
972        );
973    }
974
975    #[test]
976    fn serialize_package_with_non_existent_fs() {
977        let temp = TempDir::new().unwrap();
978        let wasmer_toml = r#"
979                [package]
980                name = "some/package"
981                version = "0.0.0"
982                description = "Test package"
983
984                [fs]
985                "/first" = "./first"
986            "#;
987        let manifest = temp.path().join("wasmer.toml");
988
989        std::fs::write(&manifest, wasmer_toml).unwrap();
990
991        let error = Package::from_manifest(manifest).unwrap_err();
992
993        match error {
994            WasmerPackageError::PathNotExists { path } => {
995                assert_eq!(path, PathBuf::from_str("./first").unwrap());
996            }
997            e => panic!("unexpected error: {e:?}"),
998        }
999    }
1000
1001    #[test]
1002    fn serialize_package_with_bundled_directories() {
1003        let temp = TempDir::new().unwrap();
1004        let wasmer_toml = r#"
1005                [package]
1006                name = "some/package"
1007                version = "0.0.0"
1008                description = "Test package"
1009
1010                [fs]
1011                "/first" = "first"
1012                second = "nested/dir"
1013                "second/child" = "third"
1014                empty = "empty"
1015            "#;
1016        let manifest = temp.path().join("wasmer.toml");
1017        std::fs::write(&manifest, wasmer_toml).unwrap();
1018        // Now we want to set up the following filesystem tree:
1019        //
1020        // - first/ ("/first")
1021        //   - file.txt
1022        // - nested/
1023        //   - dir/ ("second")
1024        //     - .wasmerignore
1025        //     - .hidden (should be ignored)
1026        //     - ignore_me (should be ignored)
1027        //     - README.md
1028        //     - another-dir/
1029        //       - empty.txt
1030        // - third/ ("second/child")
1031        //   - file.txt
1032        // - empty/ ("empty")
1033        //
1034        // The "/first" entry
1035        let first = temp.path().join("first");
1036        std::fs::create_dir_all(&first).unwrap();
1037        std::fs::write(first.join("file.txt"), "File").unwrap();
1038        // The "second" entry
1039        let second = temp.path().join("nested").join("dir");
1040        std::fs::create_dir_all(&second).unwrap();
1041        std::fs::write(second.join(".wasmerignore"), "ignore_me").unwrap();
1042        std::fs::write(second.join(".hidden"), "something something").unwrap();
1043        std::fs::write(second.join("ignore_me"), "something something").unwrap();
1044        std::fs::write(second.join("README.md"), "please").unwrap();
1045        let another_dir = temp.path().join("nested").join("dir").join("another-dir");
1046        std::fs::create_dir_all(&another_dir).unwrap();
1047        std::fs::write(another_dir.join("empty.txt"), "").unwrap();
1048        // The "second/child" entry
1049        let third = temp.path().join("third");
1050        std::fs::create_dir_all(&third).unwrap();
1051        std::fs::write(third.join("file.txt"), "Hello, World!").unwrap();
1052        // The "empty" entry
1053        let empty_dir = temp.path().join("empty");
1054        std::fs::create_dir_all(empty_dir).unwrap();
1055
1056        let package = Package::from_manifest(manifest).unwrap();
1057
1058        let webc = package.serialize().unwrap();
1059        let webc = from_bytes(webc).unwrap();
1060        let manifest = webc.manifest();
1061        let wapm_metadata = manifest.wapm().unwrap().unwrap();
1062        assert!(wapm_metadata.name.is_none());
1063        assert!(wapm_metadata.version.is_none());
1064        assert!(wapm_metadata.description.is_none());
1065        let fs_table = manifest.filesystem().unwrap().unwrap();
1066        assert_eq!(
1067            fs_table,
1068            [
1069                FileSystemMapping {
1070                    from: None,
1071                    volume_name: "/first".to_string(),
1072                    host_path: None,
1073                    mount_path: "/first".to_string(),
1074                },
1075                FileSystemMapping {
1076                    from: None,
1077                    volume_name: "/nested/dir".to_string(),
1078                    host_path: None,
1079                    mount_path: "/second".to_string(),
1080                },
1081                FileSystemMapping {
1082                    from: None,
1083                    volume_name: "/third".to_string(),
1084                    host_path: None,
1085                    mount_path: "/second/child".to_string(),
1086                },
1087                FileSystemMapping {
1088                    from: None,
1089                    volume_name: "/empty".to_string(),
1090                    host_path: None,
1091                    mount_path: "/empty".to_string(),
1092                },
1093            ]
1094        );
1095
1096        let first_file_hash: [u8; 32] = sha2::Sha256::digest(b"File").into();
1097        let readme_hash: [u8; 32] = sha2::Sha256::digest(b"please").into();
1098        let empty_hash: [u8; 32] = sha2::Sha256::digest(b"").into();
1099        let third_file_hash: [u8; 32] = sha2::Sha256::digest(b"Hello, World!").into();
1100
1101        let first_volume = webc.get_volume("/first").unwrap();
1102        assert_eq!(
1103            first_volume.read_file("/file.txt").unwrap(),
1104            (b"File".as_slice().into(), Some(first_file_hash)),
1105        );
1106
1107        let nested_dir_volume = webc.get_volume("/nested/dir").unwrap();
1108        assert_eq!(
1109            nested_dir_volume.read_file("README.md").unwrap(),
1110            (b"please".as_slice().into(), Some(readme_hash)),
1111        );
1112        assert!(nested_dir_volume.read_file(".wasmerignore").is_some());
1113        assert!(nested_dir_volume.read_file(".hidden").is_some());
1114        assert!(nested_dir_volume.read_file("ignore_me").is_none());
1115        assert_eq!(
1116            nested_dir_volume
1117                .read_file("/another-dir/empty.txt")
1118                .unwrap(),
1119            (b"".as_slice().into(), Some(empty_hash))
1120        );
1121
1122        let third_volume = webc.get_volume("/third").unwrap();
1123        assert_eq!(
1124            third_volume.read_file("/file.txt").unwrap(),
1125            (b"Hello, World!".as_slice().into(), Some(third_file_hash))
1126        );
1127
1128        let empty_volume = webc.get_volume("/empty").unwrap();
1129        assert_eq!(
1130            empty_volume.read_dir("/").unwrap().len(),
1131            0,
1132            "Directories should be included, even if empty"
1133        );
1134    }
1135
1136    #[test]
1137    fn serialize_package_with_metadata_files() {
1138        let temp = TempDir::new().unwrap();
1139        let wasmer_toml = r#"
1140                [package]
1141                name = "some/package"
1142                version = "0.0.0"
1143                description = "Test package"
1144                readme = "README.md"
1145                license-file = "LICENSE"
1146            "#;
1147        let manifest = temp.path().join("wasmer.toml");
1148        std::fs::write(&manifest, wasmer_toml).unwrap();
1149        std::fs::write(temp.path().join("README.md"), "readme").unwrap();
1150        std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
1151
1152        let serialized = Package::from_manifest(manifest)
1153            .unwrap()
1154            .serialize()
1155            .unwrap();
1156
1157        let webc = from_bytes(serialized).unwrap();
1158        let metadata_volume = webc.get_volume("metadata").unwrap();
1159
1160        let readme_hash: [u8; 32] = sha2::Sha256::digest(b"readme").into();
1161        let license_hash: [u8; 32] = sha2::Sha256::digest(b"license").into();
1162
1163        assert_eq!(
1164            metadata_volume.read_file("/README.md").unwrap(),
1165            (b"readme".as_slice().into(), Some(readme_hash))
1166        );
1167        assert_eq!(
1168            metadata_volume.read_file("/LICENSE").unwrap(),
1169            (b"license".as_slice().into(), Some(license_hash))
1170        );
1171    }
1172
1173    #[test]
1174    fn load_package_with_wit_bindings() {
1175        let temp = TempDir::new().unwrap();
1176        let wasmer_toml = r#"
1177            [package]
1178            name = "some/package"
1179            version = "0.0.0"
1180            description = ""
1181
1182            [[module]]
1183            name = "my-lib"
1184            source = "./my-lib.wasm"
1185            abi = "none"
1186            bindings = { wit-bindgen = "0.1.0", wit-exports = "./file.wit" }
1187        "#;
1188        std::fs::write(temp.path().join("wasmer.toml"), wasmer_toml).unwrap();
1189        std::fs::write(temp.path().join("file.wit"), "file").unwrap();
1190        std::fs::write(temp.path().join("my-lib.wasm"), b"\0asm...").unwrap();
1191
1192        let package = Package::from_manifest(temp.path().join("wasmer.toml"))
1193            .unwrap()
1194            .serialize()
1195            .unwrap();
1196        let webc = from_bytes(package).unwrap();
1197
1198        assert_eq!(
1199            webc.manifest().bindings,
1200            vec![Binding {
1201                name: "library-bindings".to_string(),
1202                kind: "wit@0.1.0".to_string(),
1203                annotations: ciborium::value::Value::serialized(&BindingsExtended::Wit(
1204                    WitBindings {
1205                        exports: "metadata://file.wit".to_string(),
1206                        module: "my-lib".to_string(),
1207                    }
1208                ))
1209                .unwrap(),
1210            }]
1211        );
1212        let metadata_volume = webc.get_volume("metadata").unwrap();
1213        let file_hash: [u8; 32] = sha2::Sha256::digest(b"file").into();
1214        assert_eq!(
1215            metadata_volume.read_file("/file.wit").unwrap(),
1216            (b"file".as_slice().into(), Some(file_hash))
1217        );
1218        insta::with_settings! {
1219            { description => wasmer_toml },
1220            { insta::assert_yaml_snapshot!(webc.manifest()); }
1221        }
1222    }
1223
1224    #[test]
1225    fn load_package_with_wai_bindings() {
1226        let temp = TempDir::new().unwrap();
1227        let wasmer_toml = r#"
1228            [package]
1229            name = "some/package"
1230            version = "0.0.0"
1231            description = ""
1232
1233            [[module]]
1234            name = "my-lib"
1235            source = "./my-lib.wasm"
1236            abi = "none"
1237            bindings = { wai-version = "0.2.0", exports = "./file.wai", imports = ["a.wai", "b.wai"] }
1238        "#;
1239        std::fs::write(temp.path().join("wasmer.toml"), wasmer_toml).unwrap();
1240        std::fs::write(temp.path().join("file.wai"), "file").unwrap();
1241        std::fs::write(temp.path().join("a.wai"), "a").unwrap();
1242        std::fs::write(temp.path().join("b.wai"), "b").unwrap();
1243        std::fs::write(temp.path().join("my-lib.wasm"), b"\0asm...").unwrap();
1244
1245        let package = Package::from_manifest(temp.path().join("wasmer.toml"))
1246            .unwrap()
1247            .serialize()
1248            .unwrap();
1249        let webc = from_bytes(package).unwrap();
1250
1251        assert_eq!(
1252            webc.manifest().bindings,
1253            vec![Binding {
1254                name: "library-bindings".to_string(),
1255                kind: "wai@0.2.0".to_string(),
1256                annotations: ciborium::value::Value::serialized(&BindingsExtended::Wai(
1257                    WaiBindings {
1258                        exports: Some("metadata://file.wai".to_string()),
1259                        module: "my-lib".to_string(),
1260                        imports: vec![
1261                            "metadata://a.wai".to_string(),
1262                            "metadata://b.wai".to_string(),
1263                        ]
1264                    }
1265                ))
1266                .unwrap(),
1267            }]
1268        );
1269        let metadata_volume = webc.get_volume("metadata").unwrap();
1270
1271        let file_hash: [u8; 32] = sha2::Sha256::digest(b"file").into();
1272        let a_hash: [u8; 32] = sha2::Sha256::digest(b"a").into();
1273        let b_hash: [u8; 32] = sha2::Sha256::digest(b"b").into();
1274
1275        assert_eq!(
1276            metadata_volume.read_file("/file.wai").unwrap(),
1277            (b"file".as_slice().into(), Some(file_hash))
1278        );
1279        assert_eq!(
1280            metadata_volume.read_file("/a.wai").unwrap(),
1281            (b"a".as_slice().into(), Some(a_hash))
1282        );
1283        assert_eq!(
1284            metadata_volume.read_file("/b.wai").unwrap(),
1285            (b"b".as_slice().into(), Some(b_hash))
1286        );
1287        insta::with_settings! {
1288            { description => wasmer_toml },
1289            { insta::assert_yaml_snapshot!(webc.manifest()); }
1290        }
1291    }
1292
1293    /// See <https://github.com/wasmerio/pirita/issues/105> for more.
1294    #[test]
1295    fn absolute_paths_in_wasmer_toml_issue_105() {
1296        let temp = TempDir::new().unwrap();
1297        let base_dir = temp.path().canonicalize().unwrap();
1298        let sep = std::path::MAIN_SEPARATOR;
1299        let wasmer_toml = format!(
1300            r#"
1301                [package]
1302                name = 'some/package'
1303                version = '0.0.0'
1304                description = 'Test package'
1305                readme = '{BASE_DIR}{sep}README.md'
1306                license-file = '{BASE_DIR}{sep}LICENSE'
1307
1308                [[module]]
1309                name = 'first'
1310                source = '{BASE_DIR}{sep}target{sep}debug{sep}package.wasm'
1311                bindings = {{ wai-version = '0.2.0', exports = '{BASE_DIR}{sep}bindings{sep}file.wai', imports = ['{BASE_DIR}{sep}bindings{sep}a.wai'] }}
1312            "#,
1313            BASE_DIR = base_dir.display(),
1314        );
1315        let manifest = temp.path().join("wasmer.toml");
1316        std::fs::write(&manifest, &wasmer_toml).unwrap();
1317        std::fs::write(temp.path().join("README.md"), "readme").unwrap();
1318        std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
1319        let bindings = temp.path().join("bindings");
1320        std::fs::create_dir_all(&bindings).unwrap();
1321        std::fs::write(bindings.join("file.wai"), "file.wai").unwrap();
1322        std::fs::write(bindings.join("a.wai"), "a.wai").unwrap();
1323        let target = temp.path().join("target").join("debug");
1324        std::fs::create_dir_all(&target).unwrap();
1325        std::fs::write(target.join("package.wasm"), b"\0asm...").unwrap();
1326
1327        let serialized = Package::from_manifest(manifest)
1328            .unwrap()
1329            .serialize()
1330            .unwrap();
1331
1332        let webc = from_bytes(serialized).unwrap();
1333        let manifest = webc.manifest();
1334        let wapm = manifest.wapm().unwrap().unwrap();
1335
1336        // we should be able to look up the files using the manifest
1337        let lookup = |item: VolumeSpecificPath| {
1338            let volume = webc.get_volume(&item.volume).unwrap();
1339            let (contents, _) = volume.read_file(&item.path).unwrap();
1340            String::from_utf8(contents.into()).unwrap()
1341        };
1342        assert_eq!(lookup(wapm.license_file.unwrap()), "license");
1343        assert_eq!(lookup(wapm.readme.unwrap()), "readme");
1344
1345        // The paths for bindings are stored slightly differently, but it's the
1346        // same general idea
1347        let lookup = |item: &str| {
1348            let (volume, path) = item.split_once(":/").unwrap();
1349            let volume = webc.get_volume(volume).unwrap();
1350            let (content, _) = volume.read_file(path).unwrap();
1351            String::from_utf8(content.into()).unwrap()
1352        };
1353        let bindings = manifest.bindings[0].get_wai_bindings().unwrap();
1354        assert_eq!(lookup(&bindings.imports[0]), "a.wai");
1355        assert_eq!(lookup(bindings.exports.unwrap().as_str()), "file.wai");
1356
1357        // Snapshot tests for good measure
1358        let mut settings = insta::Settings::clone_current();
1359        let base_dir = base_dir.display().to_string();
1360        settings.set_description(wasmer_toml.replace(&base_dir, "[BASE_DIR]"));
1361        let filter = regex::escape(&base_dir);
1362        settings.add_filter(&filter, "[BASE_DIR]");
1363        settings.bind(|| {
1364            insta::assert_yaml_snapshot!(webc.manifest());
1365        });
1366    }
1367
1368    #[test]
1369    fn serializing_will_skip_missing_metadata_by_default() {
1370        let temp = TempDir::new().unwrap();
1371        let wasmer_toml = r#"
1372                [package]
1373                name = 'some/package'
1374                version = '0.0.0'
1375                description = 'Test package'
1376                readme = '/this/does/not/exist/README.md'
1377                license-file = 'LICENSE.wtf'
1378            "#;
1379        let manifest = temp.path().join("wasmer.toml");
1380        std::fs::write(&manifest, wasmer_toml).unwrap();
1381        let pkg = Package::from_manifest(manifest).unwrap();
1382
1383        let serialized = pkg.serialize().unwrap();
1384
1385        let webc = from_bytes(serialized).unwrap();
1386        let manifest = webc.manifest();
1387        let wapm = manifest.wapm().unwrap().unwrap();
1388        // We re-wrote the WAPM annotations to just not include the license file
1389        assert!(wapm.license_file.is_none());
1390        assert!(wapm.readme.is_none());
1391
1392        // Note: serializing in strict mode should still fail
1393        let pkg = Package {
1394            strictness: Strictness::Strict,
1395            ..pkg
1396        };
1397        assert!(pkg.serialize().is_err());
1398    }
1399
1400    #[test]
1401    fn serialize_package_without_local_base_fs_paths() {
1402        let temp = TempDir::new().unwrap();
1403        let wasmer_toml = r#"
1404                [package]
1405                name = "some/package"
1406                version = "0.0.0"
1407                description = "Test package"
1408                readme = 'README.md'
1409                license-file = 'LICENSE'
1410
1411                [fs]
1412                "/path_in_wasix" = "local-dir/dir1"
1413            "#;
1414        let manifest = temp.path().join("wasmer.toml");
1415        std::fs::write(&manifest, wasmer_toml).unwrap();
1416
1417        std::fs::write(temp.path().join("README.md"), "readme").unwrap();
1418        std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
1419
1420        // Now we want to set up the following filesystem tree:
1421        //
1422        // - local-dir/
1423        //   - dir1/
1424        //     - a
1425        //     - b
1426        let dir1 = temp.path().join("local-dir").join("dir1");
1427        std::fs::create_dir_all(&dir1).unwrap();
1428
1429        let a = dir1.join("a");
1430        let b = dir1.join("b");
1431
1432        std::fs::write(a, "a").unwrap();
1433        std::fs::write(b, "b").unwrap();
1434
1435        let package = Package::from_manifest(manifest).unwrap();
1436
1437        let webc = package.serialize().unwrap();
1438        let webc = from_bytes(webc).unwrap();
1439        let manifest = webc.manifest();
1440        let wapm_metadata = manifest.wapm().unwrap().unwrap();
1441
1442        assert!(wapm_metadata.name.is_none());
1443        assert!(wapm_metadata.version.is_none());
1444        assert!(wapm_metadata.description.is_none());
1445
1446        let fs_table = manifest.filesystem().unwrap().unwrap();
1447        assert_eq!(
1448            fs_table,
1449            [FileSystemMapping {
1450                from: None,
1451                volume_name: "/local-dir/dir1".to_string(),
1452                host_path: None,
1453                mount_path: "/path_in_wasix".to_string(),
1454            },]
1455        );
1456
1457        let readme_hash: [u8; 32] = sha2::Sha256::digest(b"readme").into();
1458        let license_hash: [u8; 32] = sha2::Sha256::digest(b"license").into();
1459
1460        let a_hash: [u8; 32] = sha2::Sha256::digest(b"a").into();
1461        let b_hash: [u8; 32] = sha2::Sha256::digest(b"b").into();
1462
1463        let dir1_volume = webc.get_volume("/local-dir/dir1").unwrap();
1464        let meta_volume = webc.get_volume("metadata").unwrap();
1465
1466        assert_eq!(
1467            meta_volume.read_file("LICENSE").unwrap(),
1468            (b"license".as_slice().into(), Some(license_hash)),
1469        );
1470        assert_eq!(
1471            meta_volume.read_file("README.md").unwrap(),
1472            (b"readme".as_slice().into(), Some(readme_hash)),
1473        );
1474        assert_eq!(
1475            dir1_volume.read_file("a").unwrap(),
1476            (b"a".as_slice().into(), Some(a_hash))
1477        );
1478        assert_eq!(
1479            dir1_volume.read_file("b").unwrap(),
1480            (b"b".as_slice().into(), Some(b_hash))
1481        );
1482    }
1483
1484    #[test]
1485    fn serialize_package_with_nested_fs_entries_without_local_base_fs_paths() {
1486        let temp = TempDir::new().unwrap();
1487        let wasmer_toml = r#"
1488                [package]
1489                name = "some/package"
1490                version = "0.0.0"
1491                description = "Test package"
1492                readme = 'README.md'
1493                license-file = 'LICENSE'
1494
1495                [fs]
1496                "/path_in_wasix" = "local-dir/dir1"
1497            "#;
1498        let manifest = temp.path().join("wasmer.toml");
1499        std::fs::write(&manifest, wasmer_toml).unwrap();
1500
1501        std::fs::write(temp.path().join("README.md"), "readme").unwrap();
1502        std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
1503
1504        // Now we want to set up the following filesystem tree:
1505        //
1506        // - local-dir/
1507        //   - dir1/
1508        //     - dir2/
1509        //       - a
1510        //     - b
1511        let local_dir = temp.path().join("local-dir");
1512        std::fs::create_dir_all(&local_dir).unwrap();
1513
1514        let dir1 = local_dir.join("dir1");
1515        std::fs::create_dir_all(&dir1).unwrap();
1516
1517        let dir2 = dir1.join("dir2");
1518        std::fs::create_dir_all(&dir2).unwrap();
1519
1520        let a = dir2.join("a");
1521        let b = dir1.join("b");
1522
1523        std::fs::write(a, "a").unwrap();
1524        std::fs::write(b, "b").unwrap();
1525
1526        let package = Package::from_manifest(manifest).unwrap();
1527
1528        let webc = package.serialize().unwrap();
1529        let webc = from_bytes(webc).unwrap();
1530        let manifest = webc.manifest();
1531        let wapm_metadata = manifest.wapm().unwrap().unwrap();
1532
1533        assert!(wapm_metadata.name.is_none());
1534        assert!(wapm_metadata.version.is_none());
1535        assert!(wapm_metadata.description.is_none());
1536
1537        let fs_table = manifest.filesystem().unwrap().unwrap();
1538        assert_eq!(
1539            fs_table,
1540            [FileSystemMapping {
1541                from: None,
1542                volume_name: "/local-dir/dir1".to_string(),
1543                host_path: None,
1544                mount_path: "/path_in_wasix".to_string(),
1545            },]
1546        );
1547
1548        let readme_hash: [u8; 32] = sha2::Sha256::digest(b"readme").into();
1549        let license_hash: [u8; 32] = sha2::Sha256::digest(b"license").into();
1550
1551        let a_hash: [u8; 32] = sha2::Sha256::digest(b"a").into();
1552        let dir2_hash: [u8; 32] = sha2::Sha256::digest(a_hash).into();
1553        let b_hash: [u8; 32] = sha2::Sha256::digest(b"b").into();
1554
1555        let dir1_volume = webc.get_volume("/local-dir/dir1").unwrap();
1556        let meta_volume = webc.get_volume("metadata").unwrap();
1557
1558        assert_eq!(
1559            meta_volume.read_file("LICENSE").unwrap(),
1560            (b"license".as_slice().into(), Some(license_hash)),
1561        );
1562        assert_eq!(
1563            meta_volume.read_file("README.md").unwrap(),
1564            (b"readme".as_slice().into(), Some(readme_hash)),
1565        );
1566        assert_eq!(
1567            dir1_volume
1568                .read_dir("/")
1569                .unwrap()
1570                .into_iter()
1571                .map(|(p, h, _)| (p, h))
1572                .collect::<Vec<_>>(),
1573            vec![
1574                (PathSegment::parse("b").unwrap(), Some(b_hash)),
1575                (PathSegment::parse("dir2").unwrap(), Some(dir2_hash))
1576            ]
1577        );
1578        assert_eq!(
1579            dir1_volume
1580                .read_dir("/dir2")
1581                .unwrap()
1582                .into_iter()
1583                .map(|(p, h, _)| (p, h))
1584                .collect::<Vec<_>>(),
1585            vec![(PathSegment::parse("a").unwrap(), Some(a_hash))]
1586        );
1587        assert_eq!(
1588            dir1_volume.read_file("/dir2/a").unwrap(),
1589            (b"a".as_slice().into(), Some(a_hash))
1590        );
1591        assert_eq!(
1592            dir1_volume.read_file("/b").unwrap(),
1593            (b"b".as_slice().into(), Some(b_hash))
1594        );
1595    }
1596
1597    #[test]
1598    fn serialize_package_mapped_to_same_dir_without_local_base_fs_paths() {
1599        let temp = TempDir::new().unwrap();
1600        let wasmer_toml = r#"
1601                [package]
1602                name = "some/package"
1603                version = "0.0.0"
1604                description = "Test package"
1605                readme = 'README.md'
1606                license-file = 'LICENSE'
1607
1608                [fs]
1609                "/dir1" = "local-dir1/dir"
1610                "/dir2" = "local-dir2/dir"
1611            "#;
1612        let manifest = temp.path().join("wasmer.toml");
1613        std::fs::write(&manifest, wasmer_toml).unwrap();
1614
1615        std::fs::write(temp.path().join("README.md"), "readme").unwrap();
1616        std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
1617
1618        // Now we want to set up the following filesystem tree:
1619        //
1620        // - local-dir1/
1621        //   - dir
1622        // - local-dir2/
1623        //   - dir
1624        let dir1 = temp.path().join("local-dir1").join("dir");
1625        std::fs::create_dir_all(&dir1).unwrap();
1626        let dir2 = temp.path().join("local-dir2").join("dir");
1627        std::fs::create_dir_all(&dir2).unwrap();
1628
1629        let package = Package::from_manifest(manifest).unwrap();
1630
1631        let webc = package.serialize().unwrap();
1632        let webc = from_bytes(webc).unwrap();
1633        let manifest = webc.manifest();
1634        let wapm_metadata = manifest.wapm().unwrap().unwrap();
1635
1636        assert!(wapm_metadata.name.is_none());
1637        assert!(wapm_metadata.version.is_none());
1638        assert!(wapm_metadata.description.is_none());
1639
1640        let fs_table = manifest.filesystem().unwrap().unwrap();
1641        assert_eq!(
1642            fs_table,
1643            [
1644                FileSystemMapping {
1645                    from: None,
1646                    volume_name: "/local-dir1/dir".to_string(),
1647                    host_path: None,
1648                    mount_path: "/dir1".to_string(),
1649                },
1650                FileSystemMapping {
1651                    from: None,
1652                    volume_name: "/local-dir2/dir".to_string(),
1653                    host_path: None,
1654                    mount_path: "/dir2".to_string(),
1655                },
1656            ]
1657        );
1658
1659        let readme_hash: [u8; 32] = sha2::Sha256::digest(b"readme").into();
1660        let license_hash: [u8; 32] = sha2::Sha256::digest(b"license").into();
1661
1662        let dir1_volume = webc.get_volume("/local-dir1/dir").unwrap();
1663        let dir2_volume = webc.get_volume("/local-dir2/dir").unwrap();
1664        let meta_volume = webc.get_volume("metadata").unwrap();
1665
1666        assert_eq!(
1667            meta_volume.read_file("LICENSE").unwrap(),
1668            (b"license".as_slice().into(), Some(license_hash)),
1669        );
1670        assert_eq!(
1671            meta_volume.read_file("README.md").unwrap(),
1672            (b"readme".as_slice().into(), Some(readme_hash)),
1673        );
1674        assert!(dir1_volume.read_dir("/").unwrap().is_empty());
1675        assert!(dir2_volume.read_dir("/").unwrap().is_empty());
1676    }
1677
1678    #[test]
1679    fn metadata_only_contains_relevant_files() {
1680        let temp = TempDir::new().unwrap();
1681        let wasmer_toml = r#"
1682            [package]
1683            name = "some/package"
1684            version = "0.0.0"
1685            description = ""
1686            license-file = "./path/to/LICENSE"
1687            readme = "README.md"
1688
1689            [[module]]
1690            name = "asdf"
1691            source = "asdf.wasm"
1692            abi = "none"
1693            bindings = { wai-version = "0.2.0", exports = "asdf.wai", imports = ["browser.wai"] }
1694        "#;
1695
1696        let manifest = temp.path().join("wasmer.toml");
1697        std::fs::write(&manifest, wasmer_toml).unwrap();
1698
1699        let license_dir = temp.path().join("path").join("to");
1700        std::fs::create_dir_all(&license_dir).unwrap();
1701        std::fs::write(license_dir.join("LICENSE"), "license").unwrap();
1702        std::fs::write(temp.path().join("README.md"), "readme").unwrap();
1703        std::fs::write(temp.path().join("asdf.wasm"), b"\0asm...").unwrap();
1704        std::fs::write(temp.path().join("asdf.wai"), "exports").unwrap();
1705        std::fs::write(temp.path().join("browser.wai"), "imports").unwrap();
1706        std::fs::write(temp.path().join("unwanted_file.txt"), "unwanted_file").unwrap();
1707
1708        let package = Package::from_manifest(manifest).unwrap();
1709
1710        let contents: Vec<_> = package
1711            .get_volume("metadata")
1712            .unwrap()
1713            .read_dir(&PathSegments::ROOT)
1714            .unwrap()
1715            .into_iter()
1716            .map(|(path, _, _)| path)
1717            .collect();
1718
1719        assert_eq!(
1720            contents,
1721            vec![
1722                PathSegment::parse("README.md").unwrap(),
1723                PathSegment::parse("asdf.wai").unwrap(),
1724                PathSegment::parse("browser.wai").unwrap(),
1725                PathSegment::parse("path").unwrap(),
1726            ]
1727        );
1728    }
1729
1730    #[test]
1731    fn create_from_in_memory() -> anyhow::Result<()> {
1732        let wasmer_toml = r#"
1733            [dependencies]
1734            "wasmer/python" = "3.12.9+build.9"
1735            
1736            
1737            [[command]]
1738            module = "wasmer/python:python"
1739            name = "hello"
1740            runner = "wasi"
1741            
1742            [command.annotations.wasi]
1743            main-args = [ "-c", "import os; print([f for f in os.walk('/public')]); " ]
1744            
1745            [fs]
1746            "/public" = "public" 
1747        "#;
1748
1749        let manifest = toml::from_str(wasmer_toml)?;
1750
1751        let file_modified = SystemTime::now();
1752        let file_data = String::from("Hello, world!").as_bytes().to_vec();
1753
1754        let file = MemoryFile {
1755            modified: file_modified,
1756            data: file_data,
1757        };
1758
1759        let mut nodes = BTreeMap::new();
1760        nodes.insert(String::from("hello.txt"), MemoryNode::File(file));
1761
1762        let dir_modified = SystemTime::now();
1763        let dir = MemoryDir {
1764            modified: dir_modified,
1765            nodes,
1766        };
1767
1768        let volume = MemoryVolume { node: dir };
1769        let mut volumes = BTreeMap::new();
1770
1771        volumes.insert("public".to_string(), volume);
1772
1773        let atoms = BTreeMap::new();
1774        let package = super::Package::from_in_memory(
1775            manifest,
1776            volumes,
1777            atoms,
1778            MemoryVolume {
1779                node: MemoryDir {
1780                    modified: SystemTime::now(),
1781                    nodes: BTreeMap::new(),
1782                },
1783            },
1784            Strictness::Strict,
1785        )?;
1786
1787        _ = package.serialize()?;
1788
1789        Ok(())
1790    }
1791
1792    #[test]
1793    fn compare_fs_mem_manifest() -> anyhow::Result<()> {
1794        let wasmer_toml = r#"
1795            [package]
1796            name = "test"
1797            version = "0.0.0"
1798            description = "asdf"
1799        "#;
1800
1801        let temp = TempDir::new()?;
1802        let manifest_path = temp.path().join("wasmer.toml");
1803        std::fs::write(&manifest_path, wasmer_toml).unwrap();
1804
1805        let fs_package = super::Package::from_manifest(manifest_path)?;
1806
1807        let manifest = toml::from_str(wasmer_toml)?;
1808        let memory_package = super::Package::from_in_memory(
1809            manifest,
1810            Default::default(),
1811            Default::default(),
1812            MemoryVolume {
1813                node: MemoryDir {
1814                    modified: SystemTime::UNIX_EPOCH,
1815                    nodes: BTreeMap::new(),
1816                },
1817            },
1818            Strictness::Lossy,
1819        )?;
1820
1821        assert_eq!(memory_package.serialize()?, fs_package.serialize()?);
1822
1823        Ok(())
1824    }
1825
1826    #[test]
1827    fn compare_fs_mem_manifest_and_atoms() -> anyhow::Result<()> {
1828        let wasmer_toml = r#"
1829            [package]
1830            name = "test"
1831            version = "0.0.0"
1832            description = "asdf"
1833
1834            [[module]]
1835            name = "foo"
1836            source = "foo.wasm"
1837            abi = "wasi"
1838        "#;
1839
1840        let temp = TempDir::new()?;
1841        let manifest_path = temp.path().join("wasmer.toml");
1842        std::fs::write(&manifest_path, wasmer_toml).unwrap();
1843
1844        let atom_path = temp.path().join("foo.wasm");
1845        std::fs::write(&atom_path, b"").unwrap();
1846
1847        let fs_package = super::Package::from_manifest(manifest_path)?;
1848
1849        let manifest = toml::from_str(wasmer_toml)?;
1850        let mut atoms = BTreeMap::new();
1851        atoms.insert("foo".to_owned(), (None, OwnedBuffer::new()));
1852        let memory_package = super::Package::from_in_memory(
1853            manifest,
1854            Default::default(),
1855            atoms,
1856            MemoryVolume {
1857                node: MemoryDir {
1858                    modified: SystemTime::UNIX_EPOCH,
1859                    nodes: BTreeMap::new(),
1860                },
1861            },
1862            Strictness::Lossy,
1863        )?;
1864
1865        assert_eq!(memory_package.serialize()?, fs_package.serialize()?);
1866
1867        Ok(())
1868    }
1869
1870    #[test]
1871    fn compare_fs_mem_volume() -> anyhow::Result<()> {
1872        let wasmer_toml = r#"
1873            [package]
1874            name = "test"
1875            version = "0.0.0"
1876            description = "asdf"
1877
1878            [[module]]
1879            name = "foo"
1880            source = "foo.wasm"
1881            abi = "wasi"
1882
1883            [fs]
1884            "/bar" = "bar"
1885        "#;
1886
1887        let temp = TempDir::new()?;
1888        let manifest_path = temp.path().join("wasmer.toml");
1889        std::fs::write(&manifest_path, wasmer_toml).unwrap();
1890
1891        let atom_path = temp.path().join("foo.wasm");
1892        std::fs::write(&atom_path, b"").unwrap();
1893
1894        let bar = temp.path().join("bar");
1895        std::fs::create_dir(&bar).unwrap();
1896
1897        let baz = bar.join("baz");
1898        std::fs::write(&baz, b"abc")?;
1899
1900        let baz_metadata = std::fs::metadata(&baz)?;
1901
1902        let fs_package = super::Package::from_manifest(manifest_path)?;
1903
1904        let manifest = toml::from_str(wasmer_toml)?;
1905
1906        let mut atoms = BTreeMap::new();
1907        atoms.insert("foo".to_owned(), (None, OwnedBuffer::new()));
1908
1909        let mut volumes = BTreeMap::new();
1910        volumes.insert(
1911            "/bar".to_owned(),
1912            MemoryVolume {
1913                node: MemoryDir {
1914                    modified: SystemTime::UNIX_EPOCH,
1915                    nodes: {
1916                        let mut children = BTreeMap::new();
1917
1918                        children.insert(
1919                            "baz".to_owned(),
1920                            MemoryNode::File(MemoryFile {
1921                                modified: baz_metadata.modified()?,
1922                                data: b"abc".to_vec(),
1923                            }),
1924                        );
1925
1926                        children
1927                    },
1928                },
1929            },
1930        );
1931        let memory_package = super::Package::from_in_memory(
1932            manifest,
1933            volumes,
1934            atoms,
1935            MemoryVolume {
1936                node: MemoryDir {
1937                    modified: SystemTime::UNIX_EPOCH,
1938                    nodes: Default::default(),
1939                },
1940            },
1941            Strictness::Lossy,
1942        )?;
1943
1944        assert_eq!(memory_package.serialize()?, fs_package.serialize()?);
1945
1946        Ok(())
1947    }
1948
1949    #[test]
1950    fn compare_fs_mem_bindings() -> anyhow::Result<()> {
1951        let temp = TempDir::new().unwrap();
1952
1953        let wasmer_toml = r#"
1954            [package]
1955            name = "some/package"
1956            version = "0.0.0"
1957            description = ""
1958            license-file = "LICENSE"
1959            readme = "README.md"
1960
1961            [[module]]
1962            name = "asdf"
1963            source = "asdf.wasm"
1964            abi = "none"
1965            bindings = { wai-version = "0.2.0", exports = "asdf.wai", imports = ["browser.wai"] }
1966
1967            [fs]
1968            "/dir1" = "local-dir1/dir"
1969            "/dir2" = "local-dir2/dir"
1970        "#;
1971
1972        let manifest = temp.path().join("wasmer.toml");
1973        std::fs::write(&manifest, wasmer_toml).unwrap();
1974
1975        std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
1976        std::fs::write(temp.path().join("README.md"), "readme").unwrap();
1977        std::fs::write(temp.path().join("asdf.wasm"), b"\0asm...").unwrap();
1978        std::fs::write(temp.path().join("asdf.wai"), "exports").unwrap();
1979        std::fs::write(temp.path().join("browser.wai"), "imports").unwrap();
1980
1981        // Now we want to set up the following filesystem tree:
1982        //
1983        // - local-dir1/
1984        //   - dir
1985        // - local-dir2/
1986        //   - dir
1987        let dir1 = temp.path().join("local-dir1").join("dir");
1988        std::fs::create_dir_all(&dir1).unwrap();
1989        let dir2 = temp.path().join("local-dir2").join("dir");
1990        std::fs::create_dir_all(&dir2).unwrap();
1991
1992        let fs_package = super::Package::from_manifest(manifest)?;
1993
1994        let manifest = toml::from_str(wasmer_toml)?;
1995
1996        let mut atoms = BTreeMap::new();
1997        atoms.insert(
1998            "asdf".to_owned(),
1999            (None, OwnedBuffer::from_static(b"\0asm...")),
2000        );
2001
2002        let mut volumes = BTreeMap::new();
2003        volumes.insert(
2004            "/local-dir1/dir".to_owned(),
2005            MemoryVolume {
2006                node: MemoryDir {
2007                    modified: SystemTime::UNIX_EPOCH,
2008                    nodes: Default::default(),
2009                },
2010            },
2011        );
2012        volumes.insert(
2013            "/local-dir2/dir".to_owned(),
2014            MemoryVolume {
2015                node: MemoryDir {
2016                    modified: SystemTime::UNIX_EPOCH,
2017                    nodes: Default::default(),
2018                },
2019            },
2020        );
2021
2022        let memory_package = super::Package::from_in_memory(
2023            manifest,
2024            volumes,
2025            atoms,
2026            MemoryVolume {
2027                node: MemoryDir {
2028                    modified: SystemTime::UNIX_EPOCH,
2029                    nodes: {
2030                        let mut children = BTreeMap::new();
2031
2032                        children.insert(
2033                            "README.md".to_owned(),
2034                            MemoryNode::File(MemoryFile {
2035                                modified: temp.path().join("README.md").metadata()?.modified()?,
2036                                data: b"readme".to_vec(),
2037                            }),
2038                        );
2039
2040                        children.insert(
2041                            "LICENSE".to_owned(),
2042                            MemoryNode::File(MemoryFile {
2043                                modified: temp.path().join("LICENSE").metadata()?.modified()?,
2044                                data: b"license".to_vec(),
2045                            }),
2046                        );
2047
2048                        children.insert(
2049                            "asdf.wai".to_owned(),
2050                            MemoryNode::File(MemoryFile {
2051                                modified: temp.path().join("asdf.wai").metadata()?.modified()?,
2052                                data: b"exports".to_vec(),
2053                            }),
2054                        );
2055
2056                        children.insert(
2057                            "browser.wai".to_owned(),
2058                            MemoryNode::File(MemoryFile {
2059                                modified: temp.path().join("browser.wai").metadata()?.modified()?,
2060                                data: b"imports".to_vec(),
2061                            }),
2062                        );
2063
2064                        children
2065                    },
2066                },
2067            },
2068            Strictness::Lossy,
2069        )?;
2070
2071        let memory_package = memory_package.serialize()?;
2072        let fs_package = fs_package.serialize()?;
2073
2074        assert_eq!(memory_package, fs_package);
2075
2076        Ok(())
2077    }
2078}