Skip to main content

wasmer_package/package/
package.rs

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