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, PathSegmentError, PathSegments, ToPathSegments, Volume,
14 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 pub fn mount_all(
39 container: &Container,
40 ) -> OverlayFileSystem<EmptyFileSystem, Vec<WebcVolumeFileSystem>> {
41 let mut filesystems = Vec::new();
42
43 for volume in container.volumes().into_values() {
44 filesystems.push(WebcVolumeFileSystem::new(volume));
45 }
46
47 OverlayFileSystem::new(EmptyFileSystem::default(), filesystems)
48 }
49}
50
51impl FileSystem for WebcVolumeFileSystem {
52 fn readlink(&self, path: &Path) -> crate::Result<PathBuf> {
53 let path = normalize(path).map_err(|_| FsError::InvalidInput)?;
54
55 match self.volume().metadata(&path) {
56 Some(meta) if !meta.is_symlink() => Err(FsError::InvalidInput),
57 Some(_) => self
58 .volume()
59 .read_link(&path)
60 .map(|(target, _)| PathBuf::from(target))
61 .ok_or(FsError::EntryNotFound),
62 None => Err(FsError::EntryNotFound),
63 }
64 }
65
66 fn read_dir(&self, path: &Path) -> Result<crate::ReadDir, FsError> {
67 let meta = self.metadata(path)?;
68
69 if !meta.is_dir() {
70 return Err(FsError::BaseNotDirectory);
71 }
72
73 let path = normalize(path).map_err(|_| FsError::InvalidInput)?;
74
75 let mut entries = Vec::new();
76
77 for (name, _, meta) in self
78 .volume()
79 .read_dir(&path)
80 .ok_or(FsError::EntryNotFound)?
81 {
82 let path = PathBuf::from(path.join(name).to_string());
83 entries.push(DirEntry {
84 path,
85 metadata: Ok(compat_meta(meta)),
86 });
87 }
88
89 Ok(ReadDir::new(entries))
90 }
91
92 fn create_dir(&self, path: &Path) -> Result<(), FsError> {
93 if self.metadata(path).is_ok() {
95 return Err(FsError::AlreadyExists);
96 }
97
98 let parent = path.parent().unwrap_or_else(|| Path::new("/"));
100
101 match self.metadata(parent) {
102 Ok(parent_meta) if parent_meta.is_dir() => {
103 Err(FsError::PermissionDenied)
106 }
107 Ok(_) | Err(FsError::EntryNotFound) => Err(FsError::BaseNotDirectory),
108 Err(other) => Err(other),
109 }
110 }
111
112 fn remove_dir(&self, path: &Path) -> Result<(), FsError> {
113 let meta = self.metadata(path)?;
115
116 if !meta.is_dir() {
118 return Err(FsError::BaseNotDirectory);
119 }
120
121 Err(FsError::PermissionDenied)
123 }
124
125 fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<(), FsError>> {
126 Box::pin(async {
127 let _ = self.metadata(from)?;
129
130 let dest_parent = to.parent().unwrap_or_else(|| Path::new("/"));
132 let parent_meta = self.metadata(dest_parent)?;
133 if !parent_meta.is_dir() {
134 return Err(FsError::BaseNotDirectory);
135 }
136
137 Err(FsError::PermissionDenied)
139 })
140 }
141
142 fn metadata(&self, path: &Path) -> Result<Metadata, FsError> {
143 let path = normalize(path).map_err(|_| FsError::InvalidInput)?;
144
145 self.volume()
146 .metadata(path)
147 .map(compat_meta)
148 .ok_or(FsError::EntryNotFound)
149 }
150
151 fn symlink_metadata(&self, path: &Path) -> crate::Result<Metadata> {
152 self.metadata(path)
153 }
154
155 fn remove_file(&self, path: &Path) -> Result<(), FsError> {
156 let meta = self.metadata(path)?;
157
158 if !meta.is_file() {
159 return Err(FsError::NotAFile);
160 }
161
162 Err(FsError::PermissionDenied)
163 }
164
165 fn new_open_options(&self) -> crate::OpenOptions<'_> {
166 crate::OpenOptions::new(self)
167 }
168}
169
170impl FileOpener for WebcVolumeFileSystem {
171 fn open(
172 &self,
173 path: &Path,
174 conf: &OpenOptionsConfig,
175 ) -> crate::Result<Box<dyn crate::VirtualFile + Send + Sync + 'static>> {
176 if let Some(parent) = path.parent() {
177 let parent_meta = self.metadata(parent)?;
178 if !parent_meta.is_dir() {
179 return Err(FsError::BaseNotDirectory);
180 }
181 }
182
183 let timestamps = match self.volume().metadata(path) {
184 Some(m) if m.is_file() => m.timestamps(),
185 Some(_) => return Err(FsError::NotAFile),
186 None if conf.create() || conf.create_new() => {
187 return Err(FsError::PermissionDenied);
189 }
190 None => return Err(FsError::EntryNotFound),
191 };
192
193 match self.volume().read_file(path) {
194 Some((bytes, _)) => Ok(Box::new(File {
195 timestamps,
196 content: Cursor::new(bytes),
197 })),
198 None => {
199 Err(FsError::UnknownError)
202 }
203 }
204 }
205}
206
207#[derive(Debug, Clone, PartialEq)]
208struct File {
209 timestamps: Option<webc::Timestamps>,
210 content: Cursor<SharedBytes>,
211}
212
213impl VirtualFile for File {
214 fn last_accessed(&self) -> u64 {
215 0
216 }
217
218 fn last_modified(&self) -> u64 {
219 self.timestamps
220 .map(|t| t.modified())
221 .unwrap_or_else(|| get_modified(None))
222 }
223
224 fn created_time(&self) -> u64 {
225 0
226 }
227
228 fn size(&self) -> u64 {
229 self.content.get_ref().len().try_into().unwrap()
230 }
231
232 fn set_len(&mut self, _new_size: u64) -> crate::Result<()> {
233 Err(FsError::PermissionDenied)
234 }
235
236 fn unlink(&mut self) -> crate::Result<()> {
237 Err(FsError::PermissionDenied)
238 }
239
240 fn poll_read_ready(
241 self: Pin<&mut Self>,
242 _cx: &mut std::task::Context<'_>,
243 ) -> Poll<std::io::Result<usize>> {
244 let bytes_remaining =
245 self.content.get_ref().len() - usize::try_from(self.content.position()).unwrap();
246 Poll::Ready(Ok(bytes_remaining))
247 }
248
249 fn poll_write_ready(
250 self: Pin<&mut Self>,
251 _cx: &mut std::task::Context<'_>,
252 ) -> Poll<std::io::Result<usize>> {
253 Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into()))
254 }
255
256 fn as_owned_buffer(&self) -> Option<SharedBytes> {
257 Some(self.content.get_ref().clone())
258 }
259}
260
261impl AsyncRead for File {
262 fn poll_read(
263 mut self: Pin<&mut Self>,
264 cx: &mut std::task::Context<'_>,
265 buf: &mut tokio::io::ReadBuf<'_>,
266 ) -> Poll<std::io::Result<()>> {
267 AsyncRead::poll_read(Pin::new(&mut self.content), cx, buf)
268 }
269}
270
271impl AsyncSeek for File {
272 fn start_seek(mut self: Pin<&mut Self>, position: std::io::SeekFrom) -> std::io::Result<()> {
273 AsyncSeek::start_seek(Pin::new(&mut self.content), position)
274 }
275
276 fn poll_complete(
277 mut self: Pin<&mut Self>,
278 cx: &mut std::task::Context<'_>,
279 ) -> Poll<std::io::Result<u64>> {
280 AsyncSeek::poll_complete(Pin::new(&mut self.content), cx)
281 }
282}
283
284impl AsyncWrite for File {
285 fn poll_write(
286 self: Pin<&mut Self>,
287 _cx: &mut std::task::Context<'_>,
288 _buf: &[u8],
289 ) -> Poll<Result<usize, std::io::Error>> {
290 Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into()))
291 }
292
293 fn poll_flush(
294 self: Pin<&mut Self>,
295 _cx: &mut std::task::Context<'_>,
296 ) -> Poll<Result<(), std::io::Error>> {
297 Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into()))
298 }
299
300 fn poll_shutdown(
301 self: Pin<&mut Self>,
302 _cx: &mut std::task::Context<'_>,
303 ) -> Poll<Result<(), std::io::Error>> {
304 Poll::Ready(Err(std::io::ErrorKind::PermissionDenied.into()))
305 }
306}
307
308fn get_modified(timestamps: Option<webc::Timestamps>) -> u64 {
313 let modified = timestamps.map(|t| t.modified()).unwrap_or_default();
314 modified.max(1_000_000_000)
316}
317
318fn compat_meta(meta: WebcMetadata) -> Metadata {
319 match meta {
320 WebcMetadata::Dir { timestamps } => Metadata {
321 ft: FileType {
322 dir: true,
323 ..Default::default()
324 },
325 modified: get_modified(timestamps),
326 ..Default::default()
327 },
328 WebcMetadata::File {
329 length, timestamps, ..
330 } => Metadata {
331 ft: FileType {
332 file: true,
333 ..Default::default()
334 },
335 len: length.try_into().unwrap(),
336 modified: get_modified(timestamps),
337 ..Default::default()
338 },
339 WebcMetadata::Symlink {
340 target_length,
341 timestamps,
342 } => Metadata {
343 ft: FileType {
344 symlink: true,
345 ..Default::default()
346 },
347 len: target_length.try_into().unwrap(),
348 modified: get_modified(timestamps),
349 ..Default::default()
350 },
351 }
352}
353
354fn normalize(path: &Path) -> Result<PathSegments, PathSegmentError> {
357 let result = path.to_path_segments();
359
360 if let Err(e) = &result {
361 tracing::debug!(
362 error = e as &dyn std::error::Error,
363 path=%path.display(),
364 "Unable to normalize a path",
365 );
366 }
367
368 result
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use crate::DirEntry;
375 use std::collections::BTreeMap;
376 use std::convert::TryFrom;
377 use tokio::io::AsyncReadExt;
378 use wasmer_package::utils::from_bytes;
379 use webc::PathSegment;
380
381 const PYTHON_WEBC: &[u8] =
382 include_bytes!("../../../wasmer-test-files/examples/python-0.1.0.wasmer");
383
384 fn symlink_fs() -> WebcVolumeFileSystem {
385 let timestamps = webc::v3::Timestamps::default();
386 let dir = webc::v3::write::Directory::new(
387 BTreeMap::from_iter([
388 (
389 PathSegment::parse("target.txt").unwrap(),
390 webc::v3::write::DirEntry::File(webc::v3::write::FileEntry::borrowed(
391 b"target", timestamps,
392 )),
393 ),
394 (
395 PathSegment::parse("link").unwrap(),
396 webc::v3::write::DirEntry::Symlink(webc::v3::write::SymlinkEntry::borrowed(
397 "target.txt",
398 timestamps,
399 )),
400 ),
401 ]),
402 timestamps,
403 );
404 let manifest = webc::metadata::Manifest::default();
405 let mut writer = webc::v3::write::Writer::new(webc::v3::ChecksumAlgorithm::Sha256)
406 .write_manifest(&manifest)
407 .unwrap()
408 .write_atoms(BTreeMap::new())
409 .unwrap();
410 writer.write_volume("atom", dir).unwrap();
411 let webc = writer.finish(webc::v3::SignatureAlgorithm::None).unwrap();
412 let container = from_bytes(webc).unwrap();
413 let volume = container.volumes()["atom"].clone();
414
415 WebcVolumeFileSystem::new(volume)
416 }
417
418 #[test]
419 fn normalize_paths() {
420 let inputs: Vec<(&str, &[&str])> = vec![
421 ("/", &[]),
422 ("/path/to/", &["path", "to"]),
423 ("/path/to/file.txt", &["path", "to", "file.txt"]),
424 ("/folder/..", &[]),
425 ("/.hidden", &[".hidden"]),
426 ("/folder/../../../../../../../file.txt", &["file.txt"]),
427 #[cfg(windows)]
428 (r"C:\path\to\file.txt", &["path", "to", "file.txt"]),
429 ];
430
431 for (path, expected) in inputs {
432 let normalized = normalize(path.as_ref()).unwrap();
433 assert_eq!(normalized, expected.to_path_segments().unwrap());
434 }
435 }
436
437 #[test]
438 #[cfg_attr(not(windows), ignore = "Only works with PathBuf's Windows logic")]
439 fn normalize_windows_paths() {
440 let inputs: Vec<(&str, &[&str])> = vec![
441 (r"C:\path\to\file.txt", &["path", "to", "file.txt"]),
442 (r"C:/path/to/file.txt", &["path", "to", "file.txt"]),
443 (r"\\system07\C$\", &[]),
444 (r"c:\temp\test-file.txt", &["temp", "test-file.txt"]),
445 (
446 r"\\127.0.0.1\c$\temp\test-file.txt",
447 &["temp", "test-file.txt"],
448 ),
449 (r"\\.\c:\temp\test-file.txt", &["temp", "test-file.txt"]),
450 (r"\\?\c:\temp\test-file.txt", &["temp", "test-file.txt"]),
451 (
452 r"\\127.0.0.1\c$\temp\test-file.txt",
453 &["temp", "test-file.txt"],
454 ),
455 (
456 r"\\.\Volume{b75e2c83-0000-0000-0000-602f00000000}\temp\test-file.txt",
457 &["temp", "test-file.txt"],
458 ),
459 ];
460
461 for (path, expected) in inputs {
462 let normalized = normalize(path.as_ref()).unwrap();
463 assert_eq!(normalized, expected.to_path_segments().unwrap(), "{}", path);
464 }
465 }
466
467 #[test]
468 fn invalid_paths() {
469 let paths = [".", "..", "./file.txt", ""];
470
471 for path in paths {
472 assert!(normalize(path.as_ref()).is_err(), "{}", path);
473 }
474 }
475
476 #[test]
477 fn symlink_metadata_and_readlink() {
478 let fs = symlink_fs();
479
480 let link = fs.symlink_metadata("/link".as_ref()).unwrap();
481 assert!(link.ft.is_symlink());
482 assert_eq!(link.len(), "target.txt".len() as u64);
483 assert_eq!(
484 fs.readlink("/link".as_ref()).unwrap(),
485 Path::new("target.txt")
486 );
487 assert_eq!(
488 fs.new_open_options().read(true).open("/link").unwrap_err(),
489 FsError::NotAFile,
490 );
491
492 let entries: Vec<_> = fs
493 .read_dir("/".as_ref())
494 .unwrap()
495 .map(|entry| entry.unwrap())
496 .collect();
497 let link_entry = entries
498 .iter()
499 .find(|entry| entry.path == Path::new("/link"))
500 .unwrap();
501 assert!(link_entry.metadata().unwrap().ft.is_symlink());
502
503 assert_eq!(
504 fs.readlink("/target.txt".as_ref()).unwrap_err(),
505 FsError::InvalidInput
506 );
507 assert_eq!(
508 fs.readlink("/missing".as_ref()).unwrap_err(),
509 FsError::EntryNotFound
510 );
511 }
512
513 #[test]
514 fn mount_all_volumes_in_python() {
515 let container = from_bytes(PYTHON_WEBC).unwrap();
516
517 let fs = WebcVolumeFileSystem::mount_all(&container);
518
519 let lib_meta = fs.metadata("/lib/python3.6/".as_ref()).unwrap();
521 assert!(lib_meta.is_dir());
522 }
523
524 #[test]
525 fn read_dir() {
526 let container = from_bytes(PYTHON_WEBC).unwrap();
527 let volumes = container.volumes();
528 let volume = volumes["atom"].clone();
529
530 let fs = WebcVolumeFileSystem::new(volume);
531
532 let entries: Vec<_> = fs
533 .read_dir("/lib".as_ref())
534 .unwrap()
535 .map(|r| r.unwrap())
536 .collect();
537
538 let modified = get_modified(None);
539 let expected = vec![
540 DirEntry {
541 path: "/lib/.DS_Store".into(),
542 metadata: Ok(Metadata {
543 ft: FileType {
544 file: true,
545 ..Default::default()
546 },
547 accessed: 0,
548 created: 0,
549 modified,
550 len: 6148,
551 }),
552 },
553 DirEntry {
554 path: "/lib/Parser".into(),
555 metadata: Ok(Metadata {
556 ft: FileType {
557 dir: true,
558 ..Default::default()
559 },
560 accessed: 0,
561 created: 0,
562 modified,
563 len: 0,
564 }),
565 },
566 DirEntry {
567 path: "/lib/python.wasm".into(),
568 metadata: Ok(crate::Metadata {
569 ft: crate::FileType {
570 file: true,
571 ..Default::default()
572 },
573 accessed: 0,
574 created: 0,
575 modified,
576 len: 4694941,
577 }),
578 },
579 DirEntry {
580 path: "/lib/python3.6".into(),
581 metadata: Ok(crate::Metadata {
582 ft: crate::FileType {
583 dir: true,
584 ..Default::default()
585 },
586 accessed: 0,
587 created: 0,
588 modified,
589 len: 0,
590 }),
591 },
592 ];
593 assert_eq!(entries, expected);
594 }
595
596 #[test]
597 fn metadata() {
598 let container = from_bytes(PYTHON_WEBC).unwrap();
599 let volumes = container.volumes();
600 let volume = volumes["atom"].clone();
601
602 let fs = WebcVolumeFileSystem::new(volume);
603
604 let modified = get_modified(None);
605 let python_wasm = crate::Metadata {
606 ft: crate::FileType {
607 file: true,
608 ..Default::default()
609 },
610 accessed: 0,
611 created: 0,
612 modified,
613 len: 4694941,
614 };
615 assert_eq!(
616 fs.metadata("/lib/python.wasm".as_ref()).unwrap(),
617 python_wasm,
618 );
619 assert_eq!(
620 fs.metadata("/../../../../lib/python.wasm".as_ref())
621 .unwrap(),
622 python_wasm,
623 );
624 assert_eq!(
625 fs.metadata("/lib/python3.6/../python3.6/../python.wasm".as_ref())
626 .unwrap(),
627 python_wasm,
628 );
629 assert_eq!(
630 fs.metadata("/lib/python3.6".as_ref()).unwrap(),
631 crate::Metadata {
632 ft: crate::FileType {
633 dir: true,
634 ..Default::default()
635 },
636 accessed: 0,
637 created: 0,
638 modified,
639 len: 0,
640 },
641 );
642 assert_eq!(
643 fs.metadata("/this/does/not/exist".as_ref()).unwrap_err(),
644 FsError::EntryNotFound
645 );
646 }
647
648 #[tokio::test]
649 async fn file_opener() {
650 let container = from_bytes(PYTHON_WEBC).unwrap();
651 let volumes = container.volumes();
652 let volume = volumes["atom"].clone();
653
654 let fs = WebcVolumeFileSystem::new(volume);
655
656 assert_eq!(
657 fs.new_open_options()
658 .create(true)
659 .write(true)
660 .open("/file.txt")
661 .unwrap_err(),
662 FsError::PermissionDenied,
663 );
664 assert_eq!(
665 fs.new_open_options().read(true).open("/lib").unwrap_err(),
666 FsError::NotAFile,
667 );
668 assert_eq!(
669 fs.new_open_options()
670 .read(true)
671 .open("/this/does/not/exist.txt")
672 .unwrap_err(),
673 FsError::EntryNotFound,
674 );
675
676 let mut f = fs
678 .new_open_options()
679 .read(true)
680 .open("/lib/python.wasm")
681 .unwrap();
682 let mut buffer = Vec::new();
683 f.read_to_end(&mut buffer).await.unwrap();
684 assert!(buffer.starts_with(b"\0asm"));
685 assert_eq!(
686 fs.metadata("/lib/python.wasm".as_ref()).unwrap().len(),
687 u64::try_from(buffer.len()).unwrap(),
688 );
689 }
690
691 #[test]
692 fn remove_dir_is_not_allowed() {
693 let container = from_bytes(PYTHON_WEBC).unwrap();
694 let volumes = container.volumes();
695 let volume = volumes["atom"].clone();
696
697 let fs = WebcVolumeFileSystem::new(volume);
698
699 assert_eq!(
700 fs.remove_dir("/lib".as_ref()).unwrap_err(),
701 FsError::PermissionDenied,
702 );
703 assert_eq!(
704 fs.remove_dir("/this/does/not/exist".as_ref()).unwrap_err(),
705 FsError::EntryNotFound,
706 );
707 assert_eq!(
708 fs.remove_dir("/lib/python.wasm".as_ref()).unwrap_err(),
709 FsError::BaseNotDirectory,
710 );
711 }
712
713 #[test]
714 fn remove_file_is_not_allowed() {
715 let container = from_bytes(PYTHON_WEBC).unwrap();
716 let volumes = container.volumes();
717 let volume = volumes["atom"].clone();
718
719 let fs = WebcVolumeFileSystem::new(volume);
720
721 assert_eq!(
722 fs.remove_file("/lib".as_ref()).unwrap_err(),
723 FsError::NotAFile,
724 );
725 assert_eq!(
726 fs.remove_file("/this/does/not/exist".as_ref()).unwrap_err(),
727 FsError::EntryNotFound,
728 );
729 assert_eq!(
730 fs.remove_file("/lib/python.wasm".as_ref()).unwrap_err(),
731 FsError::PermissionDenied,
732 );
733 }
734
735 #[test]
736 fn create_dir_is_not_allowed() {
737 let container = from_bytes(PYTHON_WEBC).unwrap();
738 let volumes = container.volumes();
739 let volume = volumes["atom"].clone();
740
741 let fs = WebcVolumeFileSystem::new(volume);
742
743 assert_eq!(
744 fs.create_dir("/lib".as_ref()).unwrap_err(),
745 FsError::AlreadyExists,
746 );
747 assert_eq!(
748 fs.create_dir("/this/does/not/exist".as_ref()).unwrap_err(),
749 FsError::BaseNotDirectory,
750 );
751 assert_eq!(
752 fs.create_dir("/lib/nested/".as_ref()).unwrap_err(),
753 FsError::PermissionDenied,
754 );
755 }
756
757 #[tokio::test]
758 async fn rename_is_not_allowed() {
759 let container = from_bytes(PYTHON_WEBC).unwrap();
760 let volumes = container.volumes();
761 let volume = volumes["atom"].clone();
762
763 let fs = WebcVolumeFileSystem::new(volume);
764
765 assert_eq!(
766 fs.rename("/lib".as_ref(), "/other".as_ref())
767 .await
768 .unwrap_err(),
769 FsError::PermissionDenied,
770 );
771 assert_eq!(
772 fs.rename("/this/does/not/exist".as_ref(), "/another".as_ref())
773 .await
774 .unwrap_err(),
775 FsError::EntryNotFound,
776 );
777 assert_eq!(
778 fs.rename("/lib/python.wasm".as_ref(), "/lib/another.wasm".as_ref())
779 .await
780 .unwrap_err(),
781 FsError::PermissionDenied,
782 );
783 }
784}