Skip to main content

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