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 crate::DirEntry;
488 use std::collections::BTreeMap;
489 use std::convert::TryFrom;
490 use tokio::io::AsyncReadExt;
491 use wasmer_package::utils::from_bytes;
492 use webc::PathSegment;
493
494 const PYTHON_WEBC: &[u8] =
495 include_bytes!("../../../wasmer-test-files/examples/python-0.1.0.wasmer");
496
497 fn symlink_fs() -> WebcVolumeFileSystem {
498 let timestamps = webc::v3::Timestamps::default();
499 let dir = webc::v3::write::Directory::new(
500 BTreeMap::from_iter([
501 (
502 PathSegment::parse("target.txt").unwrap(),
503 webc::v3::write::DirEntry::File(webc::v3::write::FileEntry::borrowed(
504 b"target", timestamps,
505 )),
506 ),
507 (
508 PathSegment::parse("link").unwrap(),
509 webc::v3::write::DirEntry::Symlink(webc::v3::write::SymlinkEntry::borrowed(
510 "target.txt",
511 timestamps,
512 )),
513 ),
514 ]),
515 timestamps,
516 );
517 let manifest = webc::metadata::Manifest::default();
518 let mut writer = webc::v3::write::Writer::new(webc::v3::ChecksumAlgorithm::Sha256)
519 .write_manifest(&manifest)
520 .unwrap()
521 .write_atoms(BTreeMap::new())
522 .unwrap();
523 writer.write_volume("atom", dir).unwrap();
524 let webc = writer.finish(webc::v3::SignatureAlgorithm::None).unwrap();
525 let container = from_bytes(webc).unwrap();
526 let volume = container.volumes()["atom"].clone();
527
528 WebcVolumeFileSystem::new(volume)
529 }
530
531 #[test]
532 fn normalize_paths() {
533 let inputs: Vec<(&str, &[&str])> = vec![
534 ("/", &[]),
535 ("/path/to/", &["path", "to"]),
536 ("/path/to/file.txt", &["path", "to", "file.txt"]),
537 ("/folder/..", &[]),
538 ("/.hidden", &[".hidden"]),
539 ("/folder/../../../../../../../file.txt", &["file.txt"]),
540 #[cfg(windows)]
541 (r"C:\path\to\file.txt", &["path", "to", "file.txt"]),
542 ];
543
544 for (path, expected) in inputs {
545 let normalized = normalize(path.as_ref()).unwrap();
546 assert_eq!(normalized, expected.to_path_segments().unwrap());
547 }
548 }
549
550 #[test]
551 #[cfg_attr(not(windows), ignore = "Only works with PathBuf's Windows logic")]
552 fn normalize_windows_paths() {
553 let inputs: Vec<(&str, &[&str])> = vec![
554 (r"C:\path\to\file.txt", &["path", "to", "file.txt"]),
555 (r"C:/path/to/file.txt", &["path", "to", "file.txt"]),
556 (r"\\system07\C$\", &[]),
557 (r"c:\temp\test-file.txt", &["temp", "test-file.txt"]),
558 (
559 r"\\127.0.0.1\c$\temp\test-file.txt",
560 &["temp", "test-file.txt"],
561 ),
562 (r"\\.\c:\temp\test-file.txt", &["temp", "test-file.txt"]),
563 (r"\\?\c:\temp\test-file.txt", &["temp", "test-file.txt"]),
564 (
565 r"\\127.0.0.1\c$\temp\test-file.txt",
566 &["temp", "test-file.txt"],
567 ),
568 (
569 r"\\.\Volume{b75e2c83-0000-0000-0000-602f00000000}\temp\test-file.txt",
570 &["temp", "test-file.txt"],
571 ),
572 ];
573
574 for (path, expected) in inputs {
575 let normalized = normalize(path.as_ref()).unwrap();
576 assert_eq!(normalized, expected.to_path_segments().unwrap(), "{}", path);
577 }
578 }
579
580 #[test]
581 fn invalid_paths() {
582 let paths = [".", "..", "./file.txt", ""];
583
584 for path in paths {
585 assert!(normalize(path.as_ref()).is_err(), "{}", path);
586 }
587 }
588
589 #[test]
590 fn symlink_metadata_and_readlink() {
591 let fs = symlink_fs();
592
593 let link = fs.symlink_metadata("/link".as_ref()).unwrap();
594 assert!(link.ft.is_symlink());
595 assert_eq!(link.len(), "target.txt".len() as u64);
596 assert_eq!(
597 fs.readlink("/link".as_ref()).unwrap(),
598 Path::new("target.txt")
599 );
600 assert!(
603 fs.new_open_options().read(true).open("/link").is_ok(),
604 "open() should follow the symlink to its target file",
605 );
606
607 let entries: Vec<_> = fs
608 .read_dir("/".as_ref())
609 .unwrap()
610 .map(|entry| entry.unwrap())
611 .collect();
612 let link_entry = entries
613 .iter()
614 .find(|entry| entry.path == Path::new("/link"))
615 .unwrap();
616 assert!(link_entry.metadata().unwrap().ft.is_symlink());
617
618 assert_eq!(
619 fs.readlink("/target.txt".as_ref()).unwrap_err(),
620 FsError::InvalidInput
621 );
622 assert_eq!(
623 fs.readlink("/missing".as_ref()).unwrap_err(),
624 FsError::EntryNotFound
625 );
626 }
627
628 fn follow_symlinks_fs() -> WebcVolumeFileSystem {
642 use webc::v3::write::{DirEntry, Directory, FileEntry, SymlinkEntry};
643
644 let ts = webc::v3::Timestamps::default();
645 let file = |bytes: &'static [u8]| DirEntry::File(FileEntry::borrowed(bytes, ts));
646 let link = |target: &'static str| DirEntry::Symlink(SymlinkEntry::borrowed(target, ts));
647 let seg = |s: &str| PathSegment::parse(s).unwrap();
648
649 let git_core = Directory::new(
650 BTreeMap::from_iter([(seg("git"), link("../../bin/real.wasm"))]),
651 ts,
652 );
653 let libexec = Directory::new(
654 BTreeMap::from_iter([(seg("git-core"), DirEntry::Dir(git_core))]),
655 ts,
656 );
657 let bin = Directory::new(
658 BTreeMap::from_iter([(seg("real.wasm"), file(b"\0asm-real"))]),
659 ts,
660 );
661 let root = Directory::new(
662 BTreeMap::from_iter([
663 (seg("a.txt"), file(b"content-a")),
664 (seg("rel"), link("a.txt")),
665 (seg("hop1"), link("hop2")),
666 (seg("hop2"), link("a.txt")),
667 (seg("loop1"), link("loop2")),
668 (seg("loop2"), link("loop1")),
669 (seg("bin"), DirEntry::Dir(bin)),
670 (seg("libexec"), DirEntry::Dir(libexec)),
671 (seg("bindir"), link("bin")),
672 ]),
673 ts,
674 );
675
676 let manifest = webc::metadata::Manifest::default();
677 let mut writer = webc::v3::write::Writer::new(webc::v3::ChecksumAlgorithm::Sha256)
678 .write_manifest(&manifest)
679 .unwrap()
680 .write_atoms(BTreeMap::new())
681 .unwrap();
682 writer.write_volume("atom", root).unwrap();
683 let webc = writer.finish(webc::v3::SignatureAlgorithm::None).unwrap();
684 let container = from_bytes(webc).unwrap();
685 let volume = container.volumes()["atom"].clone();
686
687 WebcVolumeFileSystem::new(volume)
688 }
689
690 #[tokio::test]
691 async fn open_follows_symlinks() {
692 let fs = follow_symlinks_fs();
693
694 async fn read(fs: &WebcVolumeFileSystem, path: &str) -> Vec<u8> {
695 let mut f = fs
696 .new_open_options()
697 .read(true)
698 .open(path)
699 .unwrap_or_else(|e| panic!("opening {path}: {e:?}"));
700 let mut buffer = Vec::new();
701 f.read_to_end(&mut buffer).await.unwrap();
702 buffer
703 }
704
705 assert_eq!(read(&fs, "/rel").await, b"content-a");
707 assert_eq!(read(&fs, "/hop1").await, b"content-a");
708 assert_eq!(read(&fs, "/libexec/git-core/git").await, b"\0asm-real");
710 assert_eq!(read(&fs, "/bindir/real.wasm").await, b"\0asm-real");
712 }
713
714 #[test]
715 fn open_symlink_loop_fails() {
716 let fs = follow_symlinks_fs();
717
718 assert_eq!(
719 fs.new_open_options().read(true).open("/loop1").unwrap_err(),
720 FsError::InvalidInput,
721 );
722 }
723
724 #[test]
725 fn open_symlink_chain_respects_max_depth() {
726 use webc::v3::write::{DirEntry, Directory, FileEntry, SymlinkEntry};
727
728 fn chain_fs(n: usize) -> WebcVolumeFileSystem {
731 let ts = webc::v3::Timestamps::default();
732 let seg = |s: &str| PathSegment::parse(s).unwrap();
733
734 let mut children = BTreeMap::new();
735 children.insert(
736 seg("target.txt"),
737 DirEntry::File(FileEntry::borrowed(b"target", ts)),
738 );
739 for i in 0..n {
740 let target = if i + 1 == n {
741 "target.txt".to_string()
742 } else {
743 format!("link{}", i + 1)
744 };
745 children.insert(
746 seg(&format!("link{i}")),
747 DirEntry::Symlink(SymlinkEntry::owned(target, ts)),
748 );
749 }
750
751 let manifest = webc::metadata::Manifest::default();
752 let mut writer = webc::v3::write::Writer::new(webc::v3::ChecksumAlgorithm::Sha256)
753 .write_manifest(&manifest)
754 .unwrap()
755 .write_atoms(BTreeMap::new())
756 .unwrap();
757 writer
758 .write_volume("atom", Directory::new(children, ts))
759 .unwrap();
760 let webc = writer.finish(webc::v3::SignatureAlgorithm::None).unwrap();
761 let container = from_bytes(webc).unwrap();
762 WebcVolumeFileSystem::new(container.volumes()["atom"].clone())
763 }
764
765 assert!(
768 chain_fs(40)
769 .new_open_options()
770 .read(true)
771 .open("/link0")
772 .is_ok(),
773 "a 40-link chain should resolve",
774 );
775
776 assert_eq!(
778 chain_fs(41)
779 .new_open_options()
780 .read(true)
781 .open("/link0")
782 .unwrap_err(),
783 FsError::InvalidInput,
784 );
785 }
786
787 #[test]
788 fn metadata_follows_symlinks() {
789 let fs = symlink_fs();
790
791 let target = fs.metadata("/link".as_ref()).unwrap();
793 assert!(target.is_file());
794 assert_eq!(target.len(), "target".len() as u64);
795
796 let link = fs.symlink_metadata("/link".as_ref()).unwrap();
798 assert!(link.ft.is_symlink());
799 assert_eq!(link.len(), "target.txt".len() as u64);
800 }
801
802 #[test]
803 fn read_dir_follows_symlinked_directory() {
804 let fs = follow_symlinks_fs();
805
806 assert!(fs.metadata("/bindir".as_ref()).unwrap().is_dir());
808
809 let entries: Vec<_> = fs
812 .read_dir("/bindir".as_ref())
813 .unwrap()
814 .map(|entry| entry.unwrap().path)
815 .collect();
816 assert_eq!(entries, vec![PathBuf::from("/bindir/real.wasm")]);
817 }
818
819 #[test]
820 fn symlink_metadata_follows_intermediate_symlinks() {
821 let fs = follow_symlinks_fs();
822
823 let meta = fs.symlink_metadata("/bindir/real.wasm".as_ref()).unwrap();
825 assert!(meta.is_file());
826 assert_eq!(meta.len(), b"\0asm-real".len() as u64);
827
828 assert!(
830 fs.symlink_metadata("/bindir".as_ref())
831 .unwrap()
832 .ft
833 .is_symlink()
834 );
835 }
836
837 #[tokio::test]
838 async fn write_ops_reach_permission_denied_through_symlinked_parent() {
839 let fs = follow_symlinks_fs();
840
841 assert_eq!(
844 fs.create_dir("/bindir/new".as_ref()).unwrap_err(),
845 FsError::PermissionDenied,
846 );
847 assert_eq!(
848 fs.rename("/bin/real.wasm".as_ref(), "/bindir/new.wasm".as_ref())
849 .await
850 .unwrap_err(),
851 FsError::PermissionDenied,
852 );
853 }
854
855 #[test]
856 fn mount_all_volumes_in_python() {
857 let container = from_bytes(PYTHON_WEBC).unwrap();
858
859 let fs = WebcVolumeFileSystem::mount_all(&container);
860
861 let lib_meta = fs.metadata("/lib/python3.6/".as_ref()).unwrap();
863 assert!(lib_meta.is_dir());
864 }
865
866 #[test]
867 fn read_dir() {
868 let container = from_bytes(PYTHON_WEBC).unwrap();
869 let volumes = container.volumes();
870 let volume = volumes["atom"].clone();
871
872 let fs = WebcVolumeFileSystem::new(volume);
873
874 let entries: Vec<_> = fs
875 .read_dir("/lib".as_ref())
876 .unwrap()
877 .map(|r| r.unwrap())
878 .collect();
879
880 let modified = get_modified(None);
881 let expected = vec![
882 DirEntry {
883 path: "/lib/.DS_Store".into(),
884 metadata: Ok(Metadata {
885 ft: FileType {
886 file: true,
887 ..Default::default()
888 },
889 accessed: 0,
890 created: 0,
891 modified,
892 len: 6148,
893 }),
894 },
895 DirEntry {
896 path: "/lib/Parser".into(),
897 metadata: Ok(Metadata {
898 ft: FileType {
899 dir: true,
900 ..Default::default()
901 },
902 accessed: 0,
903 created: 0,
904 modified,
905 len: 0,
906 }),
907 },
908 DirEntry {
909 path: "/lib/python.wasm".into(),
910 metadata: Ok(crate::Metadata {
911 ft: crate::FileType {
912 file: true,
913 ..Default::default()
914 },
915 accessed: 0,
916 created: 0,
917 modified,
918 len: 4694941,
919 }),
920 },
921 DirEntry {
922 path: "/lib/python3.6".into(),
923 metadata: Ok(crate::Metadata {
924 ft: crate::FileType {
925 dir: true,
926 ..Default::default()
927 },
928 accessed: 0,
929 created: 0,
930 modified,
931 len: 0,
932 }),
933 },
934 ];
935 assert_eq!(entries, expected);
936 }
937
938 #[test]
939 fn metadata() {
940 let container = from_bytes(PYTHON_WEBC).unwrap();
941 let volumes = container.volumes();
942 let volume = volumes["atom"].clone();
943
944 let fs = WebcVolumeFileSystem::new(volume);
945
946 let modified = get_modified(None);
947 let python_wasm = crate::Metadata {
948 ft: crate::FileType {
949 file: true,
950 ..Default::default()
951 },
952 accessed: 0,
953 created: 0,
954 modified,
955 len: 4694941,
956 };
957 assert_eq!(
958 fs.metadata("/lib/python.wasm".as_ref()).unwrap(),
959 python_wasm,
960 );
961 assert_eq!(
962 fs.metadata("/../../../../lib/python.wasm".as_ref())
963 .unwrap(),
964 python_wasm,
965 );
966 assert_eq!(
967 fs.metadata("/lib/python3.6/../python3.6/../python.wasm".as_ref())
968 .unwrap(),
969 python_wasm,
970 );
971 assert_eq!(
972 fs.metadata("/lib/python3.6".as_ref()).unwrap(),
973 crate::Metadata {
974 ft: crate::FileType {
975 dir: true,
976 ..Default::default()
977 },
978 accessed: 0,
979 created: 0,
980 modified,
981 len: 0,
982 },
983 );
984 assert_eq!(
985 fs.metadata("/this/does/not/exist".as_ref()).unwrap_err(),
986 FsError::EntryNotFound
987 );
988 }
989
990 #[tokio::test]
991 async fn file_opener() {
992 let container = from_bytes(PYTHON_WEBC).unwrap();
993 let volumes = container.volumes();
994 let volume = volumes["atom"].clone();
995
996 let fs = WebcVolumeFileSystem::new(volume);
997
998 assert_eq!(
999 fs.new_open_options()
1000 .create(true)
1001 .write(true)
1002 .open("/file.txt")
1003 .unwrap_err(),
1004 FsError::PermissionDenied,
1005 );
1006 assert_eq!(
1007 fs.new_open_options().read(true).open("/lib").unwrap_err(),
1008 FsError::NotAFile,
1009 );
1010 assert_eq!(
1011 fs.new_open_options()
1012 .read(true)
1013 .open("/this/does/not/exist.txt")
1014 .unwrap_err(),
1015 FsError::EntryNotFound,
1016 );
1017
1018 let mut f = fs
1020 .new_open_options()
1021 .read(true)
1022 .open("/lib/python.wasm")
1023 .unwrap();
1024 let mut buffer = Vec::new();
1025 f.read_to_end(&mut buffer).await.unwrap();
1026 assert!(buffer.starts_with(b"\0asm"));
1027 assert_eq!(
1028 fs.metadata("/lib/python.wasm".as_ref()).unwrap().len(),
1029 u64::try_from(buffer.len()).unwrap(),
1030 );
1031 }
1032
1033 #[test]
1034 fn remove_dir_is_not_allowed() {
1035 let container = from_bytes(PYTHON_WEBC).unwrap();
1036 let volumes = container.volumes();
1037 let volume = volumes["atom"].clone();
1038
1039 let fs = WebcVolumeFileSystem::new(volume);
1040
1041 assert_eq!(
1042 fs.remove_dir("/lib".as_ref()).unwrap_err(),
1043 FsError::PermissionDenied,
1044 );
1045 assert_eq!(
1046 fs.remove_dir("/this/does/not/exist".as_ref()).unwrap_err(),
1047 FsError::EntryNotFound,
1048 );
1049 assert_eq!(
1050 fs.remove_dir("/lib/python.wasm".as_ref()).unwrap_err(),
1051 FsError::BaseNotDirectory,
1052 );
1053 }
1054
1055 #[test]
1056 fn remove_file_is_not_allowed() {
1057 let container = from_bytes(PYTHON_WEBC).unwrap();
1058 let volumes = container.volumes();
1059 let volume = volumes["atom"].clone();
1060
1061 let fs = WebcVolumeFileSystem::new(volume);
1062
1063 assert_eq!(
1064 fs.remove_file("/lib".as_ref()).unwrap_err(),
1065 FsError::NotAFile,
1066 );
1067 assert_eq!(
1068 fs.remove_file("/this/does/not/exist".as_ref()).unwrap_err(),
1069 FsError::EntryNotFound,
1070 );
1071 assert_eq!(
1072 fs.remove_file("/lib/python.wasm".as_ref()).unwrap_err(),
1073 FsError::PermissionDenied,
1074 );
1075 }
1076
1077 #[test]
1078 fn create_dir_is_not_allowed() {
1079 let container = from_bytes(PYTHON_WEBC).unwrap();
1080 let volumes = container.volumes();
1081 let volume = volumes["atom"].clone();
1082
1083 let fs = WebcVolumeFileSystem::new(volume);
1084
1085 assert_eq!(
1086 fs.create_dir("/lib".as_ref()).unwrap_err(),
1087 FsError::AlreadyExists,
1088 );
1089 assert_eq!(
1090 fs.create_dir("/this/does/not/exist".as_ref()).unwrap_err(),
1091 FsError::BaseNotDirectory,
1092 );
1093 assert_eq!(
1094 fs.create_dir("/lib/nested/".as_ref()).unwrap_err(),
1095 FsError::PermissionDenied,
1096 );
1097 }
1098
1099 #[tokio::test]
1100 async fn rename_is_not_allowed() {
1101 let container = from_bytes(PYTHON_WEBC).unwrap();
1102 let volumes = container.volumes();
1103 let volume = volumes["atom"].clone();
1104
1105 let fs = WebcVolumeFileSystem::new(volume);
1106
1107 assert_eq!(
1108 fs.rename("/lib".as_ref(), "/other".as_ref())
1109 .await
1110 .unwrap_err(),
1111 FsError::PermissionDenied,
1112 );
1113 assert_eq!(
1114 fs.rename("/this/does/not/exist".as_ref(), "/another".as_ref())
1115 .await
1116 .unwrap_err(),
1117 FsError::EntryNotFound,
1118 );
1119 assert_eq!(
1120 fs.rename("/lib/python.wasm".as_ref(), "/lib/another.wasm".as_ref())
1121 .await
1122 .unwrap_err(),
1123 FsError::PermissionDenied,
1124 );
1125 }
1126}