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, OverlayFileSystem, 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 = 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 = count_file_system(&fs, Path::new("/"));
72
73    let loaded = BinaryPackage {
74        id: root.clone(),
75        package_ids,
76        when_cached: crate::syscalls::platform_clock_time_get(
77            wasmer_wasix_types::wasi::Snapshot0Clockid::Monotonic,
78            1_000_000,
79        )
80        .ok()
81        .map(|ts| ts as u128),
82        hash: OnceCell::new(),
83        entrypoint_cmd: resolution.package.entrypoint.clone(),
84        webc_fs: Arc::new(fs),
85        commands,
86        uses: Vec::new(),
87        file_system_memory_footprint,
88
89        additional_host_mapped_directories: vec![],
90    };
91
92    Ok(loaded)
93}
94
95fn commands(
96    commands: &BTreeMap<String, ItemLocation>,
97    containers: &HashMap<PackageId, Container>,
98    resolution: &Resolution,
99) -> Result<Vec<BinaryPackageCommand>, Error> {
100    let mut pkg_commands = Vec::new();
101
102    for (
103        name,
104        ItemLocation {
105            name: original_name,
106            package,
107        },
108    ) in commands
109    {
110        let webc = &containers[package];
111        let manifest = webc.manifest();
112        let command_metadata = &manifest.commands[original_name];
113
114        if let Some(cmd) =
115            load_binary_command(package, name, command_metadata, containers, resolution)?
116        {
117            pkg_commands.push(cmd);
118        }
119    }
120
121    Ok(pkg_commands)
122}
123
124/// Given a [`webc::metadata::Command`], figure out which atom it uses and load
125/// that atom into a [`BinaryPackageCommand`].
126#[tracing::instrument(skip_all, fields(%package_id, %command_name))]
127fn load_binary_command(
128    package_id: &PackageId,
129    command_name: &str,
130    cmd: &webc::metadata::Command,
131    containers: &HashMap<PackageId, Container>,
132    resolution: &Resolution,
133) -> Result<Option<BinaryPackageCommand>, anyhow::Error> {
134    let AtomAnnotation {
135        name: atom_name,
136        dependency,
137        ..
138    } = match atom_name_for_command(command_name, cmd)? {
139        Some(name) => name,
140        None => {
141            tracing::warn!(
142                cmd.name=command_name,
143                cmd.runner=%cmd.runner,
144                "Skipping unsupported command",
145            );
146            return Ok(None);
147        }
148    };
149
150    let package = &containers[package_id];
151
152    let (webc, resolved_package_id) = match dependency {
153        Some(dep) => {
154            let ix = resolution
155                .graph
156                .packages()
157                .get(package_id)
158                .copied()
159                .unwrap();
160            let graph = resolution.graph.graph();
161            let edge_reference = graph
162                .edges_directed(ix, petgraph::Direction::Outgoing)
163                .find(|edge| edge.weight().alias == dep)
164                .with_context(|| format!("Unable to find the \"{dep}\" dependency for the \"{command_name}\" command in \"{package_id}\""))?;
165
166            let other_package = graph.node_weight(edge_reference.target()).unwrap();
167            let id = &other_package.id;
168
169            tracing::debug!(
170                dependency=%dep,
171                resolved_package_id=%id,
172                "command atom resolution: resolved dependency",
173            );
174            (&containers[id], id)
175        }
176        None => (package, package_id),
177    };
178
179    let atom = webc.get_atom(&atom_name);
180
181    if atom.is_none() && cmd.annotations.is_empty() {
182        tracing::info!("applying legacy atom hack");
183        return legacy_atom_hack(webc, command_name, cmd);
184    }
185
186    let hash = to_module_hash(webc.manifest().atom_signature(&atom_name)?);
187
188    let atom = atom.with_context(|| {
189
190        let available_atoms = webc.atoms().keys().map(|x| x.as_str()).collect::<Vec<_>>().join(",");
191
192        tracing::warn!(
193            %atom_name,
194            %resolved_package_id,
195            %available_atoms,
196            "invalid command: could not find atom in package",
197        );
198
199        format!(
200            "The '{command_name}' command uses the '{atom_name}' atom, but it isn't present in the package: {resolved_package_id})"
201        )
202    })?;
203
204    // Get WebAssembly features from manifest atom annotations
205    let features = if let Some(atom_metadata) = webc.manifest().atoms.get(&atom_name) {
206        extract_features_from_atom_metadata(atom_metadata)
207    } else {
208        None
209    };
210
211    let suggested_compiler_optimizations =
212        if let Some(atom_metadata) = webc.manifest().atoms.get(&atom_name) {
213            extract_suggested_compiler_opts_from_atom_metadata(atom_metadata)
214        } else {
215            wasmer_config::package::SuggestedCompilerOptimizations::default()
216        };
217
218    let cmd = BinaryPackageCommand::new(
219        command_name.to_string(),
220        cmd.clone(),
221        atom,
222        hash,
223        features,
224        suggested_compiler_optimizations,
225    );
226
227    Ok(Some(cmd))
228}
229
230fn extract_suggested_compiler_opts_from_atom_metadata(
231    atom_metadata: &webc::metadata::Atom,
232) -> wasmer_config::package::SuggestedCompilerOptimizations {
233    let mut ret = SuggestedCompilerOptimizations::default();
234
235    if let Some(sco) = atom_metadata
236        .annotations
237        .get(SuggestedCompilerOptimizations::KEY)
238    {
239        if let Some((_, v)) = sco.as_map().and_then(|v| {
240            v.iter().find(|(k, _)| {
241                k.as_text()
242                    .is_some_and(|v| v == SuggestedCompilerOptimizations::PASS_PARAMS_KEY)
243            })
244        }) {
245            ret.pass_params = v.as_bool()
246        }
247    }
248
249    ret
250}
251
252fn atom_name_for_command(
253    command_name: &str,
254    cmd: &webc::metadata::Command,
255) -> Result<Option<AtomAnnotation>, anyhow::Error> {
256    use webc::metadata::annotations::{WASI_RUNNER_URI, WCGI_RUNNER_URI};
257
258    if let Some(atom) = cmd
259        .atom()
260        .context("Unable to deserialize atom annotations")?
261    {
262        return Ok(Some(atom));
263    }
264
265    if [WASI_RUNNER_URI, WCGI_RUNNER_URI]
266        .iter()
267        .any(|uri| cmd.runner.starts_with(uri))
268    {
269        // Note: We use the command name as the atom name as a special case
270        // for known runner types because sometimes people will construct
271        // a manifest by hand instead of using wapm2pirita.
272        tracing::debug!(
273            command = command_name,
274            "No annotations specifying the atom name found. Falling back to the command name"
275        );
276        return Ok(Some(AtomAnnotation::new(command_name, None)));
277    }
278
279    Ok(None)
280}
281
282/// HACK: Some older packages like `sharrattj/bash` and `sharrattj/coreutils`
283/// contain commands with no annotations. When this happens, you can just assume
284/// it wants to use the first atom in the WEBC file.
285///
286/// That works because most of these packages only have a single atom (e.g. in
287/// `sharrattj/coreutils` there are commands for `ls`, `pwd`, and so on, but
288/// under the hood they all use the `coreutils` atom).
289///
290/// See <https://github.com/wasmerio/wasmer/commit/258903140680716da1431d92bced67d486865aeb>
291/// for more.
292fn legacy_atom_hack(
293    webc: &Container,
294    command_name: &str,
295    metadata: &webc::metadata::Command,
296) -> Result<Option<BinaryPackageCommand>, anyhow::Error> {
297    let (name, atom) = webc
298        .atoms()
299        .into_iter()
300        .next()
301        .ok_or_else(|| anyhow::Error::msg("container does not have any atom"))?;
302
303    tracing::debug!(
304        command_name,
305        atom.name = name.as_str(),
306        atom.len = atom.len(),
307        "(hack) The command metadata is malformed. Falling back to the first atom in the WEBC file",
308    );
309
310    let hash = to_module_hash(webc.manifest().atom_signature(&name)?);
311
312    // Get WebAssembly features from manifest atom annotations
313    let features = if let Some(atom_metadata) = webc.manifest().atoms.get(&name) {
314        extract_features_from_atom_metadata(atom_metadata)
315    } else {
316        None
317    };
318
319    // Get WebAssembly features from manifest atom annotations
320    let suggested_opts_from_manifest = if let Some(atom_metadata) = webc.manifest().atoms.get(&name)
321    {
322        extract_suggested_compiler_opts_from_atom_metadata(atom_metadata)
323    } else {
324        SuggestedCompilerOptimizations::default()
325    };
326
327    Ok(Some(BinaryPackageCommand::new(
328        command_name.to_string(),
329        metadata.clone(),
330        atom,
331        hash,
332        features,
333        suggested_opts_from_manifest,
334    )))
335}
336
337async fn fetch_dependencies(
338    loader: &dyn PackageLoader,
339    pkg: &ResolvedPackage,
340    graph: &DependencyGraph,
341) -> Result<HashMap<PackageId, Container>, Error> {
342    let mut packages = HashSet::new();
343
344    for loc in pkg.commands.values() {
345        packages.insert(loc.package.clone());
346    }
347
348    for mapping in &pkg.filesystem {
349        packages.insert(mapping.package.clone());
350    }
351
352    // We don't need to download the root package
353    packages.remove(&pkg.root_package);
354
355    let packages = packages.into_iter().filter_map(|id| {
356        let crate::runtime::resolver::Node { pkg, dist, .. } = &graph[&id];
357        let summary = PackageSummary {
358            pkg: pkg.clone(),
359            dist: dist.clone()?,
360        };
361        Some((id, summary))
362    });
363    let packages: HashMap<PackageId, Container> = futures::stream::iter(packages)
364        .map(|(id, s)| async move {
365            match loader.load(&s).await {
366                Ok(webc) => Ok((id, webc)),
367                Err(e) => Err(e),
368            }
369        })
370        .buffer_unordered(MAX_PARALLEL_DOWNLOADS)
371        .try_collect()
372        .await?;
373
374    Ok(packages)
375}
376
377/// How many bytes worth of files does a directory contain?
378fn count_file_system(fs: &dyn FileSystem, path: &Path) -> u64 {
379    let mut total = 0;
380
381    let dir = match fs.read_dir(path) {
382        Ok(d) => d,
383        Err(_err) => {
384            return 0;
385        }
386    };
387
388    for entry in dir.flatten() {
389        if let Ok(meta) = entry.metadata() {
390            total += meta.len();
391            if meta.is_dir() {
392                total += count_file_system(fs, entry.path.as_path());
393            }
394        }
395    }
396
397    total
398}
399
400/// Given a set of [`ResolvedFileSystemMapping`]s and the [`Container`] for each
401/// package in a dependency tree, construct the resulting filesystem.
402fn filesystem(
403    packages: &HashMap<PackageId, Container>,
404    pkg: &ResolvedPackage,
405    root_is_local_dir: bool,
406) -> Result<Box<dyn FileSystem + Send + Sync>, Error> {
407    if pkg.filesystem.is_empty() {
408        return Ok(Box::new(OverlayFileSystem::<
409            virtual_fs::EmptyFileSystem,
410            Vec<WebcVolumeFileSystem>,
411        >::new(
412            virtual_fs::EmptyFileSystem::default(), vec![]
413        )));
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        if container.version() == webc::Version::V2 && found_v2.is_none() {
428            found_v2 = Some(package.clone());
429        }
430        if container.version() == webc::Version::V3 && found_v3.is_none() {
431            found_v3 = Some(package.clone());
432        }
433    }
434
435    match (found_v2, found_v3) {
436        (None, Some(_)) => filesystem_v3(packages, pkg, root_is_local_dir),
437        (Some(_), None) => filesystem_v2(packages, pkg, root_is_local_dir),
438        (Some(v2), Some(v3)) => {
439            anyhow::bail!(
440                "Mix of webc v2 and v3 in the same dependency tree is not supported; v2: {v2}, v3: {v3}"
441            )
442        }
443        (None, None) => anyhow::bail!("Internal error: no packages found in tree"),
444    }
445}
446
447/// Build the filesystem for webc v3 packages.
448fn filesystem_v3(
449    packages: &HashMap<PackageId, Container>,
450    pkg: &ResolvedPackage,
451    root_is_local_dir: bool,
452) -> Result<Box<dyn FileSystem + Send + Sync>, Error> {
453    let mut volumes: HashMap<&PackageId, BTreeMap<String, Volume>> = HashMap::new();
454
455    let mut mountings: Vec<_> = pkg.filesystem.iter().collect();
456    mountings.sort_by_key(|m| std::cmp::Reverse(m.mount_path.as_path()));
457
458    let union_fs = UnionFileSystem::new();
459
460    for ResolvedFileSystemMapping {
461        mount_path,
462        volume_name,
463        package,
464        ..
465    } in &pkg.filesystem
466    {
467        if *package == pkg.root_package && root_is_local_dir {
468            continue;
469        }
470
471        // Note: We want to reuse existing Volume instances if we can. That way
472        // we can keep the memory usage down. A webc::compat::Volume is
473        // reference-counted, anyway.
474        // looks like we need to insert it
475        let container = packages.get(package).with_context(|| {
476            format!(
477                "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
478                pkg.root_package, package,
479            )
480        })?;
481        let container_volumes = match volumes.entry(package) {
482            std::collections::hash_map::Entry::Occupied(entry) => &*entry.into_mut(),
483            std::collections::hash_map::Entry::Vacant(entry) => &*entry.insert(container.volumes()),
484        };
485
486        let volume = container_volumes.get(volume_name).with_context(|| {
487            format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume")
488        })?;
489
490        let webc_vol = WebcVolumeFileSystem::new(volume.clone());
491        union_fs.mount(volume_name.clone(), mount_path, Box::new(webc_vol))?;
492    }
493
494    let fs = OverlayFileSystem::new(virtual_fs::EmptyFileSystem::default(), [union_fs]);
495
496    Ok(Box::new(fs))
497}
498
499/// Build the filesystem for webc v2 packages.
500///
501// # Note to future readers
502//
503// Sooo... this code is a bit convoluted because we're constrained by the
504// filesystem implementations we've got available.
505//
506// Ideally, we would create a WebcVolumeFileSystem for each volume we're
507// using, then we'd have a single "union" filesystem which lets you mount
508// filesystem objects under various paths and can deal with conflicts.
509//
510// The OverlayFileSystem lets us make files from multiple filesystem
511// implementations available at the same time, however all of the
512// filesystems will be mounted at "/", when the user wants to mount volumes
513// at arbitrary locations.
514//
515// The TmpFileSystem *does* allow mounting at non-root paths, however it can't
516// handle nested paths (e.g. mounting to "/lib" and "/lib/python3.10" - see
517// <https://github.com/wasmerio/wasmer/issues/3678> for more) and you aren't
518// allowed to mount to "/" because it's a special directory that already
519// exists.
520//
521// As a result, we'll duct-tape things together and hope for the best 🤞
522fn filesystem_v2(
523    packages: &HashMap<PackageId, Container>,
524    pkg: &ResolvedPackage,
525    root_is_local_dir: bool,
526) -> Result<Box<dyn FileSystem + Send + Sync>, Error> {
527    let mut filesystems = Vec::new();
528    let mut volumes: HashMap<&PackageId, BTreeMap<String, Volume>> = HashMap::new();
529
530    let mut mountings: Vec<_> = pkg.filesystem.iter().collect();
531    mountings.sort_by_key(|m| std::cmp::Reverse(m.mount_path.as_path()));
532
533    for ResolvedFileSystemMapping {
534        mount_path,
535        volume_name,
536        package,
537        original_path,
538    } in &pkg.filesystem
539    {
540        if *package == pkg.root_package && root_is_local_dir {
541            continue;
542        }
543
544        // Note: We want to reuse existing Volume instances if we can. That way
545        // we can keep the memory usage down. A webc::compat::Volume is
546        // reference-counted, anyway.
547        let container_volumes = match volumes.entry(package) {
548            std::collections::hash_map::Entry::Occupied(entry) => &*entry.into_mut(),
549            std::collections::hash_map::Entry::Vacant(entry) => {
550                // looks like we need to insert it
551                let container = packages.get(package)
552                    .with_context(|| format!(
553                        "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
554                        pkg.root_package,
555                        package,
556                    ))?;
557                &*entry.insert(container.volumes())
558            }
559        };
560
561        let volume = container_volumes.get(volume_name).with_context(|| {
562            format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume")
563        })?;
564
565        let mount_path = mount_path.clone();
566        // Get a filesystem which will map "$mount_dir/some-path" to
567        // "$original_path/some-path" on the original volume
568        let fs = if let Some(original) = original_path {
569            let original = PathBuf::from(original);
570
571            MappedPathFileSystem::new(
572                WebcVolumeFileSystem::new(volume.clone()),
573                Box::new(move |path: &Path| {
574                    let without_mount_dir = path
575                        .strip_prefix(&mount_path)
576                        .map_err(|_| virtual_fs::FsError::BaseNotDirectory)?;
577                    Ok(original.join(without_mount_dir))
578                }) as DynPathMapper,
579            )
580        } else {
581            MappedPathFileSystem::new(
582                WebcVolumeFileSystem::new(volume.clone()),
583                Box::new(move |path: &Path| {
584                    let without_mount_dir = path
585                        .strip_prefix(&mount_path)
586                        .map_err(|_| virtual_fs::FsError::BaseNotDirectory)?;
587                    Ok(without_mount_dir.to_owned())
588                }) as DynPathMapper,
589            )
590        };
591
592        filesystems.push(fs);
593    }
594
595    let fs = OverlayFileSystem::new(virtual_fs::EmptyFileSystem::default(), filesystems);
596
597    Ok(Box::new(fs))
598}
599
600type DynPathMapper = Box<dyn Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync>;
601
602/// A [`FileSystem`] implementation that lets you map the [`Path`] to something
603/// else.
604#[derive(Clone, PartialEq)]
605struct MappedPathFileSystem<F, M> {
606    inner: F,
607    map: M,
608}
609
610impl<F, M> MappedPathFileSystem<F, M>
611where
612    M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
613{
614    fn new(inner: F, map: M) -> Self {
615        MappedPathFileSystem { inner, map }
616    }
617
618    fn path(&self, path: &Path) -> Result<PathBuf, virtual_fs::FsError> {
619        let path = (self.map)(path)?;
620
621        // Don't forget to make the path absolute again.
622        Ok(Path::new("/").join(path))
623    }
624}
625
626impl<M, F> FileSystem for MappedPathFileSystem<F, M>
627where
628    F: FileSystem,
629    M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
630{
631    fn readlink(&self, path: &Path) -> virtual_fs::Result<PathBuf> {
632        let path = self.path(path)?;
633        self.inner.readlink(&path)
634    }
635
636    fn read_dir(&self, path: &Path) -> virtual_fs::Result<virtual_fs::ReadDir> {
637        let path = self.path(path)?;
638        self.inner.read_dir(&path)
639    }
640
641    fn create_dir(&self, path: &Path) -> virtual_fs::Result<()> {
642        let path = self.path(path)?;
643        self.inner.create_dir(&path)
644    }
645
646    fn remove_dir(&self, path: &Path) -> virtual_fs::Result<()> {
647        let path = self.path(path)?;
648        self.inner.remove_dir(&path)
649    }
650
651    fn rename<'a>(&'a self, from: &Path, to: &Path) -> BoxFuture<'a, virtual_fs::Result<()>> {
652        let from = from.to_owned();
653        let to = to.to_owned();
654        Box::pin(async move {
655            let from = self.path(&from)?;
656            let to = self.path(&to)?;
657            self.inner.rename(&from, &to).await
658        })
659    }
660
661    fn metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
662        let path = self.path(path)?;
663        self.inner.metadata(&path)
664    }
665
666    fn symlink_metadata(&self, path: &Path) -> virtual_fs::Result<virtual_fs::Metadata> {
667        let path = self.path(path)?;
668        self.inner.symlink_metadata(&path)
669    }
670
671    fn remove_file(&self, path: &Path) -> virtual_fs::Result<()> {
672        let path = self.path(path)?;
673        self.inner.remove_file(&path)
674    }
675
676    fn new_open_options(&self) -> virtual_fs::OpenOptions<'_> {
677        virtual_fs::OpenOptions::new(self)
678    }
679
680    fn mount(
681        &self,
682        name: String,
683        path: &Path,
684        fs: Box<dyn FileSystem + Send + Sync>,
685    ) -> virtual_fs::Result<()> {
686        let path = self.path(path)?;
687        self.inner.mount(name, path.as_path(), fs)
688    }
689}
690
691impl<F, M> virtual_fs::FileOpener for MappedPathFileSystem<F, M>
692where
693    F: FileSystem,
694    M: Fn(&Path) -> Result<PathBuf, virtual_fs::FsError> + Send + Sync + 'static,
695{
696    fn open(
697        &self,
698        path: &Path,
699        conf: &virtual_fs::OpenOptionsConfig,
700    ) -> virtual_fs::Result<Box<dyn virtual_fs::VirtualFile + Send + Sync + 'static>> {
701        let path = self.path(path)?;
702        self.inner
703            .new_open_options()
704            .options(conf.clone())
705            .open(path)
706    }
707}
708
709impl<F, M> Debug for MappedPathFileSystem<F, M>
710where
711    F: Debug,
712{
713    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
714        f.debug_struct("MappedPathFileSystem")
715            .field("inner", &self.inner)
716            .field("map", &std::any::type_name::<M>())
717            .finish()
718    }
719}