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            && let Some(parent) = current_exe.parent()
705            && let Ok(temp) = TempDir::new_in(parent)
706        {
707            return Ok(temp);
708        }
709
710        // Oh well, this will probably fail, but at least we tried.
711        std::fs::create_dir_all(&temp_dir)?;
712        TempDir::new_in(temp_dir)
713    }
714}
715
716/// A polyfill for [`Archive::unpack()`] that is WASI-compatible.
717///
718/// This works around `canonicalize()` being [unsupported][github] on
719/// `wasm32-wasip1`.
720///
721/// [github]: https://github.com/rust-lang/rust/blob/5b1dc9de77106cb08ce9a1a8deaa14f52751d7e4/library/std/src/sys/wasi/fs.rs#L654-L658
722fn unpack_archive(
723    mut archive: Archive<impl std::io::Read>,
724    dest: &Path,
725) -> Result<(), std::io::Error> {
726    cfg_if::cfg_if! {
727        if #[cfg(all(target_family = "wasm", target_os = "wasi"))]
728        {
729            // A naive version of unpack() that should be good enough for WASI
730            // https://github.com/alexcrichton/tar-rs/blob/c77f47cb1b4b47fc4404a170d9d91cb42cc762ea/src/archive.rs#L216-L247
731            for entry in archive.entries()? {
732                let mut entry = entry?;
733                let item_path = entry.path()?;
734                let full_path = resolve_archive_path(dest, &item_path);
735
736                match entry.header().entry_type() {
737                    tar::EntryType::Directory => {
738                        std::fs::create_dir_all(&full_path)?;
739                    }
740                    tar::EntryType::Regular => {
741                        if let Some(parent) = full_path.parent() {
742                            std::fs::create_dir_all(parent)?;
743                        }
744                        let mut f = File::create(&full_path)?;
745                        std::io::copy(&mut entry, &mut f)?;
746
747                        let mtime = entry.header().mtime().unwrap_or_default();
748                        if let Err(e) = set_timestamp(full_path.as_path(), mtime) {
749                            println!("WARN: {e:?}");
750                        }
751                    }
752                    _ => {}
753                }
754            }
755            Ok(())
756
757        } else {
758            archive.unpack(dest)
759        }
760    }
761}
762
763#[cfg(all(target_family = "wasm", target_os = "wasi"))]
764fn set_timestamp(path: &Path, timestamp: u64) -> Result<(), anyhow::Error> {
765    let fd = unsafe {
766        libc::open(
767            path.as_os_str().as_encoded_bytes().as_ptr() as _,
768            libc::O_RDONLY,
769        )
770    };
771
772    if fd < 0 {
773        anyhow::bail!(format!("failed to open: {}", path.display()));
774    }
775
776    let timespec = [
777        // accessed
778        libc::timespec {
779            tv_sec: unsafe { libc::time(std::ptr::null_mut()) }, // now
780            tv_nsec: 0,
781        },
782        // modified
783        libc::timespec {
784            tv_sec: timestamp as i64,
785            tv_nsec: 0,
786        },
787    ];
788
789    let res = unsafe { libc::futimens(fd, timespec.as_ptr() as _) };
790
791    if res < 0 {
792        anyhow::bail!("failed to set timestamp for: {}", path.display());
793    }
794
795    Ok(())
796}
797
798#[cfg(all(target_family = "wasm", target_os = "wasi"))]
799fn resolve_archive_path(base_dir: &Path, path: &Path) -> PathBuf {
800    let mut buffer = base_dir.to_path_buf();
801
802    for component in path.components() {
803        match component {
804            std::path::Component::Prefix(_)
805            | std::path::Component::RootDir
806            | std::path::Component::CurDir => continue,
807            std::path::Component::ParentDir => {
808                buffer.pop();
809            }
810            std::path::Component::Normal(segment) => {
811                buffer.push(segment);
812            }
813        }
814    }
815
816    buffer
817}
818
819fn read_manifest(base_dir: &Path) -> Result<(PathBuf, WasmerManifest), WasmerPackageError> {
820    for path in ["wasmer.toml", "wapm.toml"] {
821        let path = base_dir.join(path);
822
823        match std::fs::read_to_string(&path) {
824            Ok(s) => {
825                let toml_file = toml::from_str(&s).map_err({
826                    let path = path.clone();
827                    |error| WasmerPackageError::TomlDeserialize { path, error }
828                })?;
829
830                return Ok((path, toml_file));
831            }
832            Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
833            Err(error) => {
834                return Err(WasmerPackageError::FileRead { path, error });
835            }
836        }
837    }
838
839    Err(WasmerPackageError::MissingManifest)
840}
841
842#[derive(Debug)]
843enum BaseDir {
844    /// An existing directory.
845    Path(PathBuf),
846    /// A temporary directory that will be deleted on drop.
847    Temp(TempDir),
848}
849
850impl BaseDir {
851    fn path(&self) -> &Path {
852        match self {
853            BaseDir::Path(p) => p.as_path(),
854            BaseDir::Temp(t) => t.path(),
855        }
856    }
857}
858
859impl From<TempDir> for BaseDir {
860    fn from(v: TempDir) -> Self {
861        Self::Temp(v)
862    }
863}
864
865impl From<PathBuf> for BaseDir {
866    fn from(v: PathBuf) -> Self {
867        Self::Path(v)
868    }
869}
870
871#[cfg(test)]
872mod tests {
873    use std::{
874        collections::BTreeMap,
875        fs::File,
876        path::{Path, PathBuf},
877        str::FromStr,
878        time::SystemTime,
879    };
880
881    use flate2::{Compression, write::GzEncoder};
882    use sha2::Digest;
883    use shared_buffer::OwnedBuffer;
884    use tempfile::TempDir;
885    use webc::{
886        PathSegment, PathSegments,
887        metadata::{
888            Binding, BindingsExtended, WaiBindings, WitBindings,
889            annotations::{FileSystemMapping, VolumeSpecificPath},
890        },
891    };
892
893    use crate::{package::*, utils::from_bytes};
894
895    #[test]
896    fn nonexistent_files() {
897        let temp = TempDir::new().unwrap();
898
899        assert!(Package::from_manifest(temp.path().join("nonexistent.toml")).is_err());
900        assert!(Package::from_tarball_file(temp.path().join("nonexistent.tar.gz")).is_err());
901    }
902
903    #[test]
904    fn load_a_tarball() {
905        let coreutils = Path::new(env!("CARGO_MANIFEST_DIR"))
906            .join("..")
907            .join("..")
908            .join("tests")
909            .join("old-tar-gz")
910            .join("coreutils-1.0.11.tar.gz");
911        assert!(coreutils.exists());
912
913        let package = Package::from_tarball_file(coreutils).unwrap();
914
915        let wapm = package.manifest().wapm().unwrap().unwrap();
916        assert!(wapm.name.is_none());
917        assert!(wapm.version.is_none());
918        assert!(wapm.description.is_none());
919    }
920
921    #[test]
922    fn tarball_with_no_manifest() {
923        let temp = TempDir::new().unwrap();
924        let empty_tarball = temp.path().join("empty.tar.gz");
925        let mut f = File::create(&empty_tarball).unwrap();
926        tar::Builder::new(GzEncoder::new(&mut f, Compression::fast()))
927            .finish()
928            .unwrap();
929
930        assert!(Package::from_tarball_file(&empty_tarball).is_err());
931    }
932
933    #[test]
934    fn empty_package_on_disk() {
935        let temp = TempDir::new().unwrap();
936        let manifest = temp.path().join("wasmer.toml");
937        std::fs::write(
938            &manifest,
939            r#"
940                [package]
941                name = "some/package"
942                version = "0.0.0"
943                description = "A dummy package"
944            "#,
945        )
946        .unwrap();
947
948        let package = Package::from_manifest(&manifest).unwrap();
949
950        let wapm = package.manifest().wapm().unwrap().unwrap();
951        assert!(wapm.name.is_none());
952        assert!(wapm.version.is_none());
953        assert!(wapm.description.is_none());
954    }
955
956    #[test]
957    fn load_old_cowsay() {
958        let tarball = Path::new(env!("CARGO_MANIFEST_DIR"))
959            .join("..")
960            .join("..")
961            .join("tests")
962            .join("old-tar-gz")
963            .join("cowsay-0.3.0.tar.gz");
964
965        let pkg = Package::from_tarball_file(tarball).unwrap();
966
967        insta::assert_yaml_snapshot!(pkg.manifest());
968        assert_eq!(
969            pkg.manifest.commands.keys().collect::<Vec<_>>(),
970            ["cowsay", "cowthink"],
971        );
972    }
973
974    #[test]
975    fn serialize_package_with_non_existent_fs() {
976        let temp = TempDir::new().unwrap();
977        let wasmer_toml = r#"
978                [package]
979                name = "some/package"
980                version = "0.0.0"
981                description = "Test package"
982
983                [fs]
984                "/first" = "./first"
985            "#;
986        let manifest = temp.path().join("wasmer.toml");
987
988        std::fs::write(&manifest, wasmer_toml).unwrap();
989
990        let error = Package::from_manifest(manifest).unwrap_err();
991
992        match error {
993            WasmerPackageError::PathNotExists { path } => {
994                assert_eq!(path, PathBuf::from_str("./first").unwrap());
995            }
996            e => panic!("unexpected error: {e:?}"),
997        }
998    }
999
1000    #[test]
1001    fn serialize_package_with_bundled_directories() {
1002        let temp = TempDir::new().unwrap();
1003        let wasmer_toml = r#"
1004                [package]
1005                name = "some/package"
1006                version = "0.0.0"
1007                description = "Test package"
1008
1009                [fs]
1010                "/first" = "first"
1011                second = "nested/dir"
1012                "second/child" = "third"
1013                empty = "empty"
1014            "#;
1015        let manifest = temp.path().join("wasmer.toml");
1016        std::fs::write(&manifest, wasmer_toml).unwrap();
1017        // Now we want to set up the following filesystem tree:
1018        //
1019        // - first/ ("/first")
1020        //   - file.txt
1021        // - nested/
1022        //   - dir/ ("second")
1023        //     - .wasmerignore
1024        //     - .hidden (should be ignored)
1025        //     - ignore_me (should be ignored)
1026        //     - README.md
1027        //     - another-dir/
1028        //       - empty.txt
1029        // - third/ ("second/child")
1030        //   - file.txt
1031        // - empty/ ("empty")
1032        //
1033        // The "/first" entry
1034        let first = temp.path().join("first");
1035        std::fs::create_dir_all(&first).unwrap();
1036        std::fs::write(first.join("file.txt"), "File").unwrap();
1037        // The "second" entry
1038        let second = temp.path().join("nested").join("dir");
1039        std::fs::create_dir_all(&second).unwrap();
1040        std::fs::write(second.join(".wasmerignore"), "ignore_me").unwrap();
1041        std::fs::write(second.join(".hidden"), "something something").unwrap();
1042        std::fs::write(second.join("ignore_me"), "something something").unwrap();
1043        std::fs::write(second.join("README.md"), "please").unwrap();
1044        let another_dir = temp.path().join("nested").join("dir").join("another-dir");
1045        std::fs::create_dir_all(&another_dir).unwrap();
1046        std::fs::write(another_dir.join("empty.txt"), "").unwrap();
1047        // The "second/child" entry
1048        let third = temp.path().join("third");
1049        std::fs::create_dir_all(&third).unwrap();
1050        std::fs::write(third.join("file.txt"), "Hello, World!").unwrap();
1051        // The "empty" entry
1052        let empty_dir = temp.path().join("empty");
1053        std::fs::create_dir_all(empty_dir).unwrap();
1054
1055        let package = Package::from_manifest(manifest).unwrap();
1056
1057        let webc = package.serialize().unwrap();
1058        let webc = from_bytes(webc).unwrap();
1059        let manifest = webc.manifest();
1060        let wapm_metadata = manifest.wapm().unwrap().unwrap();
1061        assert!(wapm_metadata.name.is_none());
1062        assert!(wapm_metadata.version.is_none());
1063        assert!(wapm_metadata.description.is_none());
1064        let fs_table = manifest.filesystem().unwrap().unwrap();
1065        assert_eq!(
1066            fs_table,
1067            [
1068                FileSystemMapping {
1069                    from: None,
1070                    volume_name: "/first".to_string(),
1071                    host_path: None,
1072                    mount_path: "/first".to_string(),
1073                },
1074                FileSystemMapping {
1075                    from: None,
1076                    volume_name: "/nested/dir".to_string(),
1077                    host_path: None,
1078                    mount_path: "/second".to_string(),
1079                },
1080                FileSystemMapping {
1081                    from: None,
1082                    volume_name: "/third".to_string(),
1083                    host_path: None,
1084                    mount_path: "/second/child".to_string(),
1085                },
1086                FileSystemMapping {
1087                    from: None,
1088                    volume_name: "/empty".to_string(),
1089                    host_path: None,
1090                    mount_path: "/empty".to_string(),
1091                },
1092            ]
1093        );
1094
1095        let first_file_hash: [u8; 32] = sha2::Sha256::digest(b"File").into();
1096        let readme_hash: [u8; 32] = sha2::Sha256::digest(b"please").into();
1097        let empty_hash: [u8; 32] = sha2::Sha256::digest(b"").into();
1098        let third_file_hash: [u8; 32] = sha2::Sha256::digest(b"Hello, World!").into();
1099
1100        let first_volume = webc.get_volume("/first").unwrap();
1101        assert_eq!(
1102            first_volume.read_file("/file.txt").unwrap(),
1103            (b"File".as_slice().into(), Some(first_file_hash)),
1104        );
1105
1106        let nested_dir_volume = webc.get_volume("/nested/dir").unwrap();
1107        assert_eq!(
1108            nested_dir_volume.read_file("README.md").unwrap(),
1109            (b"please".as_slice().into(), Some(readme_hash)),
1110        );
1111        assert!(nested_dir_volume.read_file(".wasmerignore").is_some());
1112        assert!(nested_dir_volume.read_file(".hidden").is_some());
1113        assert!(nested_dir_volume.read_file("ignore_me").is_none());
1114        assert_eq!(
1115            nested_dir_volume
1116                .read_file("/another-dir/empty.txt")
1117                .unwrap(),
1118            (b"".as_slice().into(), Some(empty_hash))
1119        );
1120
1121        let third_volume = webc.get_volume("/third").unwrap();
1122        assert_eq!(
1123            third_volume.read_file("/file.txt").unwrap(),
1124            (b"Hello, World!".as_slice().into(), Some(third_file_hash))
1125        );
1126
1127        let empty_volume = webc.get_volume("/empty").unwrap();
1128        assert_eq!(
1129            empty_volume.read_dir("/").unwrap().len(),
1130            0,
1131            "Directories should be included, even if empty"
1132        );
1133    }
1134
1135    #[test]
1136    fn serialize_package_with_metadata_files() {
1137        let temp = TempDir::new().unwrap();
1138        let wasmer_toml = r#"
1139                [package]
1140                name = "some/package"
1141                version = "0.0.0"
1142                description = "Test package"
1143                readme = "README.md"
1144                license-file = "LICENSE"
1145            "#;
1146        let manifest = temp.path().join("wasmer.toml");
1147        std::fs::write(&manifest, wasmer_toml).unwrap();
1148        std::fs::write(temp.path().join("README.md"), "readme").unwrap();
1149        std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
1150
1151        let serialized = Package::from_manifest(manifest)
1152            .unwrap()
1153            .serialize()
1154            .unwrap();
1155
1156        let webc = from_bytes(serialized).unwrap();
1157        let metadata_volume = webc.get_volume("metadata").unwrap();
1158
1159        let readme_hash: [u8; 32] = sha2::Sha256::digest(b"readme").into();
1160        let license_hash: [u8; 32] = sha2::Sha256::digest(b"license").into();
1161
1162        assert_eq!(
1163            metadata_volume.read_file("/README.md").unwrap(),
1164            (b"readme".as_slice().into(), Some(readme_hash))
1165        );
1166        assert_eq!(
1167            metadata_volume.read_file("/LICENSE").unwrap(),
1168            (b"license".as_slice().into(), Some(license_hash))
1169        );
1170    }
1171
1172    #[test]
1173    fn load_package_with_wit_bindings() {
1174        let temp = TempDir::new().unwrap();
1175        let wasmer_toml = r#"
1176            [package]
1177            name = "some/package"
1178            version = "0.0.0"
1179            description = ""
1180
1181            [[module]]
1182            name = "my-lib"
1183            source = "./my-lib.wasm"
1184            abi = "none"
1185            bindings = { wit-bindgen = "0.1.0", wit-exports = "./file.wit" }
1186        "#;
1187        std::fs::write(temp.path().join("wasmer.toml"), wasmer_toml).unwrap();
1188        std::fs::write(temp.path().join("file.wit"), "file").unwrap();
1189        std::fs::write(temp.path().join("my-lib.wasm"), b"\0asm...").unwrap();
1190
1191        let package = Package::from_manifest(temp.path().join("wasmer.toml"))
1192            .unwrap()
1193            .serialize()
1194            .unwrap();
1195        let webc = from_bytes(package).unwrap();
1196
1197        assert_eq!(
1198            webc.manifest().bindings,
1199            vec![Binding {
1200                name: "library-bindings".to_string(),
1201                kind: "wit@0.1.0".to_string(),
1202                annotations: ciborium::value::Value::serialized(&BindingsExtended::Wit(
1203                    WitBindings {
1204                        exports: "metadata://file.wit".to_string(),
1205                        module: "my-lib".to_string(),
1206                    }
1207                ))
1208                .unwrap(),
1209            }]
1210        );
1211        let metadata_volume = webc.get_volume("metadata").unwrap();
1212        let file_hash: [u8; 32] = sha2::Sha256::digest(b"file").into();
1213        assert_eq!(
1214            metadata_volume.read_file("/file.wit").unwrap(),
1215            (b"file".as_slice().into(), Some(file_hash))
1216        );
1217        insta::with_settings! {
1218            { description => wasmer_toml },
1219            { insta::assert_yaml_snapshot!(webc.manifest()); }
1220        }
1221    }
1222
1223    #[test]
1224    fn load_package_with_wai_bindings() {
1225        let temp = TempDir::new().unwrap();
1226        let wasmer_toml = r#"
1227            [package]
1228            name = "some/package"
1229            version = "0.0.0"
1230            description = ""
1231
1232            [[module]]
1233            name = "my-lib"
1234            source = "./my-lib.wasm"
1235            abi = "none"
1236            bindings = { wai-version = "0.2.0", exports = "./file.wai", imports = ["a.wai", "b.wai"] }
1237        "#;
1238        std::fs::write(temp.path().join("wasmer.toml"), wasmer_toml).unwrap();
1239        std::fs::write(temp.path().join("file.wai"), "file").unwrap();
1240        std::fs::write(temp.path().join("a.wai"), "a").unwrap();
1241        std::fs::write(temp.path().join("b.wai"), "b").unwrap();
1242        std::fs::write(temp.path().join("my-lib.wasm"), b"\0asm...").unwrap();
1243
1244        let package = Package::from_manifest(temp.path().join("wasmer.toml"))
1245            .unwrap()
1246            .serialize()
1247            .unwrap();
1248        let webc = from_bytes(package).unwrap();
1249
1250        assert_eq!(
1251            webc.manifest().bindings,
1252            vec![Binding {
1253                name: "library-bindings".to_string(),
1254                kind: "wai@0.2.0".to_string(),
1255                annotations: ciborium::value::Value::serialized(&BindingsExtended::Wai(
1256                    WaiBindings {
1257                        exports: Some("metadata://file.wai".to_string()),
1258                        module: "my-lib".to_string(),
1259                        imports: vec![
1260                            "metadata://a.wai".to_string(),
1261                            "metadata://b.wai".to_string(),
1262                        ]
1263                    }
1264                ))
1265                .unwrap(),
1266            }]
1267        );
1268        let metadata_volume = webc.get_volume("metadata").unwrap();
1269
1270        let file_hash: [u8; 32] = sha2::Sha256::digest(b"file").into();
1271        let a_hash: [u8; 32] = sha2::Sha256::digest(b"a").into();
1272        let b_hash: [u8; 32] = sha2::Sha256::digest(b"b").into();
1273
1274        assert_eq!(
1275            metadata_volume.read_file("/file.wai").unwrap(),
1276            (b"file".as_slice().into(), Some(file_hash))
1277        );
1278        assert_eq!(
1279            metadata_volume.read_file("/a.wai").unwrap(),
1280            (b"a".as_slice().into(), Some(a_hash))
1281        );
1282        assert_eq!(
1283            metadata_volume.read_file("/b.wai").unwrap(),
1284            (b"b".as_slice().into(), Some(b_hash))
1285        );
1286        insta::with_settings! {
1287            { description => wasmer_toml },
1288            { insta::assert_yaml_snapshot!(webc.manifest()); }
1289        }
1290    }
1291
1292    /// See <https://github.com/wasmerio/pirita/issues/105> for more.
1293    #[test]
1294    fn absolute_paths_in_wasmer_toml_issue_105() {
1295        let temp = TempDir::new().unwrap();
1296        let base_dir = temp.path().canonicalize().unwrap();
1297        let sep = std::path::MAIN_SEPARATOR;
1298        let wasmer_toml = format!(
1299            r#"
1300                [package]
1301                name = 'some/package'
1302                version = '0.0.0'
1303                description = 'Test package'
1304                readme = '{BASE_DIR}{sep}README.md'
1305                license-file = '{BASE_DIR}{sep}LICENSE'
1306
1307                [[module]]
1308                name = 'first'
1309                source = '{BASE_DIR}{sep}target{sep}debug{sep}package.wasm'
1310                bindings = {{ wai-version = '0.2.0', exports = '{BASE_DIR}{sep}bindings{sep}file.wai', imports = ['{BASE_DIR}{sep}bindings{sep}a.wai'] }}
1311            "#,
1312            BASE_DIR = base_dir.display(),
1313        );
1314        let manifest = temp.path().join("wasmer.toml");
1315        std::fs::write(&manifest, &wasmer_toml).unwrap();
1316        std::fs::write(temp.path().join("README.md"), "readme").unwrap();
1317        std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
1318        let bindings = temp.path().join("bindings");
1319        std::fs::create_dir_all(&bindings).unwrap();
1320        std::fs::write(bindings.join("file.wai"), "file.wai").unwrap();
1321        std::fs::write(bindings.join("a.wai"), "a.wai").unwrap();
1322        let target = temp.path().join("target").join("debug");
1323        std::fs::create_dir_all(&target).unwrap();
1324        std::fs::write(target.join("package.wasm"), b"\0asm...").unwrap();
1325
1326        let serialized = Package::from_manifest(manifest)
1327            .unwrap()
1328            .serialize()
1329            .unwrap();
1330
1331        let webc = from_bytes(serialized).unwrap();
1332        let manifest = webc.manifest();
1333        let wapm = manifest.wapm().unwrap().unwrap();
1334
1335        // we should be able to look up the files using the manifest
1336        let lookup = |item: VolumeSpecificPath| {
1337            let volume = webc.get_volume(&item.volume).unwrap();
1338            let (contents, _) = volume.read_file(&item.path).unwrap();
1339            String::from_utf8(contents.into()).unwrap()
1340        };
1341        assert_eq!(lookup(wapm.license_file.unwrap()), "license");
1342        assert_eq!(lookup(wapm.readme.unwrap()), "readme");
1343
1344        // The paths for bindings are stored slightly differently, but it's the
1345        // same general idea
1346        let lookup = |item: &str| {
1347            let (volume, path) = item.split_once(":/").unwrap();
1348            let volume = webc.get_volume(volume).unwrap();
1349            let (content, _) = volume.read_file(path).unwrap();
1350            String::from_utf8(content.into()).unwrap()
1351        };
1352        let bindings = manifest.bindings[0].get_wai_bindings().unwrap();
1353        assert_eq!(lookup(&bindings.imports[0]), "a.wai");
1354        assert_eq!(lookup(bindings.exports.unwrap().as_str()), "file.wai");
1355
1356        // Snapshot tests for good measure
1357        let mut settings = insta::Settings::clone_current();
1358        let base_dir = base_dir.display().to_string();
1359        settings.set_description(wasmer_toml.replace(&base_dir, "[BASE_DIR]"));
1360        let filter = regex::escape(&base_dir);
1361        settings.add_filter(&filter, "[BASE_DIR]");
1362        settings.bind(|| {
1363            insta::assert_yaml_snapshot!(webc.manifest());
1364        });
1365    }
1366
1367    #[test]
1368    fn serializing_will_skip_missing_metadata_by_default() {
1369        let temp = TempDir::new().unwrap();
1370        let wasmer_toml = r#"
1371                [package]
1372                name = 'some/package'
1373                version = '0.0.0'
1374                description = 'Test package'
1375                readme = '/this/does/not/exist/README.md'
1376                license-file = 'LICENSE.wtf'
1377            "#;
1378        let manifest = temp.path().join("wasmer.toml");
1379        std::fs::write(&manifest, wasmer_toml).unwrap();
1380        let pkg = Package::from_manifest(manifest).unwrap();
1381
1382        let serialized = pkg.serialize().unwrap();
1383
1384        let webc = from_bytes(serialized).unwrap();
1385        let manifest = webc.manifest();
1386        let wapm = manifest.wapm().unwrap().unwrap();
1387        // We re-wrote the WAPM annotations to just not include the license file
1388        assert!(wapm.license_file.is_none());
1389        assert!(wapm.readme.is_none());
1390
1391        // Note: serializing in strict mode should still fail
1392        let pkg = Package {
1393            strictness: Strictness::Strict,
1394            ..pkg
1395        };
1396        assert!(pkg.serialize().is_err());
1397    }
1398
1399    #[test]
1400    fn serialize_package_without_local_base_fs_paths() {
1401        let temp = TempDir::new().unwrap();
1402        let wasmer_toml = r#"
1403                [package]
1404                name = "some/package"
1405                version = "0.0.0"
1406                description = "Test package"
1407                readme = 'README.md'
1408                license-file = 'LICENSE'
1409
1410                [fs]
1411                "/path_in_wasix" = "local-dir/dir1"
1412            "#;
1413        let manifest = temp.path().join("wasmer.toml");
1414        std::fs::write(&manifest, wasmer_toml).unwrap();
1415
1416        std::fs::write(temp.path().join("README.md"), "readme").unwrap();
1417        std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
1418
1419        // Now we want to set up the following filesystem tree:
1420        //
1421        // - local-dir/
1422        //   - dir1/
1423        //     - a
1424        //     - b
1425        let dir1 = temp.path().join("local-dir").join("dir1");
1426        std::fs::create_dir_all(&dir1).unwrap();
1427
1428        let a = dir1.join("a");
1429        let b = dir1.join("b");
1430
1431        std::fs::write(a, "a").unwrap();
1432        std::fs::write(b, "b").unwrap();
1433
1434        let package = Package::from_manifest(manifest).unwrap();
1435
1436        let webc = package.serialize().unwrap();
1437        let webc = from_bytes(webc).unwrap();
1438        let manifest = webc.manifest();
1439        let wapm_metadata = manifest.wapm().unwrap().unwrap();
1440
1441        assert!(wapm_metadata.name.is_none());
1442        assert!(wapm_metadata.version.is_none());
1443        assert!(wapm_metadata.description.is_none());
1444
1445        let fs_table = manifest.filesystem().unwrap().unwrap();
1446        assert_eq!(
1447            fs_table,
1448            [FileSystemMapping {
1449                from: None,
1450                volume_name: "/local-dir/dir1".to_string(),
1451                host_path: None,
1452                mount_path: "/path_in_wasix".to_string(),
1453            },]
1454        );
1455
1456        let readme_hash: [u8; 32] = sha2::Sha256::digest(b"readme").into();
1457        let license_hash: [u8; 32] = sha2::Sha256::digest(b"license").into();
1458
1459        let a_hash: [u8; 32] = sha2::Sha256::digest(b"a").into();
1460        let b_hash: [u8; 32] = sha2::Sha256::digest(b"b").into();
1461
1462        let dir1_volume = webc.get_volume("/local-dir/dir1").unwrap();
1463        let meta_volume = webc.get_volume("metadata").unwrap();
1464
1465        assert_eq!(
1466            meta_volume.read_file("LICENSE").unwrap(),
1467            (b"license".as_slice().into(), Some(license_hash)),
1468        );
1469        assert_eq!(
1470            meta_volume.read_file("README.md").unwrap(),
1471            (b"readme".as_slice().into(), Some(readme_hash)),
1472        );
1473        assert_eq!(
1474            dir1_volume.read_file("a").unwrap(),
1475            (b"a".as_slice().into(), Some(a_hash))
1476        );
1477        assert_eq!(
1478            dir1_volume.read_file("b").unwrap(),
1479            (b"b".as_slice().into(), Some(b_hash))
1480        );
1481    }
1482
1483    #[test]
1484    fn serialize_package_with_nested_fs_entries_without_local_base_fs_paths() {
1485        let temp = TempDir::new().unwrap();
1486        let wasmer_toml = r#"
1487                [package]
1488                name = "some/package"
1489                version = "0.0.0"
1490                description = "Test package"
1491                readme = 'README.md'
1492                license-file = 'LICENSE'
1493
1494                [fs]
1495                "/path_in_wasix" = "local-dir/dir1"
1496            "#;
1497        let manifest = temp.path().join("wasmer.toml");
1498        std::fs::write(&manifest, wasmer_toml).unwrap();
1499
1500        std::fs::write(temp.path().join("README.md"), "readme").unwrap();
1501        std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
1502
1503        // Now we want to set up the following filesystem tree:
1504        //
1505        // - local-dir/
1506        //   - dir1/
1507        //     - dir2/
1508        //       - a
1509        //     - b
1510        let local_dir = temp.path().join("local-dir");
1511        std::fs::create_dir_all(&local_dir).unwrap();
1512
1513        let dir1 = local_dir.join("dir1");
1514        std::fs::create_dir_all(&dir1).unwrap();
1515
1516        let dir2 = dir1.join("dir2");
1517        std::fs::create_dir_all(&dir2).unwrap();
1518
1519        let a = dir2.join("a");
1520        let b = dir1.join("b");
1521
1522        std::fs::write(a, "a").unwrap();
1523        std::fs::write(b, "b").unwrap();
1524
1525        let package = Package::from_manifest(manifest).unwrap();
1526
1527        let webc = package.serialize().unwrap();
1528        let webc = from_bytes(webc).unwrap();
1529        let manifest = webc.manifest();
1530        let wapm_metadata = manifest.wapm().unwrap().unwrap();
1531
1532        assert!(wapm_metadata.name.is_none());
1533        assert!(wapm_metadata.version.is_none());
1534        assert!(wapm_metadata.description.is_none());
1535
1536        let fs_table = manifest.filesystem().unwrap().unwrap();
1537        assert_eq!(
1538            fs_table,
1539            [FileSystemMapping {
1540                from: None,
1541                volume_name: "/local-dir/dir1".to_string(),
1542                host_path: None,
1543                mount_path: "/path_in_wasix".to_string(),
1544            },]
1545        );
1546
1547        let readme_hash: [u8; 32] = sha2::Sha256::digest(b"readme").into();
1548        let license_hash: [u8; 32] = sha2::Sha256::digest(b"license").into();
1549
1550        let a_hash: [u8; 32] = sha2::Sha256::digest(b"a").into();
1551        let dir2_hash: [u8; 32] = sha2::Sha256::digest(a_hash).into();
1552        let b_hash: [u8; 32] = sha2::Sha256::digest(b"b").into();
1553
1554        let dir1_volume = webc.get_volume("/local-dir/dir1").unwrap();
1555        let meta_volume = webc.get_volume("metadata").unwrap();
1556
1557        assert_eq!(
1558            meta_volume.read_file("LICENSE").unwrap(),
1559            (b"license".as_slice().into(), Some(license_hash)),
1560        );
1561        assert_eq!(
1562            meta_volume.read_file("README.md").unwrap(),
1563            (b"readme".as_slice().into(), Some(readme_hash)),
1564        );
1565        assert_eq!(
1566            dir1_volume
1567                .read_dir("/")
1568                .unwrap()
1569                .into_iter()
1570                .map(|(p, h, _)| (p, h))
1571                .collect::<Vec<_>>(),
1572            vec![
1573                (PathSegment::parse("b").unwrap(), Some(b_hash)),
1574                (PathSegment::parse("dir2").unwrap(), Some(dir2_hash))
1575            ]
1576        );
1577        assert_eq!(
1578            dir1_volume
1579                .read_dir("/dir2")
1580                .unwrap()
1581                .into_iter()
1582                .map(|(p, h, _)| (p, h))
1583                .collect::<Vec<_>>(),
1584            vec![(PathSegment::parse("a").unwrap(), Some(a_hash))]
1585        );
1586        assert_eq!(
1587            dir1_volume.read_file("/dir2/a").unwrap(),
1588            (b"a".as_slice().into(), Some(a_hash))
1589        );
1590        assert_eq!(
1591            dir1_volume.read_file("/b").unwrap(),
1592            (b"b".as_slice().into(), Some(b_hash))
1593        );
1594    }
1595
1596    #[test]
1597    fn serialize_package_mapped_to_same_dir_without_local_base_fs_paths() {
1598        let temp = TempDir::new().unwrap();
1599        let wasmer_toml = r#"
1600                [package]
1601                name = "some/package"
1602                version = "0.0.0"
1603                description = "Test package"
1604                readme = 'README.md'
1605                license-file = 'LICENSE'
1606
1607                [fs]
1608                "/dir1" = "local-dir1/dir"
1609                "/dir2" = "local-dir2/dir"
1610            "#;
1611        let manifest = temp.path().join("wasmer.toml");
1612        std::fs::write(&manifest, wasmer_toml).unwrap();
1613
1614        std::fs::write(temp.path().join("README.md"), "readme").unwrap();
1615        std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
1616
1617        // Now we want to set up the following filesystem tree:
1618        //
1619        // - local-dir1/
1620        //   - dir
1621        // - local-dir2/
1622        //   - dir
1623        let dir1 = temp.path().join("local-dir1").join("dir");
1624        std::fs::create_dir_all(&dir1).unwrap();
1625        let dir2 = temp.path().join("local-dir2").join("dir");
1626        std::fs::create_dir_all(&dir2).unwrap();
1627
1628        let package = Package::from_manifest(manifest).unwrap();
1629
1630        let webc = package.serialize().unwrap();
1631        let webc = from_bytes(webc).unwrap();
1632        let manifest = webc.manifest();
1633        let wapm_metadata = manifest.wapm().unwrap().unwrap();
1634
1635        assert!(wapm_metadata.name.is_none());
1636        assert!(wapm_metadata.version.is_none());
1637        assert!(wapm_metadata.description.is_none());
1638
1639        let fs_table = manifest.filesystem().unwrap().unwrap();
1640        assert_eq!(
1641            fs_table,
1642            [
1643                FileSystemMapping {
1644                    from: None,
1645                    volume_name: "/local-dir1/dir".to_string(),
1646                    host_path: None,
1647                    mount_path: "/dir1".to_string(),
1648                },
1649                FileSystemMapping {
1650                    from: None,
1651                    volume_name: "/local-dir2/dir".to_string(),
1652                    host_path: None,
1653                    mount_path: "/dir2".to_string(),
1654                },
1655            ]
1656        );
1657
1658        let readme_hash: [u8; 32] = sha2::Sha256::digest(b"readme").into();
1659        let license_hash: [u8; 32] = sha2::Sha256::digest(b"license").into();
1660
1661        let dir1_volume = webc.get_volume("/local-dir1/dir").unwrap();
1662        let dir2_volume = webc.get_volume("/local-dir2/dir").unwrap();
1663        let meta_volume = webc.get_volume("metadata").unwrap();
1664
1665        assert_eq!(
1666            meta_volume.read_file("LICENSE").unwrap(),
1667            (b"license".as_slice().into(), Some(license_hash)),
1668        );
1669        assert_eq!(
1670            meta_volume.read_file("README.md").unwrap(),
1671            (b"readme".as_slice().into(), Some(readme_hash)),
1672        );
1673        assert!(dir1_volume.read_dir("/").unwrap().is_empty());
1674        assert!(dir2_volume.read_dir("/").unwrap().is_empty());
1675    }
1676
1677    #[test]
1678    fn metadata_only_contains_relevant_files() {
1679        let temp = TempDir::new().unwrap();
1680        let wasmer_toml = r#"
1681            [package]
1682            name = "some/package"
1683            version = "0.0.0"
1684            description = ""
1685            license-file = "./path/to/LICENSE"
1686            readme = "README.md"
1687
1688            [[module]]
1689            name = "asdf"
1690            source = "asdf.wasm"
1691            abi = "none"
1692            bindings = { wai-version = "0.2.0", exports = "asdf.wai", imports = ["browser.wai"] }
1693        "#;
1694
1695        let manifest = temp.path().join("wasmer.toml");
1696        std::fs::write(&manifest, wasmer_toml).unwrap();
1697
1698        let license_dir = temp.path().join("path").join("to");
1699        std::fs::create_dir_all(&license_dir).unwrap();
1700        std::fs::write(license_dir.join("LICENSE"), "license").unwrap();
1701        std::fs::write(temp.path().join("README.md"), "readme").unwrap();
1702        std::fs::write(temp.path().join("asdf.wasm"), b"\0asm...").unwrap();
1703        std::fs::write(temp.path().join("asdf.wai"), "exports").unwrap();
1704        std::fs::write(temp.path().join("browser.wai"), "imports").unwrap();
1705        std::fs::write(temp.path().join("unwanted_file.txt"), "unwanted_file").unwrap();
1706
1707        let package = Package::from_manifest(manifest).unwrap();
1708
1709        let contents: Vec<_> = package
1710            .get_volume("metadata")
1711            .unwrap()
1712            .read_dir(&PathSegments::ROOT)
1713            .unwrap()
1714            .into_iter()
1715            .map(|(path, _, _)| path)
1716            .collect();
1717
1718        assert_eq!(
1719            contents,
1720            vec![
1721                PathSegment::parse("README.md").unwrap(),
1722                PathSegment::parse("asdf.wai").unwrap(),
1723                PathSegment::parse("browser.wai").unwrap(),
1724                PathSegment::parse("path").unwrap(),
1725            ]
1726        );
1727    }
1728
1729    #[test]
1730    fn create_from_in_memory() -> anyhow::Result<()> {
1731        let wasmer_toml = r#"
1732            [dependencies]
1733            "wasmer/python" = "3.12.9+build.9"
1734            
1735            
1736            [[command]]
1737            module = "wasmer/python:python"
1738            name = "hello"
1739            runner = "wasi"
1740            
1741            [command.annotations.wasi]
1742            main-args = [ "-c", "import os; print([f for f in os.walk('/public')]); " ]
1743            
1744            [fs]
1745            "/public" = "public" 
1746        "#;
1747
1748        let manifest = toml::from_str(wasmer_toml)?;
1749
1750        let file_modified = SystemTime::now();
1751        let file_data = String::from("Hello, world!").as_bytes().to_vec();
1752
1753        let file = MemoryFile {
1754            modified: file_modified,
1755            data: file_data,
1756        };
1757
1758        let mut nodes = BTreeMap::new();
1759        nodes.insert(String::from("hello.txt"), MemoryNode::File(file));
1760
1761        let dir_modified = SystemTime::now();
1762        let dir = MemoryDir {
1763            modified: dir_modified,
1764            nodes,
1765        };
1766
1767        let volume = MemoryVolume { node: dir };
1768        let mut volumes = BTreeMap::new();
1769
1770        volumes.insert("public".to_string(), volume);
1771
1772        let atoms = BTreeMap::new();
1773        let package = super::Package::from_in_memory(
1774            manifest,
1775            volumes,
1776            atoms,
1777            MemoryVolume {
1778                node: MemoryDir {
1779                    modified: SystemTime::now(),
1780                    nodes: BTreeMap::new(),
1781                },
1782            },
1783            Strictness::Strict,
1784        )?;
1785
1786        _ = package.serialize()?;
1787
1788        Ok(())
1789    }
1790
1791    #[test]
1792    fn compare_fs_mem_manifest() -> anyhow::Result<()> {
1793        let wasmer_toml = r#"
1794            [package]
1795            name = "test"
1796            version = "0.0.0"
1797            description = "asdf"
1798        "#;
1799
1800        let temp = TempDir::new()?;
1801        let manifest_path = temp.path().join("wasmer.toml");
1802        std::fs::write(&manifest_path, wasmer_toml).unwrap();
1803
1804        let fs_package = super::Package::from_manifest(manifest_path)?;
1805
1806        let manifest = toml::from_str(wasmer_toml)?;
1807        let memory_package = super::Package::from_in_memory(
1808            manifest,
1809            Default::default(),
1810            Default::default(),
1811            MemoryVolume {
1812                node: MemoryDir {
1813                    modified: SystemTime::UNIX_EPOCH,
1814                    nodes: BTreeMap::new(),
1815                },
1816            },
1817            Strictness::Lossy,
1818        )?;
1819
1820        assert_eq!(memory_package.serialize()?, fs_package.serialize()?);
1821
1822        Ok(())
1823    }
1824
1825    #[test]
1826    fn compare_fs_mem_manifest_and_atoms() -> anyhow::Result<()> {
1827        let wasmer_toml = r#"
1828            [package]
1829            name = "test"
1830            version = "0.0.0"
1831            description = "asdf"
1832
1833            [[module]]
1834            name = "foo"
1835            source = "foo.wasm"
1836            abi = "wasi"
1837        "#;
1838
1839        let temp = TempDir::new()?;
1840        let manifest_path = temp.path().join("wasmer.toml");
1841        std::fs::write(&manifest_path, wasmer_toml).unwrap();
1842
1843        let atom_path = temp.path().join("foo.wasm");
1844        std::fs::write(&atom_path, b"").unwrap();
1845
1846        let fs_package = super::Package::from_manifest(manifest_path)?;
1847
1848        let manifest = toml::from_str(wasmer_toml)?;
1849        let mut atoms = BTreeMap::new();
1850        atoms.insert("foo".to_owned(), (None, OwnedBuffer::new()));
1851        let memory_package = super::Package::from_in_memory(
1852            manifest,
1853            Default::default(),
1854            atoms,
1855            MemoryVolume {
1856                node: MemoryDir {
1857                    modified: SystemTime::UNIX_EPOCH,
1858                    nodes: BTreeMap::new(),
1859                },
1860            },
1861            Strictness::Lossy,
1862        )?;
1863
1864        assert_eq!(memory_package.serialize()?, fs_package.serialize()?);
1865
1866        Ok(())
1867    }
1868
1869    #[test]
1870    fn compare_fs_mem_volume() -> anyhow::Result<()> {
1871        let wasmer_toml = r#"
1872            [package]
1873            name = "test"
1874            version = "0.0.0"
1875            description = "asdf"
1876
1877            [[module]]
1878            name = "foo"
1879            source = "foo.wasm"
1880            abi = "wasi"
1881
1882            [fs]
1883            "/bar" = "bar"
1884        "#;
1885
1886        let temp = TempDir::new()?;
1887        let manifest_path = temp.path().join("wasmer.toml");
1888        std::fs::write(&manifest_path, wasmer_toml).unwrap();
1889
1890        let atom_path = temp.path().join("foo.wasm");
1891        std::fs::write(&atom_path, b"").unwrap();
1892
1893        let bar = temp.path().join("bar");
1894        std::fs::create_dir(&bar).unwrap();
1895
1896        let baz = bar.join("baz");
1897        std::fs::write(&baz, b"abc")?;
1898
1899        let baz_metadata = std::fs::metadata(&baz)?;
1900
1901        let fs_package = super::Package::from_manifest(manifest_path)?;
1902
1903        let manifest = toml::from_str(wasmer_toml)?;
1904
1905        let mut atoms = BTreeMap::new();
1906        atoms.insert("foo".to_owned(), (None, OwnedBuffer::new()));
1907
1908        let mut volumes = BTreeMap::new();
1909        volumes.insert(
1910            "/bar".to_owned(),
1911            MemoryVolume {
1912                node: MemoryDir {
1913                    modified: SystemTime::UNIX_EPOCH,
1914                    nodes: {
1915                        let mut children = BTreeMap::new();
1916
1917                        children.insert(
1918                            "baz".to_owned(),
1919                            MemoryNode::File(MemoryFile {
1920                                modified: baz_metadata.modified()?,
1921                                data: b"abc".to_vec(),
1922                            }),
1923                        );
1924
1925                        children
1926                    },
1927                },
1928            },
1929        );
1930        let memory_package = super::Package::from_in_memory(
1931            manifest,
1932            volumes,
1933            atoms,
1934            MemoryVolume {
1935                node: MemoryDir {
1936                    modified: SystemTime::UNIX_EPOCH,
1937                    nodes: Default::default(),
1938                },
1939            },
1940            Strictness::Lossy,
1941        )?;
1942
1943        assert_eq!(memory_package.serialize()?, fs_package.serialize()?);
1944
1945        Ok(())
1946    }
1947
1948    #[test]
1949    fn compare_fs_mem_bindings() -> anyhow::Result<()> {
1950        let temp = TempDir::new().unwrap();
1951
1952        let wasmer_toml = r#"
1953            [package]
1954            name = "some/package"
1955            version = "0.0.0"
1956            description = ""
1957            license-file = "LICENSE"
1958            readme = "README.md"
1959
1960            [[module]]
1961            name = "asdf"
1962            source = "asdf.wasm"
1963            abi = "none"
1964            bindings = { wai-version = "0.2.0", exports = "asdf.wai", imports = ["browser.wai"] }
1965
1966            [fs]
1967            "/dir1" = "local-dir1/dir"
1968            "/dir2" = "local-dir2/dir"
1969        "#;
1970
1971        let manifest = temp.path().join("wasmer.toml");
1972        std::fs::write(&manifest, wasmer_toml).unwrap();
1973
1974        std::fs::write(temp.path().join("LICENSE"), "license").unwrap();
1975        std::fs::write(temp.path().join("README.md"), "readme").unwrap();
1976        std::fs::write(temp.path().join("asdf.wasm"), b"\0asm...").unwrap();
1977        std::fs::write(temp.path().join("asdf.wai"), "exports").unwrap();
1978        std::fs::write(temp.path().join("browser.wai"), "imports").unwrap();
1979
1980        // Now we want to set up the following filesystem tree:
1981        //
1982        // - local-dir1/
1983        //   - dir
1984        // - local-dir2/
1985        //   - dir
1986        let dir1 = temp.path().join("local-dir1").join("dir");
1987        std::fs::create_dir_all(&dir1).unwrap();
1988        let dir2 = temp.path().join("local-dir2").join("dir");
1989        std::fs::create_dir_all(&dir2).unwrap();
1990
1991        let fs_package = super::Package::from_manifest(manifest)?;
1992
1993        let manifest = toml::from_str(wasmer_toml)?;
1994
1995        let mut atoms = BTreeMap::new();
1996        atoms.insert(
1997            "asdf".to_owned(),
1998            (None, OwnedBuffer::from_static(b"\0asm...")),
1999        );
2000
2001        let mut volumes = BTreeMap::new();
2002        volumes.insert(
2003            "/local-dir1/dir".to_owned(),
2004            MemoryVolume {
2005                node: MemoryDir {
2006                    modified: SystemTime::UNIX_EPOCH,
2007                    nodes: Default::default(),
2008                },
2009            },
2010        );
2011        volumes.insert(
2012            "/local-dir2/dir".to_owned(),
2013            MemoryVolume {
2014                node: MemoryDir {
2015                    modified: SystemTime::UNIX_EPOCH,
2016                    nodes: Default::default(),
2017                },
2018            },
2019        );
2020
2021        let memory_package = super::Package::from_in_memory(
2022            manifest,
2023            volumes,
2024            atoms,
2025            MemoryVolume {
2026                node: MemoryDir {
2027                    modified: SystemTime::UNIX_EPOCH,
2028                    nodes: {
2029                        let mut children = BTreeMap::new();
2030
2031                        children.insert(
2032                            "README.md".to_owned(),
2033                            MemoryNode::File(MemoryFile {
2034                                modified: temp.path().join("README.md").metadata()?.modified()?,
2035                                data: b"readme".to_vec(),
2036                            }),
2037                        );
2038
2039                        children.insert(
2040                            "LICENSE".to_owned(),
2041                            MemoryNode::File(MemoryFile {
2042                                modified: temp.path().join("LICENSE").metadata()?.modified()?,
2043                                data: b"license".to_vec(),
2044                            }),
2045                        );
2046
2047                        children.insert(
2048                            "asdf.wai".to_owned(),
2049                            MemoryNode::File(MemoryFile {
2050                                modified: temp.path().join("asdf.wai").metadata()?.modified()?,
2051                                data: b"exports".to_vec(),
2052                            }),
2053                        );
2054
2055                        children.insert(
2056                            "browser.wai".to_owned(),
2057                            MemoryNode::File(MemoryFile {
2058                                modified: temp.path().join("browser.wai").metadata()?.modified()?,
2059                                data: b"imports".to_vec(),
2060                            }),
2061                        );
2062
2063                        children
2064                    },
2065                },
2066            },
2067            Strictness::Lossy,
2068        )?;
2069
2070        let memory_package = memory_package.serialize()?;
2071        let fs_package = fs_package.serialize()?;
2072
2073        assert_eq!(memory_package, fs_package);
2074
2075        Ok(())
2076    }
2077}