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