1use std::{
2 collections::{BTreeMap, BTreeSet},
3 fmt::Debug,
4 fs::{self, File},
5 io::Read,
6 path::{Path, PathBuf},
7};
8
9use anyhow::{Context, Error};
10use shared_buffer::OwnedBuffer;
11
12use webc::{
13 AbstractVolume, Metadata, PathSegment, PathSegments, Timestamps, ToPathSegments, sanitize_path,
14 v3::{
15 self,
16 write::{DirEntry, Directory, FileEntry, SymlinkEntry},
17 },
18};
19
20use crate::package::{Strictness, WalkBuilderFactory};
21
22use super::WasmerPackageVolume;
23
24pub struct FsVolume {
30 name: String,
32 intermediate_directories: BTreeSet<PathBuf>,
35 metadata_files: BTreeSet<PathBuf>,
37 mapped_directories: BTreeSet<PathBuf>,
39 base_dir: PathBuf,
41 walker_factory: WalkBuilderFactory,
43}
44
45impl Debug for FsVolume {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.debug_struct("FsVolume")
48 .field("name", &self.name)
49 .field("intermediate_directories", &self.intermediate_directories)
50 .field("metadata_files", &self.metadata_files)
51 .field("mapped_directories", &self.mapped_directories)
52 .field("base_dir", &self.base_dir)
53 .finish()
54 }
55}
56
57impl FsVolume {
58 pub(crate) const METADATA: &'static str = "metadata";
60
61 pub(crate) fn new_metadata(
63 manifest: &wasmer_config::package::Manifest,
64 base_dir: impl Into<PathBuf>,
65 ) -> Result<Self, Error> {
66 let base_dir = base_dir.into();
67 let mut files = BTreeSet::new();
68
69 if let Some(package) = &manifest.package {
71 if let Some(license_file) = &package.license_file {
72 files.insert(base_dir.join(license_file));
73 }
74
75 if let Some(readme) = &package.readme {
76 files.insert(base_dir.join(readme));
77 }
78 }
79
80 for module in &manifest.modules {
81 if let Some(bindings) = &module.bindings {
82 let bindings_files = bindings.referenced_files(&base_dir)?;
83 files.extend(bindings_files);
84 }
85 }
86
87 Ok(FsVolume::new_with_intermediate_dirs(
88 FsVolume::METADATA.to_string(),
89 base_dir,
90 files,
91 BTreeSet::new(),
92 crate::package::include_everything_walker(),
93 ))
94 }
95
96 pub(crate) fn new_assets(
97 manifest: &wasmer_config::package::Manifest,
98 base_dir: &Path,
99 walker_factory: crate::package::WalkBuilderFactory,
100 ) -> Result<BTreeMap<String, Self>, Error> {
101 let dirs: BTreeSet<_> = manifest
103 .fs
104 .values()
105 .map(|path| base_dir.join(path))
106 .collect();
107
108 for path in &dirs {
109 let _ = std::fs::symlink_metadata(path).with_context(|| {
111 format!("Unable to get the metadata for \"{}\"", path.display())
112 })?;
113 }
114
115 let mut volumes = BTreeMap::new();
116 for entry in manifest.fs.values() {
117 let name = entry
118 .to_str()
119 .ok_or_else(|| anyhow::anyhow!("Failed to convert path to str"))?;
120
121 let name = sanitize_path(name);
122
123 let mut dirs = BTreeSet::new();
124 let dir = base_dir.join(entry);
125 dirs.insert(dir);
126
127 volumes.insert(
128 name.clone(),
129 FsVolume::new(
130 name.to_string(),
131 base_dir.to_path_buf(),
132 BTreeSet::new(),
133 dirs,
134 walker_factory,
135 ),
136 );
137 }
138
139 Ok(volumes)
140 }
141
142 pub(crate) fn new_with_intermediate_dirs(
143 name: String,
144 base_dir: PathBuf,
145 whitelisted_files: BTreeSet<PathBuf>,
146 whitelisted_directories: BTreeSet<PathBuf>,
147 walker_factory: crate::package::WalkBuilderFactory,
148 ) -> Self {
149 let mut intermediate_directories: BTreeSet<PathBuf> = whitelisted_files
150 .iter()
151 .filter_map(|p| p.parent())
152 .chain(whitelisted_directories.iter().map(|p| p.as_path()))
153 .flat_map(|dir| dir.ancestors())
154 .filter(|dir| dir.starts_with(&base_dir))
155 .map(|dir| dir.to_path_buf())
156 .collect();
157
158 intermediate_directories.insert(base_dir.clone());
160
161 FsVolume {
162 name,
163 intermediate_directories,
164 metadata_files: whitelisted_files,
165 mapped_directories: whitelisted_directories,
166 base_dir,
167 walker_factory,
168 }
169 }
170
171 pub(crate) fn new(
172 name: String,
173 base_dir: PathBuf,
174 whitelisted_files: BTreeSet<PathBuf>,
175 whitelisted_directories: BTreeSet<PathBuf>,
176 walker_factory: crate::package::WalkBuilderFactory,
177 ) -> Self {
178 FsVolume {
179 name,
180 intermediate_directories: BTreeSet::new(),
181 metadata_files: whitelisted_files,
182 mapped_directories: whitelisted_directories,
183 base_dir,
184 walker_factory,
185 }
186 }
187
188 fn is_accessible(&self, path: &Path) -> bool {
189 self.intermediate_directories.contains(path)
190 || self.metadata_files.contains(path)
191 || self
192 .mapped_directories
193 .iter()
194 .any(|dir| path.starts_with(dir))
195 }
196
197 fn resolve(&self, path: &PathSegments) -> Option<PathBuf> {
198 let resolved = if let Some(dir) = &self.mapped_directories.first() {
199 resolve(dir, path)
200 } else {
201 resolve(&self.base_dir, path)
202 };
203
204 let accessible = self.is_accessible(&resolved);
205 accessible.then_some(resolved)
206 }
207
208 pub fn name(&self) -> &str {
210 self.name.as_str()
211 }
212
213 pub fn read_file(&self, path: &PathSegments) -> Option<OwnedBuffer> {
215 let path = self.resolve(path)?;
216 if !path.symlink_metadata().ok()?.is_file() {
217 return None;
218 }
219
220 let mut f = File::open(path).ok()?;
221
222 if let Ok(mmapped) = OwnedBuffer::from_file(&f) {
224 return Some(mmapped);
225 }
226
227 let mut buffer = Vec::new();
229 f.read_to_end(&mut buffer).ok()?;
230 Some(OwnedBuffer::from_bytes(buffer))
231 }
232
233 #[allow(clippy::type_complexity)]
235 pub fn read_dir(
236 &self,
237 path: &PathSegments,
238 ) -> Option<Vec<(PathSegment, Option<[u8; 32]>, Metadata)>> {
239 let resolved = self.resolve(path)?;
240 if !resolved.symlink_metadata().ok()?.is_dir() {
241 return None;
242 }
243
244 let mut walker_builder = self.walker_factory.create_walk_builder(&resolved);
245 walker_builder.max_depth(Some(1));
246 let walker = walker_builder.build();
247
248 let mut entries = Vec::new();
249
250 for entry in walker {
251 let entry = entry.ok()?;
252 if entry.depth() == 0 {
254 continue;
255 }
256
257 let entry = entry.path();
258
259 if !self.is_accessible(entry) {
260 continue;
261 }
262
263 let segment: PathSegment = entry.file_name()?.to_str()?.parse().ok()?;
264
265 let path = path.join(segment.clone());
266 let metadata = self.metadata(&path)?;
267 entries.push((segment, None, metadata));
268 }
269
270 entries.sort_by_key(|k| k.0.clone());
271
272 Some(entries)
273 }
274
275 pub fn metadata(&self, path: &PathSegments) -> Option<Metadata> {
277 let path = self.resolve(path)?;
278 let meta = path.symlink_metadata().ok()?;
279
280 let timestamps = Timestamps::from_metadata(&meta).ok()?;
281
282 if meta.file_type().is_symlink() {
283 let target = fs::read_link(&path).ok()?;
284 let target = target.to_str()?;
285
286 Some(Metadata::Symlink {
287 target_length: target.len(),
288 timestamps: Some(timestamps),
289 })
290 } else if meta.is_dir() {
291 Some(Metadata::Dir {
292 timestamps: Some(timestamps),
293 })
294 } else if meta.is_file() {
295 Some(Metadata::File {
296 length: meta.len().try_into().ok()?,
297 timestamps: Some(timestamps),
298 })
299 } else {
300 None
301 }
302 }
303
304 pub(crate) fn as_directory_tree(&self, strictness: Strictness) -> Result<Directory<'_>, Error> {
305 if self.name() == "metadata" {
306 let mut root = Directory::default();
307
308 for file_path in self.metadata_files.iter() {
309 let meta = match file_path.symlink_metadata() {
310 Ok(meta) => meta,
311 Err(_) if strictness.is_strict() => {
312 anyhow::bail!("{} does not exist", file_path.display());
313 }
314 Err(_) => {
315 continue;
317 }
318 };
319
320 if !meta.is_file() && !meta.file_type().is_symlink() {
321 if strictness.is_strict() {
322 anyhow::bail!("{} is not a file", file_path.display());
323 }
324
325 continue;
326 }
327
328 let path = file_path.strip_prefix(&self.base_dir)?;
329 let path = PathBuf::from("/").join(path);
330 let segments = path.to_path_segments()?;
331 let segments: Vec<_> = segments.iter().collect();
332
333 let file_entry = if meta.file_type().is_symlink() {
334 DirEntry::Symlink(SymlinkEntry::from_path(file_path)?)
335 } else {
336 DirEntry::File(FileEntry::from_path(file_path)?)
337 };
338
339 let mut curr_dir = &mut root;
340 for (index, segment) in segments.iter().enumerate() {
341 if segments.len() == 1 {
342 curr_dir.children.insert((*segment).clone(), file_entry);
343 break;
344 } else {
345 if index == segments.len() - 1 {
346 curr_dir.children.insert((*segment).clone(), file_entry);
347 break;
348 }
349
350 let curr_entry = curr_dir
351 .children
352 .entry((*segment).clone())
353 .or_insert(DirEntry::Dir(Directory::default()));
354 let DirEntry::Dir(dir) = curr_entry else {
355 unreachable!()
356 };
357
358 curr_dir = dir;
359 }
360 }
361 }
362
363 Ok(root)
364 } else {
365 let paths: Vec<_> = self.mapped_directories.iter().cloned().collect();
366 directory_tree(paths, &self.base_dir, self.walker_factory)
367 }
368 }
369}
370
371impl AbstractVolume for FsVolume {
372 fn read_file(&self, path: &PathSegments) -> Option<(OwnedBuffer, Option<[u8; 32]>)> {
373 self.read_file(path).map(|c| (c, None))
374 }
375
376 fn read_dir(
377 &self,
378 path: &PathSegments,
379 ) -> Option<Vec<(PathSegment, Option<[u8; 32]>, Metadata)>> {
380 self.read_dir(path)
381 }
382
383 fn metadata(&self, path: &PathSegments) -> Option<Metadata> {
384 self.metadata(path)
385 }
386
387 fn read_link(&self, path: &PathSegments) -> Option<(String, Option<[u8; 32]>)> {
388 let path = self.resolve(path)?;
389 if !path.symlink_metadata().ok()?.file_type().is_symlink() {
390 return None;
391 }
392
393 let target = fs::read_link(path).ok()?;
394 Some((target.to_str()?.to_owned(), None))
395 }
396}
397
398impl WasmerPackageVolume for FsVolume {
399 fn as_directory_tree(&self, strictness: Strictness) -> Result<Directory<'_>, Error> {
400 self.as_directory_tree(strictness)
401 }
402}
403
404fn resolve(base_dir: &Path, path: &PathSegments) -> PathBuf {
406 let mut resolved = base_dir.to_path_buf();
407 for segment in path.iter() {
408 resolved.push(segment.as_str());
409 }
410
411 resolved
412}
413
414fn directory_tree(
417 paths: impl IntoIterator<Item = PathBuf>,
418 base_dir: &Path,
419 walker_factory: crate::package::WalkBuilderFactory,
420) -> Result<Directory<'static>, Error> {
421 let paths: Vec<_> = paths.into_iter().collect();
422 let mut root = Directory::default();
423
424 for path in paths {
425 let meta = path.symlink_metadata().map_err(|e| {
426 Error::from(e).context(format!(
427 "Unable to add \"{}\" to the directory tree",
428 path.display()
429 ))
430 })?;
431 let file_type = meta.file_type();
432
433 if file_type.is_symlink() {
434 let dir_entry =
435 v3::write::DirEntry::Symlink(v3::write::SymlinkEntry::from_path(&path)?);
436 let path = path.strip_prefix(base_dir)?;
437 let path_segment = PathSegment::try_from(path.as_os_str())?;
438
439 if root.children.insert(path_segment, dir_entry).is_some() {
440 println!("Warning: {path:?} already exists. Overriding the old entry");
441 }
442 } else if file_type.is_file() {
443 let dir_entry = v3::write::DirEntry::File(v3::write::FileEntry::from_path(&path)?);
444 let path = path.strip_prefix(base_dir)?;
445 let path_segment = PathSegment::try_from(path.as_os_str())?;
446
447 if root.children.insert(path_segment, dir_entry).is_some() {
448 println!("Warning: {path:?} already exists. Overriding the old entry");
449 }
450 } else {
451 let dir = webc::v3::write::Directory::from_path_with_walker(
452 &path,
453 walker_factory.create_walk_builder(&path).build(),
454 )
455 .map_err(|e| {
456 Error::from(e).context(format!(
457 "Unable to add \"{}\" to the directory tree",
458 path.display()
459 ))
460 })?;
461 for (path, child) in dir.children {
462 root.children.insert(path.clone(), child);
463 }
464 }
465 }
466
467 Ok(root)
468}
469
470#[cfg(test)]
471mod tests {
472 use tempfile::TempDir;
473 use wasmer_config::package::Manifest;
474
475 use super::*;
476
477 #[test]
478 fn metadata_volume() {
479 let temp = TempDir::new().unwrap();
480 let wasmer_toml = r#"
481 [package]
482 name = "some/package"
483 version = "0.0.0"
484 description = ""
485 license-file = "./path/to/LICENSE"
486 readme = "README.md"
487
488 [[module]]
489 name = "asdf"
490 source = "asdf.wasm"
491 abi = "none"
492 bindings = { wai-version = "0.2.0", exports = "asdf.wai", imports = ["browser.wai"] }
493 "#;
494 let wasmer_toml_path = temp.path().join("wasmer.toml");
495 std::fs::write(&wasmer_toml_path, wasmer_toml.as_bytes()).unwrap();
496 let license_dir = temp.path().join("path").join("to");
497 std::fs::create_dir_all(&license_dir).unwrap();
498 std::fs::write(license_dir.join("LICENSE"), "license").unwrap();
499 std::fs::write(temp.path().join("README.md"), "readme").unwrap();
500 std::fs::write(temp.path().join("asdf.wai"), "exports").unwrap();
501 std::fs::write(temp.path().join("browser.wai"), "imports").unwrap();
502 let manifest: Manifest = toml::from_str(wasmer_toml).unwrap();
503
504 let volume = FsVolume::new_metadata(&manifest, temp.path().to_path_buf()).unwrap();
505
506 let entries = volume.read_dir(&PathSegments::ROOT).unwrap();
507 let expected = [
508 PathSegment::parse("README.md").unwrap(),
509 PathSegment::parse("asdf.wai").unwrap(),
510 PathSegment::parse("browser.wai").unwrap(),
511 PathSegment::parse("path").unwrap(),
512 ];
513
514 for i in 0..expected.len() {
515 assert_eq!(entries[i].0, expected[i]);
516 assert!(entries[i].2.timestamps().is_some());
517 }
518
519 let license: PathSegments = "/path/to/LICENSE".parse().unwrap();
520 assert_eq!(
521 String::from_utf8(volume.read_file(&license).unwrap().into()).unwrap(),
522 "license"
523 );
524 }
525
526 #[test]
527 fn asset_volume() {
528 let temp = TempDir::new().unwrap();
529 let wasmer_toml = r#"
530 [package]
531 name = "some/package"
532 version = "0.0.0"
533 description = ""
534 license_file = "./path/to/LICENSE"
535 readme = "README.md"
536
537 [[module]]
538 name = "asdf"
539 source = "asdf.wasm"
540 abi = "none"
541 bindings = { wai-version = "0.2.0", exports = "asdf.wai", imports = ["browser.wai"] }
542
543 [fs]
544 "/etc" = "etc"
545 "#;
546 let license_dir = temp.path().join("path").join("to");
547 std::fs::create_dir_all(&license_dir).unwrap();
548 std::fs::write(license_dir.join("LICENSE"), "license").unwrap();
549 std::fs::write(temp.path().join("README.md"), "readme").unwrap();
550 std::fs::write(temp.path().join("asdf.wai"), "exports").unwrap();
551 std::fs::write(temp.path().join("browser.wai"), "imports").unwrap();
552
553 let etc = temp.path().join("etc");
554 let share = etc.join("share");
555 std::fs::create_dir_all(&share).unwrap();
556
557 std::fs::write(etc.join(".wasmerignore"), b"ignore_me").unwrap();
558 std::fs::write(etc.join(".hidden"), "anything, really").unwrap();
559 std::fs::write(etc.join("ignore_me"), "I should be ignored").unwrap();
560 std::fs::write(share.join("package.1"), "man page").unwrap();
561 std::fs::write(share.join("ignore_me"), "I should be ignored too").unwrap();
562
563 let manifest: Manifest = toml::from_str(wasmer_toml).unwrap();
564
565 let volume = FsVolume::new_assets(
566 &manifest,
567 temp.path(),
568 crate::package::wasmer_ignore_walker(),
569 )
570 .unwrap();
571
572 let volume = &volume["/etc"];
573
574 let entries = volume.read_dir(&PathSegments::ROOT).unwrap();
575 let expected = [
576 PathSegment::parse(".hidden").unwrap(),
577 PathSegment::parse(".wasmerignore").unwrap(),
578 PathSegment::parse("share").unwrap(),
579 ];
580
581 for i in 0..expected.len() {
582 assert_eq!(entries[i].0, expected[i]);
583 assert!(entries[i].2.timestamps().is_some());
584 }
585
586 let man_page: PathSegments = "/share/package.1".parse().unwrap();
587 assert_eq!(
588 String::from_utf8(volume.read_file(&man_page).unwrap().into()).unwrap(),
589 "man page"
590 );
591 }
592
593 #[cfg(unix)]
594 #[test]
595 fn fs_volume_preserves_symlinks() {
596 let temp = TempDir::new().unwrap();
597 let target_file = temp.path().join("target.txt");
598 let target_dir = temp.path().join("target-dir");
599
600 std::fs::write(&target_file, "target").unwrap();
601 std::fs::create_dir(&target_dir).unwrap();
602 std::os::unix::fs::symlink("target.txt", temp.path().join("file-link")).unwrap();
603 std::os::unix::fs::symlink("target-dir", temp.path().join("dir-link")).unwrap();
604 std::os::unix::fs::symlink("subdir/../target.txt", temp.path().join("nested-link"))
605 .unwrap();
606 std::os::unix::fs::symlink("missing.txt", temp.path().join("broken-link")).unwrap();
607
608 let volume = FsVolume::new(
609 "/assets".to_string(),
610 temp.path().to_path_buf(),
611 BTreeSet::new(),
612 BTreeSet::from([temp.path().to_path_buf()]),
613 crate::package::include_everything_walker(),
614 );
615
616 let file_link: PathSegments = "/file-link".parse().unwrap();
617 let file_link_meta = volume.metadata(&file_link).unwrap();
618 assert!(file_link_meta.is_symlink());
619 assert_eq!(
620 match file_link_meta {
621 Metadata::Symlink { target_length, .. } => target_length,
622 _ => unreachable!(),
623 },
624 "target.txt".len()
625 );
626 assert_eq!(
627 volume.read_link(&file_link).unwrap().0,
628 "target.txt".to_string()
629 );
630 assert!(volume.read_file(&file_link).is_none());
631
632 let broken_link: PathSegments = "/broken-link".parse().unwrap();
633 let broken_link_meta = volume.metadata(&broken_link).unwrap();
634 assert!(broken_link_meta.is_symlink());
635 assert_eq!(
636 volume.read_link(&broken_link).unwrap().0,
637 "missing.txt".to_string()
638 );
639
640 let nested_link: PathSegments = "/nested-link".parse().unwrap();
641 assert_eq!(
642 volume.read_link(&nested_link).unwrap().0,
643 "subdir/../target.txt".to_string()
644 );
645
646 let entries = volume.read_dir(&PathSegments::ROOT).unwrap();
647 assert!(
648 entries
649 .iter()
650 .any(|(name, _, meta)| name.as_str() == "broken-link" && meta.is_symlink())
651 );
652
653 let dir = volume.as_directory_tree(Strictness::Strict).unwrap();
654 for link in ["broken-link", "dir-link", "file-link", "nested-link"] {
655 assert!(matches!(
656 dir.children.get(&PathSegment::parse(link).unwrap()),
657 Some(v3::write::DirEntry::Symlink(_))
658 ));
659 }
660 }
661
662 #[test]
663 fn directory_tree_propagates_errors() {
664 let temp = TempDir::new().unwrap();
665
666 let base_dir = temp.path().to_path_buf();
668
669 let non_existent_path = base_dir.join("non_existent_dir");
671
672 let result = directory_tree(
674 std::iter::once(non_existent_path.clone()),
675 &base_dir,
676 crate::package::wasmer_ignore_walker(),
677 );
678
679 assert!(result.is_err());
681 let error = result.unwrap_err();
682 assert!(
683 error.to_string().contains("Unable to add"),
684 "Error message should indicate failure to add path: {error}",
685 );
686 assert!(
687 error.to_string().contains("non_existent_dir"),
688 "Error message should include the problematic path: {error}",
689 );
690 }
691}