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