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