wasmer_package/package/
package.rs

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