wasmer_package/package/
package.rs

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