wasmer_wasix/runtime/package_loader/
load_package_tree.rs

1use std::{
2    collections::{BTreeMap, HashMap, HashSet},
3    fmt::Debug,
4    path::{Path, PathBuf},
5    sync::Arc,
6};
7
8use anyhow::{Context, Error};
9use futures::{StreamExt, TryStreamExt, future::BoxFuture};
10use once_cell::sync::OnceCell;
11use petgraph::visit::EdgeRef;
12use virtual_fs::{FileSystem, UnionFileSystem, WebcVolumeFileSystem};
13use wasmer_config::package::{PackageId, SuggestedCompilerOptimizations};
14use wasmer_package::utils::wasm_annotations_to_features;
15use webc::metadata::annotations::Atom as AtomAnnotation;
16use webc::{Container, Volume};
17
18use crate::{
19    bin_factory::{BinaryPackage, BinaryPackageCommand},
20    runtime::{
21        package_loader::PackageLoader,
22        resolver::{
23            DependencyGraph, ItemLocation, PackageSummary, Resolution, ResolvedFileSystemMapping,
24            ResolvedPackage,
25        },
26    },
27};
28
29use super::to_module_hash;
30
31/// Convert WebAssembly feature annotations to a Features object
32fn wasm_annotation_to_features(
33    wasm_annotation: &webc::metadata::annotations::Wasm,
34) -> Option<wasmer_types::Features> {
35    Some(wasm_annotations_to_features(&wasm_annotation.features))
36}
37
38/// Extract WebAssembly features from atom metadata if available
39fn extract_features_from_atom_metadata(
40    atom_metadata: &webc::metadata::Atom,
41) -> Option<wasmer_types::Features> {
42    if let Ok(Some(wasm_annotation)) = atom_metadata
43        .annotation::<webc::metadata::annotations::Wasm>(webc::metadata::annotations::Wasm::KEY)
44    {
45        wasm_annotation_to_features(&wasm_annotation)
46    } else {
47        None
48    }
49}
50
51/// The maximum number of packages that will be loaded in parallel.
52const MAX_PARALLEL_DOWNLOADS: usize = 32;
53
54/// Given a fully resolved package, load it into memory for execution.
55#[tracing::instrument(level = "debug", skip_all)]
56pub async fn load_package_tree(
57    root: &Container,
58    loader: &dyn PackageLoader,
59    resolution: &Resolution,
60    root_is_local_dir: bool,
61) -> Result<BinaryPackage, Error> {
62    let mut containers = fetch_dependencies(loader, &resolution.package, &resolution.graph).await?;
63    containers.insert(resolution.package.root_package.clone(), root.clone());
64    let package_ids = containers.keys().cloned().collect();
65    let fs_opt = filesystem(&containers, &resolution.package, root_is_local_dir)?;
66
67    let root = &resolution.package.root_package;
68    let commands: Vec<BinaryPackageCommand> =
69        commands(&resolution.package.commands, &containers, resolution)?;
70
71    let file_system_memory_footprint = if let Some(fs) = &fs_opt {
72        count_file_system(fs, Path::new("/"))
73    } else {
74        0
75    };
76
77    let loaded = BinaryPackage {
78        id: root.clone(),
79        package_ids,
80        when_cached: crate::syscalls::platform_clock_time_get(
81            wasmer_wasix_types::wasi::Snapshot0Clockid::Monotonic,
82            1_000_000,
83        )
84        .ok()
85        .map(|ts| ts as u128),
86        hash: OnceCell::new(),
87        entrypoint_cmd: resolution.package.entrypoint.clone(),
88        webc_fs: fs_opt.map(Arc::new),
89        commands,
90        uses: Vec::new(),
91        file_system_memory_footprint,
92
93        additional_host_mapped_directories: vec![],
94    };
95
96    Ok(loaded)
97}
98
99fn commands(
100    commands: &BTreeMap<String, ItemLocation>,
101    containers: &HashMap<PackageId, Container>,
102    resolution: &Resolution,
103) -> Result<Vec<BinaryPackageCommand>, Error> {
104    let mut pkg_commands = Vec::new();
105
106    for (
107        name,
108        ItemLocation {
109            name: original_name,
110            package,
111        },
112    ) in commands
113    {
114        let webc = &containers[package];
115        let manifest = webc.manifest();
116        let command_metadata = &manifest.commands[original_name];
117
118        if let Some(cmd) =
119            load_binary_command(package, name, command_metadata, containers, resolution)?
120        {
121            pkg_commands.push(cmd);
122        }
123    }
124
125    Ok(pkg_commands)
126}
127
128/// Given a [`webc::metadata::Command`], figure out which atom it uses and load
129/// that atom into a [`BinaryPackageCommand`].
130#[tracing::instrument(skip_all, fields(%package_id, %command_name))]
131fn load_binary_command(
132    package_id: &PackageId,
133    command_name: &str,
134    cmd: &webc::metadata::Command,
135    containers: &HashMap<PackageId, Container>,
136    resolution: &Resolution,
137) -> Result<Option<BinaryPackageCommand>, anyhow::Error> {
138    let AtomAnnotation {
139        name: atom_name,
140        dependency,
141        ..
142    } = match atom_name_for_command(command_name, cmd)? {
143        Some(name) => name,
144        None => {
145            tracing::warn!(
146                cmd.name=command_name,
147                cmd.runner=%cmd.runner,
148                "Skipping unsupported command",
149            );
150            return Ok(None);
151        }
152    };
153
154    let package = &containers[package_id];
155
156    let (webc, resolved_package_id) = match dependency {
157        Some(dep) => {
158            let ix = resolution
159                .graph
160                .packages()
161                .get(package_id)
162                .copied()
163                .unwrap();
164            let graph = resolution.graph.graph();
165            let edge_reference = graph
166                .edges_directed(ix, petgraph::Direction::Outgoing)
167                .find(|edge| edge.weight().alias == dep)
168                .with_context(|| format!("Unable to find the \"{dep}\" dependency for the \"{command_name}\" command in \"{package_id}\""))?;
169
170            let other_package = graph.node_weight(edge_reference.target()).unwrap();
171            let id = &other_package.id;
172
173            tracing::debug!(
174                dependency=%dep,
175                resolved_package_id=%id,
176                "command atom resolution: resolved dependency",
177            );
178            (&containers[id], id)
179        }
180        None => (package, package_id),
181    };
182
183    let atom = webc.get_atom(&atom_name);
184
185    if atom.is_none() && cmd.annotations.is_empty() {
186        tracing::info!("applying legacy atom hack");
187        return legacy_atom_hack(webc, command_name, cmd);
188    }
189
190    let hash = to_module_hash(webc.manifest().atom_signature(&atom_name)?);
191
192    let atom = atom.with_context(|| {
193
194        let available_atoms = webc.atoms().keys().map(|x| x.as_str()).collect::<Vec<_>>().join(",");
195
196        tracing::warn!(
197            %atom_name,
198            %resolved_package_id,
199            %available_atoms,
200            "invalid command: could not find atom in package",
201        );
202
203        format!(
204            "The '{command_name}' command uses the '{atom_name}' atom, but it isn't present in the package: {resolved_package_id})"
205        )
206    })?;
207
208    // Get WebAssembly features from manifest atom annotations
209    let features = if let Some(atom_metadata) = webc.manifest().atoms.get(&atom_name) {
210        extract_features_from_atom_metadata(atom_metadata)
211    } else {
212        None
213    };
214
215    let suggested_compiler_optimizations =
216        if let Some(atom_metadata) = webc.manifest().atoms.get(&atom_name) {
217            extract_suggested_compiler_opts_from_atom_metadata(atom_metadata)
218        } else {
219            wasmer_config::package::SuggestedCompilerOptimizations::default()
220        };
221
222    let cmd = BinaryPackageCommand::new(
223        command_name.to_string(),
224        cmd.clone(),
225        atom,
226        hash,
227        features,
228        suggested_compiler_optimizations,
229    );
230
231    Ok(Some(cmd))
232}
233
234fn extract_suggested_compiler_opts_from_atom_metadata(
235    atom_metadata: &webc::metadata::Atom,
236) -> wasmer_config::package::SuggestedCompilerOptimizations {
237    let mut ret = SuggestedCompilerOptimizations::default();
238
239    if let Some(sco) = atom_metadata
240        .annotations
241        .get(SuggestedCompilerOptimizations::KEY)
242        && let Some((_, v)) = sco.as_map().and_then(|v| {
243            v.iter().find(|(k, _)| {
244                k.as_text()
245                    .is_some_and(|v| v == SuggestedCompilerOptimizations::PASS_PARAMS_KEY)
246            })
247        })
248    {
249        ret.pass_params = v.as_bool()
250    }
251
252    ret
253}
254
255fn atom_name_for_command(
256    command_name: &str,
257    cmd: &webc::metadata::Command,
258) -> Result<Option<AtomAnnotation>, anyhow::Error> {
259    use webc::metadata::annotations::{WASI_RUNNER_URI, WCGI_RUNNER_URI};
260
261    if let Some(atom) = cmd
262        .atom()
263        .context("Unable to deserialize atom annotations")?
264    {
265        return Ok(Some(atom));
266    }
267
268    if [WASI_RUNNER_URI, WCGI_RUNNER_URI]
269        .iter()
270        .any(|uri| cmd.runner.starts_with(uri))
271    {
272        // Note: We use the command name as the atom name as a special case
273        // for known runner types because sometimes people will construct
274        // a manifest by hand instead of using wapm2pirita.
275        tracing::debug!(
276            command = command_name,
277            "No annotations specifying the atom name found. Falling back to the command name"
278        );
279        return Ok(Some(AtomAnnotation::new(command_name, None)));
280    }
281
282    Ok(None)
283}
284
285/// HACK: Some older packages like `sharrattj/bash` and `sharrattj/coreutils`
286/// contain commands with no annotations. When this happens, you can just assume
287/// it wants to use the first atom in the WEBC file.
288///
289/// That works because most of these packages only have a single atom (e.g. in
290/// `sharrattj/coreutils` there are commands for `ls`, `pwd`, and so on, but
291/// under the hood they all use the `coreutils` atom).
292///
293/// See <https://github.com/wasmerio/wasmer/commit/258903140680716da1431d92bced67d486865aeb>
294/// for more.
295fn legacy_atom_hack(
296    webc: &Container,
297    command_name: &str,
298    metadata: &webc::metadata::Command,
299) -> Result<Option<BinaryPackageCommand>, anyhow::Error> {
300    let (name, atom) = webc
301        .atoms()
302        .into_iter()
303        .next()
304        .ok_or_else(|| anyhow::Error::msg("container does not have any atom"))?;
305
306    tracing::debug!(
307        command_name,
308        atom.name = name.as_str(),
309        atom.len = atom.len(),
310        "(hack) The command metadata is malformed. Falling back to the first atom in the WEBC file",
311    );
312
313    let hash = to_module_hash(webc.manifest().atom_signature(&name)?);
314
315    // Get WebAssembly features from manifest atom annotations
316    let features = if let Some(atom_metadata) = webc.manifest().atoms.get(&name) {
317        extract_features_from_atom_metadata(atom_metadata)
318    } else {
319        None
320    };
321
322    // Get WebAssembly features from manifest atom annotations
323    let suggested_opts_from_manifest = if let Some(atom_metadata) = webc.manifest().atoms.get(&name)
324    {
325        extract_suggested_compiler_opts_from_atom_metadata(atom_metadata)
326    } else {
327        SuggestedCompilerOptimizations::default()
328    };
329
330    Ok(Some(BinaryPackageCommand::new(
331        command_name.to_string(),
332        metadata.clone(),
333        atom,
334        hash,
335        features,
336        suggested_opts_from_manifest,
337    )))
338}
339
340async fn fetch_dependencies(
341    loader: &dyn PackageLoader,
342    pkg: &ResolvedPackage,
343    graph: &DependencyGraph,
344) -> Result<HashMap<PackageId, Container>, Error> {
345    let mut packages = HashSet::new();
346
347    for loc in pkg.commands.values() {
348        packages.insert(loc.package.clone());
349    }
350
351    for mapping in &pkg.filesystem {
352        packages.insert(mapping.package.clone());
353    }
354
355    // We don't need to download the root package
356    packages.remove(&pkg.root_package);
357
358    let packages = packages.into_iter().filter_map(|id| {
359        let crate::runtime::resolver::Node { pkg, dist, .. } = &graph[&id];
360        let summary = PackageSummary {
361            pkg: pkg.clone(),
362            dist: dist.clone()?,
363        };
364        Some((id, summary))
365    });
366    let packages: HashMap<PackageId, Container> = futures::stream::iter(packages)
367        .map(|(id, s)| async move {
368            match loader.load(&s).await {
369                Ok(webc) => Ok((id, webc)),
370                Err(e) => Err(e),
371            }
372        })
373        .buffer_unordered(MAX_PARALLEL_DOWNLOADS)
374        .try_collect()
375        .await?;
376
377    Ok(packages)
378}
379
380/// How many bytes worth of files does a directory contain?
381fn count_file_system(fs: &dyn FileSystem, path: &Path) -> u64 {
382    let mut total = 0;
383
384    let dir = match fs.read_dir(path) {
385        Ok(d) => d,
386        Err(_err) => {
387            return 0;
388        }
389    };
390
391    for entry in dir.flatten() {
392        if let Ok(meta) = entry.metadata() {
393            total += meta.len();
394            if meta.is_dir() {
395                total += count_file_system(fs, entry.path.as_path());
396            }
397        }
398    }
399
400    total
401}
402
403/// Given a set of [`ResolvedFileSystemMapping`]s and the [`Container`] for each
404/// package in a dependency tree, construct the resulting filesystem.
405///
406/// Returns `Ok(None)` if no filesystem mappings were specified.
407fn filesystem(
408    packages: &HashMap<PackageId, Container>,
409    pkg: &ResolvedPackage,
410    root_is_local_dir: bool,
411) -> Result<Option<UnionFileSystem>, Error> {
412    if pkg.filesystem.is_empty() {
413        return Ok(None);
414    }
415
416    let mut found_v2 = None;
417    let mut found_v3 = None;
418
419    for ResolvedFileSystemMapping { package, .. } in &pkg.filesystem {
420        let container = packages.get(package).with_context(|| {
421            format!(
422                "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
423                pkg.root_package, package,
424            )
425        })?;
426
427        match container.version() {
428            webc::Version::V1 => {
429                anyhow::bail!(
430                    "the package '{package}' is a webc v1 package, but webc v1 support was removed"
431                );
432            }
433            webc::Version::V2 => {
434                if found_v2.is_none() {
435                    found_v2 = Some(package.clone());
436                }
437            }
438            webc::Version::V3 => {
439                if found_v3.is_none() {
440                    found_v3 = Some(package.clone());
441                }
442            }
443            other => {
444                anyhow::bail!("the package '{package}' has an unknown webc version: {other}");
445            }
446        }
447    }
448
449    match (found_v2, found_v3) {
450        (None, Some(_)) => filesystem_v3(packages, pkg, root_is_local_dir).map(Some),
451        (Some(_), None) => filesystem_v2(packages, pkg, root_is_local_dir).map(Some),
452        (Some(v2), Some(v3)) => {
453            anyhow::bail!(
454                "Mix of webc v2 and v3 in the same dependency tree is not supported; v2: {v2}, v3: {v3}"
455            )
456        }
457        (None, None) => anyhow::bail!("Internal error: no packages found in tree"),
458    }
459}
460
461/// Build the filesystem for webc v3 packages.
462fn filesystem_v3(
463    packages: &HashMap<PackageId, Container>,
464    pkg: &ResolvedPackage,
465    root_is_local_dir: bool,
466) -> Result<UnionFileSystem, Error> {
467    let mut volumes: HashMap<&PackageId, BTreeMap<String, Volume>> = HashMap::new();
468
469    let union_fs = UnionFileSystem::new();
470
471    for ResolvedFileSystemMapping {
472        mount_path,
473        volume_name,
474        package,
475        ..
476    } in &pkg.filesystem
477    {
478        if *package == pkg.root_package && root_is_local_dir {
479            continue;
480        }
481
482        if mount_path.as_path() == Path::new("/") {
483            tracing::warn!(
484                "The \"{package}\" package wants to mount a volume at \"/\", which breaks WASIX modules' filesystems",
485            );
486        }
487
488        // Note: We want to reuse existing Volume instances if we can. That way
489        // we can keep the memory usage down. A webc::compat::Volume is
490        // reference-counted, anyway.
491        // looks like we need to insert it
492        let container = packages.get(package).with_context(|| {
493            format!(
494                "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
495                pkg.root_package, package,
496            )
497        })?;
498        let container_volumes = match volumes.entry(package) {
499            std::collections::hash_map::Entry::Occupied(entry) => &*entry.into_mut(),
500            std::collections::hash_map::Entry::Vacant(entry) => &*entry.insert(container.volumes()),
501        };
502
503        let volume = container_volumes.get(volume_name).with_context(|| {
504            format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume")
505        })?;
506
507        let webc_vol = WebcVolumeFileSystem::new(volume.clone());
508        union_fs.mount(volume_name.clone(), mount_path, Box::new(webc_vol))?;
509    }
510
511    Ok(union_fs)
512}
513
514/// Build the filesystem for webc v2 packages.
515///
516// # Note to future readers
517//
518// Sooo... this code is a bit convoluted because we're constrained by the
519// filesystem implementations we've got available.
520//
521// Ideally, we would create a WebcVolumeFileSystem for each volume we're
522// using, then we'd have a single "union" filesystem which lets you mount
523// filesystem objects under various paths and can deal with conflicts.
524//
525// The OverlayFileSystem lets us make files from multiple filesystem
526// implementations available at the same time, however all of the
527// filesystems will be mounted at "/", when the user wants to mount volumes
528// at arbitrary locations.
529//
530// The TmpFileSystem *does* allow mounting at non-root paths, however it can't
531// handle nested paths (e.g. mounting to "/lib" and "/lib/python3.10" - see
532// <https://github.com/wasmerio/wasmer/issues/3678> for more) and you aren't
533// allowed to mount to "/" because it's a special directory that already
534// exists.
535//
536// As a result, we'll duct-tape things together and hope for the best 🤞
537fn filesystem_v2(
538    packages: &HashMap<PackageId, Container>,
539    pkg: &ResolvedPackage,
540    root_is_local_dir: bool,
541) -> Result<UnionFileSystem, Error> {
542    let mut volumes: HashMap<&PackageId, BTreeMap<String, Volume>> = HashMap::new();
543
544    let union_fs = UnionFileSystem::new();
545
546    for ResolvedFileSystemMapping {
547        mount_path,
548        volume_name,
549        package,
550        original_path,
551    } in &pkg.filesystem
552    {
553        if *package == pkg.root_package && root_is_local_dir {
554            continue;
555        }
556
557        if mount_path.as_path() == Path::new("/") {
558            tracing::warn!(
559                "The \"{package}\" package wants to mount a volume at \"/\", which breaks WASIX modules' filesystems",
560            );
561        }
562
563        // Note: We want to reuse existing Volume instances if we can. That way
564        // we can keep the memory usage down. A webc::compat::Volume is
565        // reference-counted, anyway.
566        let container_volumes = match volumes.entry(package) {
567            std::collections::hash_map::Entry::Occupied(entry) => &*entry.into_mut(),
568            std::collections::hash_map::Entry::Vacant(entry) => {
569                // looks like we need to insert it
570                let container = packages.get(package)
571                    .with_context(|| format!(
572                        "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
573                        pkg.root_package,
574                        package,
575                    ))?;
576                &*entry.insert(container.volumes())
577            }
578        };
579
580        let volume = container_volumes.get(volume_name).with_context(|| {
581            format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume")
582        })?;
583
584        // UnionFileSystem strips the mount point before forwarding paths to the
585        // mounted filesystem. That means paths are already relative to the
586        // mount root and shouldn't be stripped by mount_path.
587        let fs = if let Some(original) = original_path {
588            let original = PathBuf::from(original);
589
590            MappedPathFileSystem::new(
591                WebcVolumeFileSystem::new(volume.clone()),
592                Box::new(move |path: &Path| Ok(original.join(strip_root_prefix(path))))
593                    as DynPathMapper,
594            )
595        } else {
596            MappedPathFileSystem::new(
597                WebcVolumeFileSystem::new(volume.clone()),
598                Box::new(move |path: &Path| Ok(strip_root_prefix(path))) as DynPathMapper,
599            )
600        };
601
602        union_fs.mount(volume_name.clone(), mount_path, Box::new(fs))?;
603    }
604
605    Ok(union_fs)
606}
607
608fn strip_root_prefix(path: &Path) -> PathBuf {
609    path.strip_prefix("/").unwrap_or(path).to_owned()
610}
611
612type DynPathMapper = Box<dyn Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync>;
613
614struct MappedPathFileSystem<F, M> {
615    inner: F,
616    map: M,
617}
618
619impl<F, M> MappedPathFileSystem<F, M>
620where
621    M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
622{
623    fn new(inner: F, map: M) -> Self {
624        MappedPathFileSystem { inner, map }
625    }
626
627    fn path(&self, path: &Path) -> Result<PathBuf, virtual_fs::FsError> {
628        let path = (self.map)(path)?;
629
630        // Don't forget to make the path absolute again.
631        Ok(Path::new("/").join(path))
632    }
633}
634
635impl<M, F> FileSystem for MappedPathFileSystem<F, M>
636where
637    F: FileSystem,
638    M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
639{
640    fn readlink(&self, path: &Path) -> virtual_fs::Result<PathBuf> {
641        let path = self.path(path)?;
642        self.inner.readlink(&path)
643    }
644
645    fn read_dir(&self, path: &Path) -> virtual_fs::Result<virtual_fs::ReadDir> {
646        let path = self.path(path)?;
647        self.inner.read_dir(&path)
648    }
649
650    fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> {
651        let path = self.path(path)?;
652        self.inner.create_dir(&path)
653    }
654
655    fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> {
656        let path = self.path(path)?;
657        self.inner.remove_dir(&path)
658    }
659
660    fn rename<'a>(&'a self, from: &Path, to: &Path) -> BoxFuture<'a, virtual_fs::Result<()>> {
661        let from = from.to_owned();
662        let to = to.to_owned();
663        Box::pin(async move {
664            let from = self.path(&from)?;
665            let to = self.path(&to)?;
666            self.inner.rename(&from, &to).await
667        })
668    }
669
670    fn metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
671        let path = self.path(path)?;
672        self.inner.metadata(&path)
673    }
674
675    fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
676        let path = self.path(path)?;
677        self.inner.symlink_metadata(&path)
678    }
679
680    fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> {
681        let path = self.path(path)?;
682        self.inner.remove_file(&path)
683    }
684
685    fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> {
686        virtual_fs::OpenOptions::new(self)
687    }
688
689    fn mount(
690        &self,
691        name: String,
692        path: &Path,
693        fs: Box<dyn FileSystem + Send + Sync>,
694    ) -> virtual_fs::Result<()> {
695        let path = self.path(path)?;
696        self.inner.mount(name, path.as_path(), fs)
697    }
698}
699
700impl<F, M> virtual_fs::FileOpener for MappedPathFileSystem<F, M>
701where
702    F: FileSystem,
703    M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
704{
705    fn open(
706        &self,
707        path: &Path,
708        conf: &virtual_fs::OpenOptionsConfig,
709    ) -> virtual_fs::Result<Box<dyn virtual_fs::VirtualFile + Send + Sync + 'static>> {
710        let path = self.path(path)?;
711        self.inner
712            .new_open_options()
713            .options(conf.clone())
714            .open(path)
715    }
716}
717
718impl<F, M> Debug for MappedPathFileSystem<F, M>
719where
720    F: Debug,
721{
722    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
723        f.debug_struct("MappedPathFileSystem")
724            .field("inner", &self.inner)
725            .field("map", &std::any::type_name::<M>())
726            .finish()
727    }
728}
729
730#[cfg(test)]
731mod tests {
732    use std::{
733        collections::{BTreeMap, HashMap},
734        path::{Path, PathBuf},
735    };
736
737    use ciborium::value::Value;
738    use virtual_fs::FileSystem;
739    use wasmer_config::package::PackageId;
740    use webc::{
741        Container,
742        indexmap::IndexMap,
743        metadata::{
744            Manifest,
745            annotations::{FileSystemMapping, FileSystemMappings},
746        },
747        v2::{
748            SignatureAlgorithm,
749            read::OwnedReader,
750            write::{DirEntry, Directory, FileEntry, Writer},
751        },
752    };
753
754    use super::{ResolvedFileSystemMapping, ResolvedPackage, filesystem_v2};
755
756    #[test]
757    fn v2_filesystem_mapping_resolves_mount_paths() {
758        // Regression test: v2 fs mounts are already relative to the mount root,
759        // so stripping the mount path again breaks lookups like /public.
760        let mut manifest = Manifest::default();
761        let fs = FileSystemMappings(vec![FileSystemMapping {
762            from: None,
763            volume_name: "atom".to_string(),
764            host_path: Some("/public".to_string()),
765            mount_path: "/public".to_string(),
766        }]);
767        let mut package = IndexMap::new();
768        package.insert(
769            FileSystemMappings::KEY.to_string(),
770            Value::serialized(&fs).unwrap(),
771        );
772        manifest.package = package;
773
774        let mut public_children = BTreeMap::new();
775        public_children.insert(
776            "index.html".parse().unwrap(),
777            DirEntry::File(FileEntry::from(b"ok".as_slice())),
778        );
779        let public_dir = Directory {
780            children: public_children,
781        };
782        let mut root_children = BTreeMap::new();
783        root_children.insert("public".parse().unwrap(), DirEntry::Dir(public_dir));
784        let atom_dir = Directory {
785            children: root_children,
786        };
787
788        let writer = Writer::default().write_manifest(&manifest).unwrap();
789        let writer = writer.write_atoms(BTreeMap::new()).unwrap();
790        let writer = writer.with_volume("atom", atom_dir).unwrap();
791        let bytes = writer.finish(SignatureAlgorithm::None).unwrap();
792
793        let reader = OwnedReader::parse(bytes).unwrap();
794        let container = Container::from(reader);
795
796        let pkg_id = PackageId::new_named("ns/pkg", "0.1.0".parse().unwrap());
797        let mut packages = HashMap::new();
798        packages.insert(pkg_id.clone(), container);
799
800        let pkg = ResolvedPackage {
801            root_package: pkg_id.clone(),
802            commands: BTreeMap::new(),
803            entrypoint: None,
804            filesystem: vec![ResolvedFileSystemMapping {
805                mount_path: PathBuf::from("/public"),
806                volume_name: "atom".to_string(),
807                original_path: Some("/public".to_string()),
808                package: pkg_id,
809            }],
810        };
811
812        let union_fs = filesystem_v2(&packages, &pkg, false).unwrap();
813        assert!(union_fs.metadata(Path::new("/public")).unwrap().is_dir());
814        assert!(
815            union_fs
816                .metadata(Path::new("/public/index.html"))
817                .unwrap()
818                .is_file()
819        );
820    }
821}