1use std::{
2 convert::{TryFrom, TryInto},
3 io::Cursor,
4 path::{Path, PathBuf},
5 pin::Pin,
6 result::Result,
7 task::Poll,
8};
9
10use futures::future::BoxFuture;
11use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite};
12use webc::{
13 Container, Metadata as WebcMetadata, PathSegment, PathSegmentError, PathSegments,
14 ToPathSegments, Volume, compat::SharedBytes,
15};
16
17use crate::{
18 DirEntry, EmptyFileSystem, FileOpener, FileSystem, FileType, FsError, Metadata,
19 OpenOptionsConfig, OverlayFileSystem, ReadDir, VirtualFile,
20};
21
22#[derive(Debug, Clone)]
23pub struct WebcVolumeFileSystem {
24 volume: Volume,
25}
26
27impl WebcVolumeFileSystem {
28 pub fn new(volume: Volume) -> Self {
29 WebcVolumeFileSystem { volume }
30 }
31
32 pub fn volume(&self) -> &Volume {
33 &self.volume
34 }
35
36 fn resolve_symlinks(
41 &self,
42 path: &Path,
43 follow_trailing: bool,
44 ) -> Result<(PathBuf, Option<WebcMetadata>), FsError> {
45 const MAX_SYMLINK_DEPTH: usize = 40;
48
49 let mut current = normalize(path).map_err(|_| FsError::InvalidInput)?;
50
51 for _ in 0..=MAX_SYMLINK_DEPTH {
52 match self.volume().metadata(¤t) {
53 Some(meta) if !meta.is_symlink() => {
55 return Ok((PathBuf::from(current.to_string()), Some(meta)));
56 }
57 Some(meta) => {
60 if !follow_trailing {
61 return Ok((PathBuf::from(current.to_string()), Some(meta)));
63 }
64 let segments: Vec<PathSegment> = current.iter().cloned().collect();
65 current = self.expand_symlink_at(&segments, segments.len() - 1)?;
66 }
67 None => match self.expand_first_symlink(¤t)? {
70 Some(next) => current = next,
71 None => return Ok((PathBuf::from(current.to_string()), None)), },
73 }
74 }
75
76 Err(FsError::InvalidInput)
78 }
79
80 fn expand_first_symlink(&self, path: &PathSegments) -> Result<Option<PathSegments>, FsError> {
84 let segments: Vec<PathSegment> = path.iter().cloned().collect();
85 for i in 0..segments.len() {
86 let prefix: PathSegments = segments[..=i].iter().cloned().collect();
87 match self.volume().metadata(&prefix) {
88 Some(meta) if meta.is_symlink() => {
89 return Ok(Some(self.expand_symlink_at(&segments, i)?));
90 }
91 Some(_) => {}
93 None => return Ok(None),
95 }
96 }
97 Ok(None)
98 }
99
100 fn expand_symlink_at(
104 &self,
105 segments: &[PathSegment],
106 i: usize,
107 ) -> Result<PathSegments, FsError> {
108 let link: PathSegments = segments[..=i].iter().cloned().collect();
109 let (target, _) = self
110 .volume()
111 .read_link(&link)
112 .ok_or(FsError::EntryNotFound)?;
113
114 let mut combined = String::new();
117 if target.starts_with('/') {
118 combined.push_str(&target);
119 } else {
120 for segment in &segments[..i] {
122 combined.push('/');
123 combined.push_str(segment.as_str());
124 }
125 combined.push('/');
126 combined.push_str(&target);
127 }
128 for segment in &segments[i + 1..] {
129 combined.push('/');
130 combined.push_str(segment.as_str());
131 }
132
133 normalize(Path::new(&combined)).map_err(|_| FsError::InvalidInput)
134 }
135
136 pub fn mount_all(
139 container: &Container,
140 ) -> OverlayFileSystem<EmptyFileSystem, Vec<WebcVolumeFileSystem>> {
141 let mut filesystems = Vec::new();
142
143 for volume in container.volumes().into_values() {
144 filesystems.push(WebcVolumeFileSystem::new(volume));
145 }
146
147 OverlayFileSystem::new(EmptyFileSystem::default(), filesystems)
148 }
149}
150
151impl FileSystem for WebcVolumeFileSystem {
152 fn readlink(&self, path: &Path) -> crate::Result<PathBuf> {
153 let path = normalize(path).map_err(|_| FsError::InvalidInput)?;
154
155 match self.volume().metadata(&path) {
156 Some(meta) if !meta.is_symlink() => Err(FsError::InvalidInput),
157 Some(_) => self
158 .volume()
159 .read_link(&path)
160 .map(|(target, _)| PathBuf::from(target))
161 .ok_or(FsError::EntryNotFound),
162 None => Err(FsError::EntryNotFound),
163 }
164 }
165
166 fn read_dir(&self, path: &Path) -> Result<crate::ReadDir, FsError> {
167 let (resolved, meta) = self.resolve_symlinks(path, true)?;
169 let meta = meta.map(compat_meta).ok_or(FsError::EntryNotFound)?;
170
171 if !meta.is_dir() {
172 return Err(FsError::BaseNotDirectory);
173 }
174
175 let display_path = normalize(path).map_err(|_| FsError::InvalidInput)?;
178 let resolved = normalize(resolved.as_path()).map_err(|_| FsError::InvalidInput)?;
179
180 let mut entries = Vec::new();
181
182 for (name, _, meta) in self
183 .volume()
184 .read_dir(&resolved)
185 .ok_or(FsError::EntryNotFound)?
186 {
187 let path = PathBuf::from(display_path.join(name).to_string());
188 entries.push(DirEntry {
189 path,
190 metadata: Ok(compat_meta(meta)),
191 });
192 }
193
194 Ok(ReadDir::new(entries))
195 }
196
197 fn create_dir(&self, path: &Path) -> Result<(), FsError> {
198 if self.symlink_metadata(path).is_ok() {
200 return Err(FsError::AlreadyExists);
201 }
202
203 let parent = path.parent().unwrap_or_else(|| Path::new("/"));
206
207 match self.metadata(parent) {
208 Ok(parent_meta) if parent_meta.is_dir() => {
209 Err(FsError::PermissionDenied)
212 }
213 Ok(_) | Err(FsError::EntryNotFound) => Err(FsError::BaseNotDirectory),
214 Err(other) => Err(other),
215 }
216 }
217
218 fn remove_dir(&self, path: &Path) -> Result<(), FsError> {
219 let meta = self.symlink_metadata(path)?;
222
223 if !meta.is_dir() {
225 return Err(FsError::BaseNotDirectory);
226 }
227
228 Err(FsError::PermissionDenied)
230 }
231
232 fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<(), FsError>> {
233 Box::pin(async {
234 let _ = self.symlink_metadata(from)?;
236
237 let dest_parent = to.parent().unwrap_or_else(|| Path::new("/"));
240 let parent_meta = self.metadata(dest_parent)?;
241 if !parent_meta.is_dir() {
242 return Err(FsError::BaseNotDirectory);
243 }
244
245 Err(FsError::PermissionDenied)
247 })
248 }
249
250 fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
251 let (_, meta) = self.resolve_symlinks(path, true)?;
253 meta.map(compat_meta).ok_or(FsError::EntryNotFound)
254 }
255
256 fn symlink_metadata(&self, path: &Path) -> crate::Result<Metadata> {
257 let (_, meta) = self.resolve_symlinks(path, false)?;
259 meta.map(compat_meta).ok_or(FsError::EntryNotFound)
260 }
261
262 fn remove_file(&self, path: &Path) -> Result<(), FsError> {
263 let meta = self.symlink_metadata(path)?;
266
267 if !meta.is_file() {
268 return Err(FsError::NotAFile);
269 }
270
271 Err(FsError::PermissionDenied)
272 }
273
274 fn new_open_options(&self) -> crate::OpenOptions<'_> {
275 crate::OpenOptions::new(self)
276 }
277}
278
279impl FileOpener for WebcVolumeFileSystem {
280 fn open(
281 &self,
282 path: &Path,
283 conf: &OpenOptionsConfig,
284 ) -> crate::Result<Box<dyn crate::VirtualFile + Send + Sync + 'static>> {
285 let (resolved, resolved_meta) = self.resolve_symlinks(path, true)?;
288 let path = resolved.as_path();
289 if let Some(parent) = path.parent() {
290 let parent_meta = self.metadata(parent)?;
291 if !parent_meta.is_dir() {
292 return Err(FsError::BaseNotDirectory);
293 }
294 }
295
296 let timestamps = match resolved_meta {
297 Some(m) if m.is_file() => m.timestamps(),
298 Some(_) => return Err(FsError::NotAFile),
299 None if conf.create() || conf.create_new() => {
300 return Err(FsError::PermissionDenied);
302 }
303 None => return Err(FsError::EntryNotFound),
304 };
305
306 match self.volume().read_file(path) {
307 Some((bytes, _)) => Ok(Box::new(File {
308 timestamps,
309 content: Cursor::new(bytes),
310 })),
311 None => {
312 Err(FsError::UnknownError)
315 }
316 }
317 }
318}
319
320#[derive(Debug, Clone, PartialEq)]
321struct File {
322 timestamps: Option<webc::Timestamps>,
323 content: Cursor<SharedBytes>,
324}
325
326impl VirtualFile for File {
327 fn last_accessed(&self) -> u64 {
328 0
329 }
330
331 fn last_modified(&self) -> u64 {
332 self.timestamps
333 .map(|t| t.modified())
334 .unwrap_or_else(|| get_modified(None))
335 }
336
337 fn created_time(&self) -> u64 {
338 0
339 }
340
341 fn size(&self) -> u64 {
342 self.content.get_ref().len().try_into().unwrap()
343 }
344
345 fn set_len(&mut self, _new_size: u64) -> crate::Result<()> {
346 Err(FsError::PermissionDenied)
347 }
348
349 fn unlink(&mut self) -> crate::Result<()> {
350 Err(FsError::PermissionDenied)
351 }
352
353 fn poll_read_ready(
354 self: Pin<&mut Self>,
355 _cx: &mut std::task::Context<'_>,
356 ) -> Poll<std::io::Result<usize>> {
357 let bytes_remaining =
358 self.content.get_ref().len() - usize::try_from(self.content.position()).unwrap();
359 Poll::Ready(Ok(bytes_remaining))
360 }
361
362 fn poll_write_ready(
363 self: Pin<&mut Self>,
364 _cx: &mut std::task::Context<'_>,
365 ) -> Poll<std::io::Result<usize>> {
366 Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into()))
367 }
368
369 fn as_owned_buffer(&self) -> Option<SharedBytes> {
370 Some(self.content.get_ref().clone())
371 }
372}
373
374impl AsyncRead for File {
375 fn poll_read(
376 mut self: Pin<&mut Self>,
377 cx: &mut std::task::Context<'_>,
378 buf: &mut tokio::io::ReadBuf<'_>,
379 ) -> Poll<std::io::Result<()>> {
380 AsyncRead::poll_read(Pin::new(&mut self.content), cx, buf)
381 }
382}
383
384impl AsyncSeek for File {
385 fn start_seek(mut self: Pin<&mut Self>, position: std::io::SeekFrom) -> std::io::Result<()> {
386 AsyncSeek::start_seek(Pin::new(&mut self.content), position)
387 }
388
389 fn poll_complete(
390 mut self: Pin<&mut Self>,
391 cx: &mut std::task::Context<'_>,
392 ) -> Poll<std::io::Result<u64>> {
393 AsyncSeek::poll_complete(Pin::new(&mut self.content), cx)
394 }
395}
396
397impl AsyncWrite for File {
398 fn poll_write(
399 self: Pin<&mut Self>,
400 _cx: &mut std::task::Context<'_>,
401 _buf: &[u8],
402 ) -> Poll<Result<usize, std::io::Error>> {
403 Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into()))
404 }
405
406 fn poll_flush(
407 self: Pin<&mut Self>,
408 _cx: &mut std::task::Context<'_>,
409 ) -> Poll<Result<(), std::io::Error>> {
410 Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into()))
411 }
412
413 fn poll_shutdown(
414 self: Pin<&mut Self>,
415 _cx: &mut std::task::Context<'_>,
416 ) -> Poll<Result<(), std::io::Error>> {
417 Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into()))
418 }
419}
420
421fn get_modified(timestamps: Option<webc::Timestamps>) -> u64 {
426 let modified = timestamps.map(|t| t.modified()).unwrap_or_default();
427 modified.max(1_000_000_000)
429}
430
431fn compat_meta(meta: WebcMetadata) -> Metadata {
432 match meta {
433 WebcMetadata::Dir { timestamps } => Metadata {
434 ft: FileType {
435 dir: true,
436 ..Default::default()
437 },
438 modified: get_modified(timestamps),
439 ..Default::default()
440 },
441 WebcMetadata::File {
442 length, timestamps, ..
443 } => Metadata {
444 ft: FileType {
445 file: true,
446 ..Default::default()
447 },
448 len: length.try_into().unwrap(),
449 modified: get_modified(timestamps),
450 ..Default::default()
451 },
452 WebcMetadata::Symlink {
453 target_length,
454 timestamps,
455 } => Metadata {
456 ft: FileType {
457 symlink: true,
458 ..Default::default()
459 },
460 len: target_length.try_into().unwrap(),
461 modified: get_modified(timestamps),
462 ..Default::default()
463 },
464 }
465}
466
467fn normalize(path: &Path) -> Result<PathSegments, PathSegmentError> {
470 let result = path.to_path_segments();
472
473 if let Err(e) = &result {
474 tracing::debug!(
475 error = e as &dyn std::error::Error,
476 path=%path.display(),
477 "Unable to normalize a path",
478 );
479 }
480
481 result
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use std::collections::BTreeMap;
488 use std::convert::TryFrom;
489 use tokio::io::AsyncReadExt;
490 use wasmer_package::utils::from_bytes;
491 use webc::PathSegment;
492
493 const PYTHON_WEBC: &[u8] =
494 include_bytes!("../../../wasmer-test-files/examples/python--python@3.13.5.webc");
495
496 fn symlink_fs() -> WebcVolumeFileSystem {
497 let timestamps = webc::v3::Timestamps::default();
498 let dir = webc::v3::write::Directory::new(
499 BTreeMap::from_iter([
500 (
501 PathSegment::parse("target.txt").unwrap(),
502 webc::v3::write::DirEntry::File(webc::v3::write::FileEntry::borrowed(
503 b"target", timestamps,
504 )),
505 ),
506 (
507 PathSegment::parse("link").unwrap(),
508 webc::v3::write::DirEntry::Symlink(webc::v3::write::SymlinkEntry::borrowed(
509 "target.txt",
510 timestamps,
511 )),
512 ),
513 ]),
514 timestamps,
515 );
516 let manifest = webc::metadata::Manifest::default();
517 let mut writer = webc::v3::write::Writer::new(webc::v3::ChecksumAlgorithm::Sha256)
518 .write_manifest(&manifest)
519 .unwrap()
520 .write_atoms(BTreeMap::new())
521 .unwrap();
522 writer.write_volume("atom", dir).unwrap();
523 let webc = writer.finish(webc::v3::SignatureAlgorithm::None).unwrap();
524 let container = from_bytes(webc).unwrap();
525 let volume = container.volumes()["atom"].clone();
526
527 WebcVolumeFileSystem::new(volume)
528 }
529
530 #[test]
531 fn normalize_paths() {
532 let inputs: Vec<(&str, &[&str])> = vec![
533 ("/", &[]),
534 ("/path/to/", &["path", "to"]),
535 ("/path/to/file.txt", &["path", "to", "file.txt"]),
536 ("/folder/..", &[]),
537 ("/.hidden", &[".hidden"]),
538 ("/folder/../../../../../../../file.txt", &["file.txt"]),
539 #[cfg(windows)]
540 (r"C:\path\to\file.txt", &["path", "to", "file.txt"]),
541 ];
542
543 for (path, expected) in inputs {
544 let normalized = normalize(path.as_ref()).unwrap();
545 assert_eq!(normalized, expected.to_path_segments().unwrap());
546 }
547 }
548
549 #[test]
550 #[cfg_attr(not(windows), ignore = "Only works with PathBuf's Windows logic")]
551 fn normalize_windows_paths() {
552 let inputs: Vec<(&str, &[&str])> = vec![
553 (r"C:\path\to\file.txt", &["path", "to", "file.txt"]),
554 (r"C:/path/to/file.txt", &["path", "to", "file.txt"]),
555 (r"\\system07\C$\", &[]),
556 (r"c:\temp\test-file.txt", &["temp", "test-file.txt"]),
557 (
558 r"\\127.0.0.1\c$\temp\test-file.txt",
559 &["temp", "test-file.txt"],
560 ),
561 (r"\\.\c:\temp\test-file.txt", &["temp", "test-file.txt"]),
562 (r"\\?\c:\temp\test-file.txt", &["temp", "test-file.txt"]),
563 (
564 r"\\127.0.0.1\c$\temp\test-file.txt",
565 &["temp", "test-file.txt"],
566 ),
567 (
568 r"\\.\Volume{b75e2c83-0000-0000-0000-602f00000000}\temp\test-file.txt",
569 &["temp", "test-file.txt"],
570 ),
571 ];
572
573 for (path, expected) in inputs {
574 let normalized = normalize(path.as_ref()).unwrap();
575 assert_eq!(normalized, expected.to_path_segments().unwrap(), "{}", path);
576 }
577 }
578
579 #[test]
580 fn invalid_paths() {
581 let paths = [".", "..", "./file.txt", ""];
582
583 for path in paths {
584 assert!(normalize(path.as_ref()).is_err(), "{}", path);
585 }
586 }
587
588 #[test]
589 fn symlink_metadata_and_readlink() {
590 let fs = symlink_fs();
591
592 let link = fs.symlink_metadata("/link".as_ref()).unwrap();
593 assert!(link.ft.is_symlink());
594 assert_eq!(link.len(), "target.txt".len() as u64);
595 assert_eq!(
596 fs.readlink("/link".as_ref()).unwrap(),
597 Path::new("target.txt")
598 );
599 assert!(
602 fs.new_open_options().read(true).open("/link").is_ok(),
603 "open() should follow the symlink to its target file",
604 );
605
606 let entries: Vec<_> = fs
607 .read_dir("/".as_ref())
608 .unwrap()
609 .map(|entry| entry.unwrap())
610 .collect();
611 let link_entry = entries
612 .iter()
613 .find(|entry| entry.path == Path::new("/link"))
614 .unwrap();
615 assert!(link_entry.metadata().unwrap().ft.is_symlink());
616
617 assert_eq!(
618 fs.readlink("/target.txt".as_ref()).unwrap_err(),
619 FsError::InvalidInput
620 );
621 assert_eq!(
622 fs.readlink("/missing".as_ref()).unwrap_err(),
623 FsError::EntryNotFound
624 );
625 }
626
627 fn follow_symlinks_fs() -> WebcVolumeFileSystem {
641 use webc::v3::write::{DirEntry, Directory, FileEntry, SymlinkEntry};
642
643 let ts = webc::v3::Timestamps::default();
644 let file = |bytes: &'static [u8]| DirEntry::File(FileEntry::borrowed(bytes, ts));
645 let link = |target: &'static str| DirEntry::Symlink(SymlinkEntry::borrowed(target, ts));
646 let seg = |s: &str| PathSegment::parse(s).unwrap();
647
648 let git_core = Directory::new(
649 BTreeMap::from_iter([(seg("git"), link("../../bin/real.wasm"))]),
650 ts,
651 );
652 let libexec = Directory::new(
653 BTreeMap::from_iter([(seg("git-core"), DirEntry::Dir(git_core))]),
654 ts,
655 );
656 let bin = Directory::new(
657 BTreeMap::from_iter([(seg("real.wasm"), file(b"\0asm-real"))]),
658 ts,
659 );
660 let root = Directory::new(
661 BTreeMap::from_iter([
662 (seg("a.txt"), file(b"content-a")),
663 (seg("rel"), link("a.txt")),
664 (seg("hop1"), link("hop2")),
665 (seg("hop2"), link("a.txt")),
666 (seg("loop1"), link("loop2")),
667 (seg("loop2"), link("loop1")),
668 (seg("bin"), DirEntry::Dir(bin)),
669 (seg("libexec"), DirEntry::Dir(libexec)),
670 (seg("bindir"), link("bin")),
671 ]),
672 ts,
673 );
674
675 let manifest = webc::metadata::Manifest::default();
676 let mut writer = webc::v3::write::Writer::new(webc::v3::ChecksumAlgorithm::Sha256)
677 .write_manifest(&manifest)
678 .unwrap()
679 .write_atoms(BTreeMap::new())
680 .unwrap();
681 writer.write_volume("atom", root).unwrap();
682 let webc = writer.finish(webc::v3::SignatureAlgorithm::None).unwrap();
683 let container = from_bytes(webc).unwrap();
684 let volume = container.volumes()["atom"].clone();
685
686 WebcVolumeFileSystem::new(volume)
687 }
688
689 #[tokio::test]
690 async fn open_follows_symlinks() {
691 let fs = follow_symlinks_fs();
692
693 async fn read(fs: &WebcVolumeFileSystem, path: &str) -> Vec<u8> {
694 let mut f = fs
695 .new_open_options()
696 .read(true)
697 .open(path)
698 .unwrap_or_else(|e| panic!("opening {path}: {e:?}"));
699 let mut buffer = Vec::new();
700 f.read_to_end(&mut buffer).await.unwrap();
701 buffer
702 }
703
704 assert_eq!(read(&fs, "/rel").await, b"content-a");
706 assert_eq!(read(&fs, "/hop1").await, b"content-a");
707 assert_eq!(read(&fs, "/libexec/git-core/git").await, b"\0asm-real");
709 assert_eq!(read(&fs, "/bindir/real.wasm").await, b"\0asm-real");
711 }
712
713 #[test]
714 fn open_symlink_loop_fails() {
715 let fs = follow_symlinks_fs();
716
717 assert_eq!(
718 fs.new_open_options().read(true).open("/loop1").unwrap_err(),
719 FsError::InvalidInput,
720 );
721 }
722
723 #[test]
724 fn open_symlink_chain_respects_max_depth() {
725 use webc::v3::write::{DirEntry, Directory, FileEntry, SymlinkEntry};
726
727 fn chain_fs(n: usize) -> WebcVolumeFileSystem {
730 let ts = webc::v3::Timestamps::default();
731 let seg = |s: &str| PathSegment::parse(s).unwrap();
732
733 let mut children = BTreeMap::new();
734 children.insert(
735 seg("target.txt"),
736 DirEntry::File(FileEntry::borrowed(b"target", ts)),
737 );
738 for i in 0..n {
739 let target = if i + 1 == n {
740 "target.txt".to_string()
741 } else {
742 format!("link{}", i + 1)
743 };
744 children.insert(
745 seg(&format!("link{i}")),
746 DirEntry::Symlink(SymlinkEntry::owned(target, ts)),
747 );
748 }
749
750 let manifest = webc::metadata::Manifest::default();
751 let mut writer = webc::v3::write::Writer::new(webc::v3::ChecksumAlgorithm::Sha256)
752 .write_manifest(&manifest)
753 .unwrap()
754 .write_atoms(BTreeMap::new())
755 .unwrap();
756 writer
757 .write_volume("atom", Directory::new(children, ts))
758 .unwrap();
759 let webc = writer.finish(webc::v3::SignatureAlgorithm::None).unwrap();
760 let container = from_bytes(webc).unwrap();
761 WebcVolumeFileSystem::new(container.volumes()["atom"].clone())
762 }
763
764 assert!(
767 chain_fs(40)
768 .new_open_options()
769 .read(true)
770 .open("/link0")
771 .is_ok(),
772 "a 40-link chain should resolve",
773 );
774
775 assert_eq!(
777 chain_fs(41)
778 .new_open_options()
779 .read(true)
780 .open("/link0")
781 .unwrap_err(),
782 FsError::InvalidInput,
783 );
784 }
785
786 #[test]
787 fn metadata_follows_symlinks() {
788 let fs = symlink_fs();
789
790 let target = fs.metadata("/link".as_ref()).unwrap();
792 assert!(target.is_file());
793 assert_eq!(target.len(), "target".len() as u64);
794
795 let link = fs.symlink_metadata("/link".as_ref()).unwrap();
797 assert!(link.ft.is_symlink());
798 assert_eq!(link.len(), "target.txt".len() as u64);
799 }
800
801 #[test]
802 fn read_dir_follows_symlinked_directory() {
803 let fs = follow_symlinks_fs();
804
805 assert!(fs.metadata("/bindir".as_ref()).unwrap().is_dir());
807
808 let entries: Vec<_> = fs
811 .read_dir("/bindir".as_ref())
812 .unwrap()
813 .map(|entry| entry.unwrap().path)
814 .collect();
815 assert_eq!(entries, vec![PathBuf::from("/bindir/real.wasm")]);
816 }
817
818 #[test]
819 fn symlink_metadata_follows_intermediate_symlinks() {
820 let fs = follow_symlinks_fs();
821
822 let meta = fs.symlink_metadata("/bindir/real.wasm".as_ref()).unwrap();
824 assert!(meta.is_file());
825 assert_eq!(meta.len(), b"\0asm-real".len() as u64);
826
827 assert!(
829 fs.symlink_metadata("/bindir".as_ref())
830 .unwrap()
831 .ft
832 .is_symlink()
833 );
834 }
835
836 #[tokio::test]
837 async fn write_ops_reach_permission_denied_through_symlinked_parent() {
838 let fs = follow_symlinks_fs();
839
840 assert_eq!(
843 fs.create_dir("/bindir/new".as_ref()).unwrap_err(),
844 FsError::PermissionDenied,
845 );
846 assert_eq!(
847 fs.rename("/bin/real.wasm".as_ref(), "/bindir/new.wasm".as_ref())
848 .await
849 .unwrap_err(),
850 FsError::PermissionDenied,
851 );
852 }
853
854 #[test]
855 fn mount_all_volumes_in_python() {
856 let container = from_bytes(PYTHON_WEBC).unwrap();
857
858 let fs = WebcVolumeFileSystem::mount_all(&container);
859
860 let lib_meta = fs.metadata("/lib/python3.13/".as_ref()).unwrap();
862 assert!(lib_meta.is_dir());
863 }
864
865 #[test]
866 fn read_dir() {
867 let container = from_bytes(PYTHON_WEBC).unwrap();
868 let volume = container.volumes()["/root/usr/local"].clone();
869 let fs = WebcVolumeFileSystem::new(volume);
870
871 let entries: Vec<_> = fs
872 .read_dir("/lib".as_ref())
873 .unwrap()
874 .map(|r| r.unwrap())
875 .collect();
876
877 assert_eq!(
878 entries
879 .iter()
880 .map(|entry| entry.path.as_path())
881 .collect::<Vec<_>>(),
882 [Path::new("/lib/python3.13"), Path::new("/lib/wasm32-wasi"),],
883 );
884 assert!(
885 entries
886 .iter()
887 .all(|entry| entry.metadata().unwrap().is_dir())
888 );
889 }
890
891 #[tokio::test]
892 async fn file_opener() {
893 let container = from_bytes(PYTHON_WEBC).unwrap();
894 let volumes = container.volumes();
895 let volume = volumes["/root/usr/local"].clone();
896
897 let fs = WebcVolumeFileSystem::new(volume);
898
899 assert_eq!(
900 fs.new_open_options()
901 .create(true)
902 .write(true)
903 .open("/file.txt")
904 .unwrap_err(),
905 FsError::PermissionDenied,
906 );
907 assert_eq!(
908 fs.new_open_options().read(true).open("/lib").unwrap_err(),
909 FsError::NotAFile,
910 );
911 assert_eq!(
912 fs.new_open_options()
913 .read(true)
914 .open("/this/does/not/exist.txt")
915 .unwrap_err(),
916 FsError::EntryNotFound,
917 );
918
919 let mut f = fs
921 .new_open_options()
922 .read(true)
923 .open("/bin/python3.wasm")
924 .unwrap();
925 let mut buffer = Vec::new();
926 f.read_to_end(&mut buffer).await.unwrap();
927 assert!(buffer.starts_with(b"\0asm"));
928 assert_eq!(
929 fs.metadata("/bin/python3.wasm".as_ref()).unwrap().len(),
930 u64::try_from(buffer.len()).unwrap(),
931 );
932 }
933
934 #[test]
935 fn remove_dir_is_not_allowed() {
936 let container = from_bytes(PYTHON_WEBC).unwrap();
937 let volumes = container.volumes();
938 let volume = volumes["/root/usr/local"].clone();
939
940 let fs = WebcVolumeFileSystem::new(volume);
941
942 assert_eq!(
943 fs.remove_dir("/lib".as_ref()).unwrap_err(),
944 FsError::PermissionDenied,
945 );
946 assert_eq!(
947 fs.remove_dir("/this/does/not/exist".as_ref()).unwrap_err(),
948 FsError::EntryNotFound,
949 );
950 assert_eq!(
951 fs.remove_dir("/bin/python3.wasm".as_ref()).unwrap_err(),
952 FsError::BaseNotDirectory,
953 );
954 }
955
956 #[test]
957 fn remove_file_is_not_allowed() {
958 let container = from_bytes(PYTHON_WEBC).unwrap();
959 let volumes = container.volumes();
960 let volume = volumes["/root/usr/local"].clone();
961
962 let fs = WebcVolumeFileSystem::new(volume);
963
964 assert_eq!(
965 fs.remove_file("/lib".as_ref()).unwrap_err(),
966 FsError::NotAFile,
967 );
968 assert_eq!(
969 fs.remove_file("/this/does/not/exist".as_ref()).unwrap_err(),
970 FsError::EntryNotFound,
971 );
972 assert_eq!(
973 fs.remove_file("/bin/python3.wasm".as_ref()).unwrap_err(),
974 FsError::PermissionDenied,
975 );
976 }
977
978 #[test]
979 fn create_dir_is_not_allowed() {
980 let container = from_bytes(PYTHON_WEBC).unwrap();
981 let volumes = container.volumes();
982 let volume = volumes["/root/usr/local"].clone();
983
984 let fs = WebcVolumeFileSystem::new(volume);
985
986 assert_eq!(
987 fs.create_dir("/lib".as_ref()).unwrap_err(),
988 FsError::AlreadyExists,
989 );
990 assert_eq!(
991 fs.create_dir("/this/does/not/exist".as_ref()).unwrap_err(),
992 FsError::BaseNotDirectory,
993 );
994 assert_eq!(
995 fs.create_dir("/lib/nested/".as_ref()).unwrap_err(),
996 FsError::PermissionDenied,
997 );
998 }
999
1000 #[tokio::test]
1001 async fn rename_is_not_allowed() {
1002 let container = from_bytes(PYTHON_WEBC).unwrap();
1003 let volumes = container.volumes();
1004 let volume = volumes["/root/usr/local"].clone();
1005
1006 let fs = WebcVolumeFileSystem::new(volume);
1007
1008 assert_eq!(
1009 fs.rename("/lib".as_ref(), "/other".as_ref())
1010 .await
1011 .unwrap_err(),
1012 FsError::PermissionDenied,
1013 );
1014 assert_eq!(
1015 fs.rename("/this/does/not/exist".as_ref(), "/another".as_ref())
1016 .await
1017 .unwrap_err(),
1018 FsError::EntryNotFound,
1019 );
1020 assert_eq!(
1021 fs.rename("/bin/python3.wasm".as_ref(), "/lib/another.wasm".as_ref())
1022 .await
1023 .unwrap_err(),
1024 FsError::PermissionDenied,
1025 );
1026 }
1027}