1use std::{
2 collections::{BTreeMap, HashMap, HashSet},
3 path::{Path, PathBuf},
4 sync::Arc,
5};
6
7use anyhow::{Context, Error};
8use futures::{StreamExt, TryStreamExt};
9use once_cell::sync::OnceCell;
10use petgraph::visit::EdgeRef;
11use virtual_fs::{FileSystem, WebcVolumeFileSystem};
12use wasmer_config::package::PackageId;
13use wasmer_package::utils::wasm_annotations_to_features;
14use webc::metadata::annotations::Atom as AtomAnnotation;
15use webc::{Container, Volume};
16
17use crate::{
18 bin_factory::{BinaryPackage, BinaryPackageCommand, BinaryPackageMount, BinaryPackageMounts},
19 runtime::{
20 package_loader::PackageLoader,
21 resolver::{
22 DependencyGraph, ItemLocation, PackageSummary, Resolution, ResolvedFileSystemMapping,
23 ResolvedPackage,
24 },
25 },
26};
27
28use super::to_module_hash;
29
30fn wasm_annotation_to_features(
32 wasm_annotation: &webc::metadata::annotations::Wasm,
33) -> Option<wasmer_types::Features> {
34 Some(wasm_annotations_to_features(&wasm_annotation.features))
35}
36
37fn extract_features_from_atom_metadata(
39 atom_metadata: &webc::metadata::Atom,
40) -> Option<wasmer_types::Features> {
41 if let Ok(Some(wasm_annotation)) = atom_metadata
42 .annotation::<webc::metadata::annotations::Wasm>(webc::metadata::annotations::Wasm::KEY)
43 {
44 wasm_annotation_to_features(&wasm_annotation)
45 } else {
46 None
47 }
48}
49
50const MAX_PARALLEL_DOWNLOADS: usize = 32;
52
53#[tracing::instrument(level = "debug", skip_all)]
55pub async fn load_package_tree(
56 root: &Container,
57 loader: &dyn PackageLoader,
58 resolution: &Resolution,
59 root_is_local_dir: bool,
60) -> Result<BinaryPackage, Error> {
61 let webc_version = root.version();
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 = commands(&resolution.package.commands, &containers, resolution)?;
69
70 let loaded = BinaryPackage {
71 id: root.clone(),
72 package_ids,
73 webc_version,
74 when_cached: crate::syscalls::platform_clock_time_get(
75 wasmer_wasix_types::wasi::Snapshot0Clockid::Monotonic,
76 1_000_000,
77 )
78 .ok()
79 .map(|ts| ts as u128),
80 hash: OnceCell::new(),
81 entrypoint_cmd: resolution.package.entrypoint.clone(),
82 package_mounts: fs_opt.map(Arc::new),
83 commands,
84 uses: Vec::new(),
85
86 additional_host_mapped_directories: vec![],
87 };
88
89 Ok(loaded)
90}
91
92fn commands(
93 commands: &BTreeMap<String, ItemLocation>,
94 containers: &HashMap<PackageId, Container>,
95 resolution: &Resolution,
96) -> Result<Vec<BinaryPackageCommand>, Error> {
97 let mut pkg_commands = Vec::new();
98
99 for (
100 name,
101 ItemLocation {
102 name: original_name,
103 package,
104 },
105 ) in commands
106 {
107 let webc = containers.get(package).with_context(|| {
108 format!("Unable to find the \"{package}\" package for the \"{name}\" command")
109 })?;
110 let manifest = webc.manifest();
111 let command_metadata = manifest.commands.get(original_name).with_context(|| {
112 format!(
113 "Unable to find the \"{original_name}\" command metadata in the \"{package}\" package"
114 )
115 })?;
116
117 if let Some(cmd) =
118 load_binary_command(package, name, command_metadata, containers, resolution)?
119 {
120 pkg_commands.push(cmd);
121 }
122 }
123
124 Ok(pkg_commands)
125}
126
127#[tracing::instrument(skip_all, fields(%package_id, %command_name))]
130fn load_binary_command(
131 package_id: &PackageId,
132 command_name: &str,
133 cmd: &webc::metadata::Command,
134 containers: &HashMap<PackageId, Container>,
135 resolution: &Resolution,
136) -> Result<Option<BinaryPackageCommand>, anyhow::Error> {
137 let AtomAnnotation {
138 name: atom_name,
139 dependency,
140 ..
141 } = match atom_name_for_command(command_name, cmd)? {
142 Some(name) => name,
143 None => {
144 tracing::warn!(
145 cmd.name=command_name,
146 cmd.runner=%cmd.runner,
147 "Skipping unsupported command",
148 );
149 return Ok(None);
150 }
151 };
152
153 let package = containers
154 .get(package_id)
155 .with_context(|| format!("Unable to find the \"{package_id}\" package"))?;
156
157 let (webc, resolved_package_id) = match dependency {
158 Some(dep) => {
159 let ix = resolution
160 .graph
161 .packages()
162 .get(package_id)
163 .copied()
164 .unwrap();
165 let graph = resolution.graph.graph();
166 let edge_reference = graph
167 .edges_directed(ix, petgraph::Direction::Outgoing)
168 .find(|edge| edge.weight().alias == dep)
169 .with_context(|| format!("Unable to find the \"{dep}\" dependency for the \"{command_name}\" command in \"{package_id}\""))?;
170
171 let other_package = graph.node_weight(edge_reference.target()).unwrap();
172 let id = &other_package.id;
173
174 tracing::debug!(
175 dependency=%dep,
176 resolved_package_id=%id,
177 "command atom resolution: resolved dependency",
178 );
179 let container = containers.get(id).ok_or_else(|| {
180 anyhow::anyhow!(
181 "The \"{command_name}\" command in \"{package_id}\" shadows an entry/command of the \"{dep}\" dependency. Rename the local command."
182 )
183 })?;
184
185 (container, id)
186 }
187 None => (package, package_id),
188 };
189
190 let atom = webc.get_atom(&atom_name);
191
192 if atom.is_none() && cmd.annotations.is_empty() {
193 tracing::info!("applying legacy atom hack");
194 return legacy_atom_hack(webc, package_id, command_name, cmd);
195 }
196
197 let hash = to_module_hash(webc.manifest().atom_signature(&atom_name)?);
198
199 let atom = atom.with_context(|| {
200
201 let available_atoms = webc.atoms().keys().map(|x| x.as_str()).collect::<Vec<_>>().join(",");
202
203 tracing::warn!(
204 %atom_name,
205 %resolved_package_id,
206 %available_atoms,
207 "invalid command: could not find atom in package",
208 );
209
210 format!(
211 "The '{command_name}' command uses the '{atom_name}' atom, but it isn't present in the package: {resolved_package_id})"
212 )
213 })?;
214
215 let features = if let Some(atom_metadata) = webc.manifest().atoms.get(&atom_name) {
217 extract_features_from_atom_metadata(atom_metadata)
218 } else {
219 None
220 };
221
222 let cmd = BinaryPackageCommand::new(
223 command_name.to_string(),
224 cmd.clone(),
225 atom,
226 hash,
227 features,
228 package_id.clone(),
229 resolved_package_id.clone(),
230 );
231
232 Ok(Some(cmd))
233}
234
235fn atom_name_for_command(
236 command_name: &str,
237 cmd: &webc::metadata::Command,
238) -> Result<Option<AtomAnnotation>, anyhow::Error> {
239 use webc::metadata::annotations::{WASI_RUNNER_URI, WCGI_RUNNER_URI};
240
241 if let Some(atom) = cmd
242 .atom()
243 .context("Unable to deserialize atom annotations")?
244 {
245 return Ok(Some(atom));
246 }
247
248 if [WASI_RUNNER_URI, WCGI_RUNNER_URI]
249 .iter()
250 .any(|uri| cmd.runner.starts_with(uri))
251 {
252 tracing::debug!(
256 command = command_name,
257 "No annotations specifying the atom name found. Falling back to the command name"
258 );
259 return Ok(Some(AtomAnnotation::new(command_name, None)));
260 }
261
262 Ok(None)
263}
264
265fn legacy_atom_hack(
276 webc: &Container,
277 package_id: &PackageId,
278 command_name: &str,
279 metadata: &webc::metadata::Command,
280) -> Result<Option<BinaryPackageCommand>, anyhow::Error> {
281 let (name, atom) = webc
282 .atoms()
283 .into_iter()
284 .next()
285 .ok_or_else(|| anyhow::Error::msg("container does not have any atom"))?;
286
287 tracing::debug!(
288 command_name,
289 atom.name = name.as_str(),
290 atom.len = atom.len(),
291 "(hack) The command metadata is malformed. Falling back to the first atom in the WEBC file",
292 );
293
294 let hash = to_module_hash(webc.manifest().atom_signature(&name)?);
295
296 let features = if let Some(atom_metadata) = webc.manifest().atoms.get(&name) {
298 extract_features_from_atom_metadata(atom_metadata)
299 } else {
300 None
301 };
302
303 Ok(Some(BinaryPackageCommand::new(
304 command_name.to_string(),
305 metadata.clone(),
306 atom,
307 hash,
308 features,
309 package_id.clone(),
310 package_id.clone(),
311 )))
312}
313
314async fn fetch_dependencies(
315 loader: &dyn PackageLoader,
316 pkg: &ResolvedPackage,
317 graph: &DependencyGraph,
318) -> Result<HashMap<PackageId, Container>, Error> {
319 let packages = packages_needed_for_load(pkg);
320
321 let packages = packages.into_iter().filter_map(|id| {
322 let crate::runtime::resolver::Node { pkg, dist, .. } = &graph[&id];
323 let summary = PackageSummary {
324 pkg: pkg.clone(),
325 dist: dist.clone()?,
326 };
327 Some((id, summary))
328 });
329 let packages: HashMap<PackageId, Container> = futures::stream::iter(packages)
330 .map(|(id, s)| async move {
331 match loader.load(&s).await {
332 Ok(webc) => Ok((id, webc)),
333 Err(e) => Err(e),
334 }
335 })
336 .buffer_unordered(MAX_PARALLEL_DOWNLOADS)
337 .try_collect()
338 .await?;
339
340 Ok(packages)
341}
342
343fn packages_needed_for_load(pkg: &ResolvedPackage) -> HashSet<PackageId> {
344 let mut packages = HashSet::new();
345
346 for loc in pkg.commands.values() {
347 packages.insert(loc.package.clone());
348 }
349
350 for mapping in &pkg.filesystem {
351 packages.insert(mapping.package.clone());
352 }
353
354 packages.remove(&pkg.root_package);
356
357 packages
358}
359
360fn filesystem(
365 packages: &HashMap<PackageId, Container>,
366 pkg: &ResolvedPackage,
367 root_is_local_dir: bool,
368) -> Result<Option<BinaryPackageMounts>, Error> {
369 if pkg.filesystem.is_empty() {
370 return Ok(None);
371 }
372
373 let mut found_v2 = None;
374 let mut found_v3 = None;
375
376 for ResolvedFileSystemMapping { package, .. } in &pkg.filesystem {
377 let container = packages.get(package).with_context(|| {
378 format!(
379 "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
380 pkg.root_package, package,
381 )
382 })?;
383
384 match container.version() {
385 webc::Version::V2 => {
386 if found_v2.is_none() {
387 found_v2 = Some(package.clone());
388 }
389 }
390 webc::Version::V3 => {
391 if found_v3.is_none() {
392 found_v3 = Some(package.clone());
393 }
394 }
395 other => {
396 anyhow::bail!("the package '{package}' has an unknown webc version: {other}");
397 }
398 }
399 }
400
401 match (found_v2, found_v3) {
402 (None, Some(_)) => filesystem_v3(packages, pkg, root_is_local_dir).map(Some),
403 (Some(_), None) => filesystem_v2(packages, pkg, root_is_local_dir).map(Some),
404 (Some(v2), Some(v3)) => {
405 anyhow::bail!(
406 "Mix of webc v2 and v3 in the same dependency tree is not supported; v2: {v2}, v3: {v3}"
407 )
408 }
409 (None, None) => anyhow::bail!("Internal error: no packages found in tree"),
410 }
411}
412
413fn filesystem_v3(
415 packages: &HashMap<PackageId, Container>,
416 pkg: &ResolvedPackage,
417 root_is_local_dir: bool,
418) -> Result<BinaryPackageMounts, Error> {
419 let mut volumes: HashMap<&PackageId, BTreeMap<String, Volume>> = HashMap::new();
420 let mut root_layer = None;
421 let mut mounts = Vec::new();
422
423 for ResolvedFileSystemMapping {
424 mount_path,
425 volume_name,
426 package,
427 ..
428 } in &pkg.filesystem
429 {
430 if *package == pkg.root_package && root_is_local_dir {
431 continue;
432 }
433
434 if mount_path.as_path() == Path::new("/") {
435 tracing::warn!(
436 "The \"{package}\" package wants to mount a volume at \"/\", which breaks WASIX modules' filesystems",
437 );
438 }
439
440 let container = packages.get(package).with_context(|| {
445 format!(
446 "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
447 pkg.root_package, package,
448 )
449 })?;
450 let container_volumes = match volumes.entry(package) {
451 std::collections::hash_map::Entry::Occupied(entry) => &*entry.into_mut(),
452 std::collections::hash_map::Entry::Vacant(entry) => &*entry.insert(container.volumes()),
453 };
454
455 let volume = container_volumes.get(volume_name).with_context(|| {
456 format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume")
457 })?;
458
459 let webc_vol = WebcVolumeFileSystem::new(volume.clone());
460 if mount_path.as_path() == Path::new("/") {
461 root_layer = Some(Arc::new(webc_vol) as Arc<dyn FileSystem + Send + Sync>);
462 } else {
463 mounts.push(BinaryPackageMount {
464 guest_path: mount_path.clone(),
465 fs: Arc::new(webc_vol),
466 source_path: PathBuf::from("/"),
467 });
468 }
469 }
470
471 Ok(BinaryPackageMounts { root_layer, mounts })
472}
473
474fn filesystem_v2(
498 packages: &HashMap<PackageId, Container>,
499 pkg: &ResolvedPackage,
500 root_is_local_dir: bool,
501) -> Result<BinaryPackageMounts, Error> {
502 let mut volumes: HashMap<&PackageId, BTreeMap<String, Volume>> = HashMap::new();
503 let mut root_layer = None;
504 let mut mounts = Vec::new();
505
506 for ResolvedFileSystemMapping {
507 mount_path,
508 volume_name,
509 package,
510 original_path,
511 } in &pkg.filesystem
512 {
513 if *package == pkg.root_package && root_is_local_dir {
514 continue;
515 }
516
517 if mount_path.as_path() == Path::new("/") {
518 tracing::warn!(
519 "The \"{package}\" package wants to mount a volume at \"/\", which breaks WASIX modules' filesystems",
520 );
521 }
522
523 let container_volumes = match volumes.entry(package) {
527 std::collections::hash_map::Entry::Occupied(entry) => &*entry.into_mut(),
528 std::collections::hash_map::Entry::Vacant(entry) => {
529 let container = packages.get(package)
531 .with_context(|| format!(
532 "\"{}\" wants to use the \"{}\" package, but it isn't in the dependency tree",
533 pkg.root_package,
534 package,
535 ))?;
536 &*entry.insert(container.volumes())
537 }
538 };
539
540 let volume = container_volumes.get(volume_name).with_context(|| {
541 format!("The \"{package}\" package doesn't have a \"{volume_name}\" volume")
542 })?;
543
544 let mounted_fs = Arc::new(WebcVolumeFileSystem::new(volume.clone()))
545 as Arc<dyn FileSystem + Send + Sync>;
546 let source_path = original_path
547 .as_deref()
548 .map(PathBuf::from)
549 .unwrap_or_else(|| PathBuf::from("/"));
550
551 if mount_path.as_path() == Path::new("/") {
552 root_layer = Some(mounted_fs);
553 } else {
554 mounts.push(BinaryPackageMount {
555 guest_path: mount_path.clone(),
556 fs: mounted_fs,
557 source_path,
558 });
559 }
560 }
561
562 Ok(BinaryPackageMounts { root_layer, mounts })
563}
564
565#[cfg(test)]
566mod tests {
567 use std::{
568 collections::{BTreeMap, HashMap},
569 path::{Path, PathBuf},
570 };
571
572 use anyhow::Error;
573 use ciborium::value::Value;
574 use petgraph::graph::DiGraph;
575 use virtual_fs::FileSystem;
576 use wasmer_config::package::PackageId;
577 use webc::{
578 Container,
579 indexmap::IndexMap,
580 metadata::{
581 Command as WebcCommand, Manifest,
582 annotations::{
583 Atom as AtomAnnotation, FileSystemMapping, FileSystemMappings, WASI_RUNNER_URI,
584 },
585 },
586 v2::{
587 SignatureAlgorithm,
588 read::OwnedReader,
589 write::{DirEntry, Directory, FileEntry, Writer},
590 },
591 };
592
593 use super::{ResolvedFileSystemMapping, ResolvedPackage, filesystem_v2};
594 use crate::runtime::{
595 package_loader::PackageLoader,
596 resolver::{
597 Command, DependencyGraph, DistributionInfo, Edge, ItemLocation, Node, PackageInfo,
598 PackageSummary, Resolution, WebcHash,
599 },
600 };
601
602 #[derive(Debug, Default)]
603 struct TestLoader;
604
605 #[async_trait::async_trait]
606 impl PackageLoader for TestLoader {
607 async fn load(&self, summary: &PackageSummary) -> Result<Container, Error> {
608 anyhow::bail!("unexpected dependency fetch: {}", summary.package_id())
609 }
610
611 async fn load_package_tree(
612 &self,
613 root: &Container,
614 resolution: &Resolution,
615 root_is_local_dir: bool,
616 ) -> Result<crate::bin_factory::BinaryPackage, Error> {
617 super::load_package_tree(root, self, resolution, root_is_local_dir).await
618 }
619 }
620
621 #[test]
622 fn v2_filesystem_mapping_resolves_mount_paths() {
623 let mut manifest = Manifest::default();
626 let fs = FileSystemMappings(vec![FileSystemMapping {
627 from: None,
628 volume_name: "atom".to_string(),
629 host_path: Some("/public".to_string()),
630 mount_path: "/public".to_string(),
631 }]);
632 let mut package = IndexMap::new();
633 package.insert(
634 FileSystemMappings::KEY.to_string(),
635 Value::serialized(&fs).unwrap(),
636 );
637 manifest.package = package;
638
639 let mut public_children = BTreeMap::new();
640 public_children.insert(
641 "index.html".parse().unwrap(),
642 DirEntry::File(FileEntry::from(b"ok".as_slice())),
643 );
644 let public_mount_dir = Directory {
645 children: public_children,
646 };
647 let mut root_children = BTreeMap::new();
648 root_children.insert("public".parse().unwrap(), DirEntry::Dir(public_mount_dir));
649 let atom_dir = Directory {
650 children: root_children,
651 };
652
653 let writer = Writer::default().write_manifest(&manifest).unwrap();
654 let writer = writer.write_atoms(BTreeMap::new()).unwrap();
655 let writer = writer.with_volume("atom", atom_dir).unwrap();
656 let bytes = writer.finish(SignatureAlgorithm::None).unwrap();
657
658 let reader = OwnedReader::parse(bytes).unwrap();
659 let container = Container::from(reader);
660
661 let pkg_id = PackageId::new_named("ns/pkg", "0.1.0".parse().unwrap());
662 let mut packages = HashMap::new();
663 packages.insert(pkg_id.clone(), container);
664
665 let pkg = ResolvedPackage {
666 root_package: pkg_id.clone(),
667 commands: BTreeMap::new(),
668 entrypoint: None,
669 filesystem: vec![ResolvedFileSystemMapping {
670 mount_path: PathBuf::from("/public"),
671 volume_name: "atom".to_string(),
672 original_path: Some("/public".to_string()),
673 package: pkg_id,
674 }],
675 };
676
677 let mounts = filesystem_v2(&packages, &pkg, false).unwrap();
678 let mount_fs = mounts.to_mount_fs().unwrap();
679 assert!(mount_fs.metadata(Path::new("/public")).unwrap().is_dir());
680 assert!(
681 mount_fs
682 .metadata(Path::new("/public/index.html"))
683 .unwrap()
684 .is_file()
685 );
686 }
687
688 #[tokio::test]
689 async fn load_package_tree_reports_shadowed_dependency_command_without_panic() {
690 let root_id = PackageId::new_named("root", "0.1.0".parse().unwrap());
691 let dep_id = PackageId::new_named("wasmer/static-web-server", "1.0.0".parse().unwrap());
692 let dep_alias = "wasmer/static-web-server";
693
694 let root_info = PackageInfo {
695 id: root_id.clone(),
696 commands: vec![Command {
697 name: "webserver".to_string(),
698 }],
699 entrypoint: Some("webserver".to_string()),
700 dependencies: Vec::new(),
701 filesystem: Vec::new(),
702 };
703 let dep_info = PackageInfo {
704 id: dep_id.clone(),
705 commands: vec![Command {
706 name: "webserver".to_string(),
707 }],
708 entrypoint: Some("webserver".to_string()),
709 dependencies: Vec::new(),
710 filesystem: Vec::new(),
711 };
712 let root_container = test_container([(
713 "webserver",
714 command_for_atom("webserver", Some(dep_alias.to_string())),
715 )]);
716
717 let mut graph = DiGraph::new();
718 let root = graph.add_node(Node {
719 id: root_id.clone(),
720 pkg: root_info,
721 dist: None,
722 });
723 let dep = graph.add_node(Node {
724 id: dep_id.clone(),
725 pkg: dep_info,
726 dist: Some(DistributionInfo {
727 webc: "http://localhost/wasmer-static-web-server.webc"
728 .parse()
729 .unwrap(),
730 webc_sha256: WebcHash::from([0; 32]),
731 }),
732 });
733 graph.add_edge(
734 root,
735 dep,
736 Edge {
737 alias: dep_alias.to_string(),
738 },
739 );
740
741 let dependency_graph = DependencyGraph::new(
742 root,
743 graph,
744 BTreeMap::from([(root_id.clone(), root), (dep_id.clone(), dep)]),
745 );
746 let pkg = ResolvedPackage {
747 root_package: root_id.clone(),
748 commands: BTreeMap::from([(
749 "webserver".to_string(),
750 ItemLocation {
751 name: "webserver".to_string(),
752 package: root_id.clone(),
753 },
754 )]),
755 entrypoint: Some("webserver".to_string()),
756 filesystem: Vec::new(),
757 };
758
759 let err = super::load_package_tree(
760 &root_container,
761 &TestLoader,
762 &Resolution {
763 package: pkg,
764 graph: dependency_graph,
765 },
766 false,
767 )
768 .await
769 .unwrap_err();
770 let message = format!("{err:#}");
771
772 assert!(message.contains("shadows an entry/command"));
773 assert!(message.contains("Rename the local command"));
774 }
775
776 fn command_for_atom(atom: &str, dependency: Option<String>) -> WebcCommand {
777 let mut annotations = IndexMap::new();
778 annotations.insert(
779 AtomAnnotation::KEY.to_string(),
780 Value::serialized(&AtomAnnotation::new(atom, dependency)).unwrap(),
781 );
782
783 WebcCommand {
784 runner: WASI_RUNNER_URI.to_string(),
785 annotations,
786 }
787 }
788
789 fn test_container<'a>(commands: impl IntoIterator<Item = (&'a str, WebcCommand)>) -> Container {
790 let mut manifest = Manifest::default();
791
792 for (name, command) in commands {
793 manifest.commands.insert(name.to_string(), command);
794 }
795
796 let writer = Writer::default().write_manifest(&manifest).unwrap();
797 let writer = writer.write_atoms(BTreeMap::new()).unwrap();
798 let bytes = writer.finish(SignatureAlgorithm::None).unwrap();
799 let reader = OwnedReader::parse(bytes).unwrap();
800 Container::from(reader)
801 }
802
803 #[test]
804 fn v2_filesystem_mapping_preserves_root_and_nested_mounts() {
805 let mut manifest = Manifest::default();
806 let fs = FileSystemMappings(vec![
807 FileSystemMapping {
808 from: None,
809 volume_name: "root".to_string(),
810 host_path: Some("/".to_string()),
811 mount_path: "/".to_string(),
812 },
813 FileSystemMapping {
814 from: None,
815 volume_name: "public".to_string(),
816 host_path: Some("/public".to_string()),
817 mount_path: "/public".to_string(),
818 },
819 ]);
820 let mut package = IndexMap::new();
821 package.insert(
822 FileSystemMappings::KEY.to_string(),
823 Value::serialized(&fs).unwrap(),
824 );
825 manifest.package = package;
826
827 let mut root_children = BTreeMap::new();
828 root_children.insert(
829 "root.txt".parse().unwrap(),
830 DirEntry::File(FileEntry::from(b"root".as_slice())),
831 );
832 let root_dir = Directory {
833 children: root_children,
834 };
835
836 let mut public_children = BTreeMap::new();
837 public_children.insert(
838 "index.html".parse().unwrap(),
839 DirEntry::File(FileEntry::from(b"ok".as_slice())),
840 );
841 let public_dir = Directory {
842 children: public_children,
843 };
844
845 let writer = Writer::default().write_manifest(&manifest).unwrap();
846 let writer = writer.write_atoms(BTreeMap::new()).unwrap();
847 let writer = writer.with_volume("root", root_dir).unwrap();
848 let writer = writer.with_volume("public", public_dir).unwrap();
849 let bytes = writer.finish(SignatureAlgorithm::None).unwrap();
850
851 let reader = OwnedReader::parse(bytes).unwrap();
852 let container = Container::from(reader);
853
854 let pkg_id = PackageId::new_named("ns/pkg", "0.1.0".parse().unwrap());
855 let mut packages = HashMap::new();
856 packages.insert(pkg_id.clone(), container);
857
858 let pkg = ResolvedPackage {
859 root_package: pkg_id.clone(),
860 commands: BTreeMap::new(),
861 entrypoint: None,
862 filesystem: vec![
863 ResolvedFileSystemMapping {
864 mount_path: PathBuf::from("/"),
865 volume_name: "root".to_string(),
866 original_path: Some("/".to_string()),
867 package: pkg_id.clone(),
868 },
869 ResolvedFileSystemMapping {
870 mount_path: PathBuf::from("/public"),
871 volume_name: "public".to_string(),
872 original_path: Some("/public".to_string()),
873 package: pkg_id,
874 },
875 ],
876 };
877
878 let mounts = filesystem_v2(&packages, &pkg, false).unwrap();
879 let root_layer = mounts
880 .root_layer
881 .as_ref()
882 .expect("expected root layer mount");
883 assert!(
884 root_layer
885 .metadata(Path::new("/root.txt"))
886 .unwrap()
887 .is_file()
888 );
889 assert_eq!(mounts.mounts.len(), 1);
890 assert_eq!(mounts.mounts[0].guest_path, Path::new("/public"));
891 }
892}