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