1#![allow(unsafe_code)]
2
3use super::vfs::{
4 normalize_path, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat,
5 VirtualUtimeSpec,
6};
7use crate::package_format::{
8 generated::v1::{self, TarEntryKind},
9 parse_aospkg_header, validate_mount_range, AospkgHeader,
10};
11use memmap2::Mmap;
12use std::collections::{BTreeMap, BTreeSet, HashMap};
13use std::fs::File;
14use std::hash::{Hash, Hasher};
15use std::io;
16use std::path::{Path, PathBuf};
17use std::sync::{Arc, Mutex, OnceLock, Weak};
18
19const MAX_TAR_INDEX_ENTRIES: usize = 200_000;
20const MAX_TAR_CACHE_ARCHIVES: usize = 64;
21const MAX_TAR_SYMLINKS: usize = 40;
22const MAX_TAR_REALPATH_CACHE_ENTRIES: usize = 32_768;
23
24#[derive(Clone)]
47pub struct TarFileSystem {
48 archive: Arc<CachedTarArchive>,
49 root: String,
50}
51
52impl TarFileSystem {
53 pub fn open(path: impl AsRef<Path>) -> VfsResult<Self> {
54 Self::open_at(path, "/")
55 }
56
57 pub fn open_at(path: impl AsRef<Path>, root: &str) -> VfsResult<Self> {
58 let path = path.as_ref().to_path_buf();
59 let archive = cached_archive(path)?;
60 let root = normalize_path(root);
61 let node = archive.node(&root)?;
62 if !matches!(node.kind, TarNodeKind::Directory) {
63 return Err(VfsError::new(
64 "ENOTDIR",
65 format!("tar mount root is not a directory: {root}"),
66 ));
67 }
68 Ok(Self { archive, root })
69 }
70
71 #[doc(hidden)]
72 pub fn archive_ptr(&self) -> usize {
73 Arc::as_ptr(&self.archive) as usize
74 }
75
76 pub fn source_path(&self) -> &Path {
77 &self.archive.path
78 }
79
80 pub fn archive_root(&self) -> &str {
81 &self.root
82 }
83
84 fn to_archive_path(&self, path: &str) -> String {
85 let normalized = normalize_path(path);
86 if self.root == "/" {
87 normalized
88 } else if normalized == "/" {
89 self.root.clone()
90 } else {
91 normalize_path(&format!(
92 "{}/{}",
93 self.root,
94 normalized.trim_start_matches('/')
95 ))
96 }
97 }
98
99 fn to_guest_path(&self, archive_path: &str) -> VfsResult<String> {
100 if self.root == "/" {
101 return Ok(archive_path.to_owned());
102 }
103 if archive_path == self.root {
104 return Ok(String::from("/"));
105 }
106 let prefix = format!("{}/", self.root.trim_end_matches('/'));
107 let suffix = archive_path.strip_prefix(&prefix).ok_or_else(|| {
108 VfsError::new(
109 "EXDEV",
110 format!("tar symlink resolved outside mounted subtree: {archive_path}"),
111 )
112 })?;
113 Ok(format!("/{suffix}"))
114 }
115
116 fn ensure_within_root(&self, archive_path: &str) -> VfsResult<()> {
117 if self.root == "/" || archive_path == self.root {
118 return Ok(());
119 }
120 let prefix = format!("{}/", self.root.trim_end_matches('/'));
121 if archive_path.starts_with(&prefix) {
122 Ok(())
123 } else {
124 Err(VfsError::new(
125 "EXDEV",
126 format!("tar path resolved outside mounted subtree: {archive_path}"),
127 ))
128 }
129 }
130
131 fn resolve_path(&self, path: &str, follow_final_symlink: bool) -> VfsResult<String> {
132 let normalized = self.to_archive_path(path);
133 if normalized == "/" {
134 return Ok(normalized);
135 }
136 if let Some(result) = self
137 .archive
138 .realpath_cache
139 .lock()
140 .expect("tar realpath cache poisoned")
141 .get(&(normalized.clone(), follow_final_symlink))
142 .cloned()
143 {
144 let resolved = result?;
145 self.ensure_within_root(&resolved)?;
146 return Ok(resolved);
147 }
148
149 let result = self.resolve_archive_path_uncached(&normalized, path, follow_final_symlink);
150 let mut cache = self
151 .archive
152 .realpath_cache
153 .lock()
154 .expect("tar realpath cache poisoned");
155 if cache.len() >= MAX_TAR_REALPATH_CACHE_ENTRIES {
156 cache.clear();
157 }
158 cache.insert((normalized, follow_final_symlink), result.clone());
159 drop(cache);
160
161 let resolved = result?;
162 self.ensure_within_root(&resolved)?;
163 Ok(resolved)
164 }
165
166 fn resolve_archive_path_uncached(
167 &self,
168 normalized: &str,
169 path: &str,
170 follow_final_symlink: bool,
171 ) -> VfsResult<String> {
172 let mut pending = path_components(normalized);
173 let mut current = String::from("/");
174 let mut followed = 0usize;
175
176 while let Some(component) = pending.pop_front() {
177 let candidate = join_path(¤t, &component);
178 let node = self.archive.node(&candidate)?;
179 let should_follow = follow_final_symlink || !pending.is_empty();
180
181 if should_follow {
182 if let TarNodeKind::Symlink { target } = &node.kind {
183 followed += 1;
184 if followed > MAX_TAR_SYMLINKS {
185 return Err(VfsError::new(
186 "ELOOP",
187 format!("too many levels of symbolic links, '{path}'"),
188 ));
189 }
190 let target_path = if target.starts_with('/') {
191 normalize_path(target)
192 } else {
193 normalize_path(&format!("{}/{}", parent_path(&candidate), target))
194 };
195 ensure_archive_path(&target_path)?;
196 let mut target_components = path_components(&target_path);
197 target_components.extend(pending);
198 pending = target_components;
199 current = String::from("/");
200 continue;
201 }
202 }
203
204 if !pending.is_empty() && !matches!(node.kind, TarNodeKind::Directory) {
205 return Err(VfsError::new(
206 "ENOTDIR",
207 format!("not a directory, realpath '{candidate}'"),
208 ));
209 }
210
211 current = candidate;
212 }
213
214 Ok(current)
215 }
216
217 fn readonly_error(op: &str, path: &str) -> VfsError {
218 VfsError::new("EROFS", format!("read-only tar filesystem, {op} '{path}'"))
219 }
220}
221
222impl VirtualFileSystem for TarFileSystem {
223 fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
224 let resolved = self.resolve_path(path, true)?;
225 let node = self.archive.node(&resolved)?;
226 let TarNodeKind::File { offset, size } = node.kind else {
227 return Err(if matches!(node.kind, TarNodeKind::Directory) {
228 VfsError::new(
229 "EISDIR",
230 format!("illegal operation on a directory, read '{path}'"),
231 )
232 } else {
233 VfsError::new("EINVAL", format!("not a regular file, read '{path}'"))
234 });
235 };
236 self.archive.validate_backing_file()?;
237 let range = validate_mount_range(&self.archive.container, offset, size)?;
238 Ok(self.archive.mmap[range].to_vec())
239 }
240
241 fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
242 Ok(self
243 .read_dir_with_types(path)?
244 .into_iter()
245 .map(|entry| entry.name)
246 .collect())
247 }
248
249 fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
250 let resolved = self.resolve_path(path, true)?;
251 let node = self.archive.node(&resolved)?;
252 if !matches!(node.kind, TarNodeKind::Directory) {
253 return Err(VfsError::new(
254 "ENOTDIR",
255 format!("not a directory, readdir '{path}'"),
256 ));
257 }
258
259 let children = self
260 .archive
261 .children
262 .get(&resolved)
263 .cloned()
264 .unwrap_or_default();
265 Ok(children
266 .into_iter()
267 .filter_map(|name| {
268 let child_path = join_path(&resolved, &name);
269 self.archive
270 .nodes
271 .get(&child_path)
272 .map(|child| VirtualDirEntry {
273 name,
274 is_directory: matches!(child.kind, TarNodeKind::Directory),
275 is_symbolic_link: matches!(child.kind, TarNodeKind::Symlink { .. }),
276 })
277 })
278 .collect())
279 }
280
281 fn write_file(&mut self, path: &str, _content: impl Into<Vec<u8>>) -> VfsResult<()> {
282 Err(Self::readonly_error("write", path))
283 }
284
285 fn create_dir(&mut self, path: &str) -> VfsResult<()> {
286 Err(Self::readonly_error("mkdir", path))
287 }
288
289 fn mkdir(&mut self, path: &str, _recursive: bool) -> VfsResult<()> {
290 Err(Self::readonly_error("mkdir", path))
291 }
292
293 fn exists(&self, path: &str) -> bool {
294 self.resolve_path(path, true)
295 .map(|resolved| self.archive.nodes.contains_key(&resolved))
296 .unwrap_or(false)
297 }
298
299 fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
300 let resolved = self.resolve_path(path, true)?;
301 Ok(self.archive.node(&resolved)?.stat())
302 }
303
304 fn remove_file(&mut self, path: &str) -> VfsResult<()> {
305 Err(Self::readonly_error("unlink", path))
306 }
307
308 fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
309 Err(Self::readonly_error("rmdir", path))
310 }
311
312 fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
313 Err(VfsError::new(
314 "EROFS",
315 format!("read-only tar filesystem, rename '{old_path}' to '{new_path}'"),
316 ))
317 }
318
319 fn realpath(&self, path: &str) -> VfsResult<String> {
320 let resolved = self.resolve_path(path, true)?;
321 self.to_guest_path(&resolved)
322 }
323
324 fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
325 Err(VfsError::new(
326 "EROFS",
327 format!("read-only tar filesystem, symlink '{link_path}' -> '{target}'"),
328 ))
329 }
330
331 fn read_link(&self, path: &str) -> VfsResult<String> {
332 let normalized = self.resolve_path(path, false)?;
333 match &self.archive.node(&normalized)?.kind {
334 TarNodeKind::Symlink { target } => Ok(target.clone()),
335 _ => Err(VfsError::new(
336 "EINVAL",
337 format!("not a symlink, readlink '{path}'"),
338 )),
339 }
340 }
341
342 fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
343 let normalized = self.resolve_path(path, false)?;
344 Ok(self.archive.node(&normalized)?.stat())
345 }
346
347 fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
348 Err(VfsError::new(
349 "EROFS",
350 format!("read-only tar filesystem, link '{old_path}' to '{new_path}'"),
351 ))
352 }
353
354 fn chmod(&mut self, path: &str, _mode: u32) -> VfsResult<()> {
355 Err(Self::readonly_error("chmod", path))
356 }
357
358 fn chown(&mut self, path: &str, _uid: u32, _gid: u32) -> VfsResult<()> {
359 Err(Self::readonly_error("chown", path))
360 }
361
362 fn utimes(&mut self, path: &str, _atime_ms: u64, _mtime_ms: u64) -> VfsResult<()> {
363 Err(Self::readonly_error("utimes", path))
364 }
365
366 fn utimes_spec(
367 &mut self,
368 path: &str,
369 _atime: VirtualUtimeSpec,
370 _mtime: VirtualUtimeSpec,
371 _follow_symlinks: bool,
372 ) -> VfsResult<()> {
373 Err(Self::readonly_error("utimes", path))
374 }
375
376 fn truncate(&mut self, path: &str, _length: u64) -> VfsResult<()> {
377 Err(Self::readonly_error("truncate", path))
378 }
379
380 fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
381 let resolved = self.resolve_path(path, true)?;
382 let node = self.archive.node(&resolved)?;
383 let TarNodeKind::File {
384 offset: file_offset,
385 size,
386 } = node.kind
387 else {
388 return Err(if matches!(node.kind, TarNodeKind::Directory) {
389 VfsError::new(
390 "EISDIR",
391 format!("illegal operation on a directory, pread '{path}'"),
392 )
393 } else {
394 VfsError::new("EINVAL", format!("not a regular file, pread '{path}'"))
395 });
396 };
397 if offset >= size {
398 return Ok(Vec::new());
399 }
400 let readable = (size - offset).min(length as u64);
401 self.archive.validate_backing_file()?;
402 let range = validate_mount_range(
403 &self.archive.container,
404 file_offset
405 .checked_add(offset)
406 .ok_or_else(|| VfsError::new("EOVERFLOW", "pread offset overflows u64"))?,
407 readable,
408 )?;
409 Ok(self.archive.mmap[range].to_vec())
410 }
411}
412
413struct CachedTarArchive {
414 path: PathBuf,
415 file: File,
416 mmap: Mmap,
417 container: AospkgHeader,
418 identity: FileIdentity,
419 nodes: HashMap<String, TarNode>,
420 children: BTreeMap<String, BTreeSet<String>>,
421 realpath_cache: Mutex<HashMap<(String, bool), VfsResult<String>>>,
422}
423
424impl CachedTarArchive {
425 fn node(&self, path: &str) -> VfsResult<&TarNode> {
426 self.nodes
427 .get(path)
428 .ok_or_else(|| VfsError::new("ENOENT", format!("no such file or directory, '{path}'")))
429 }
430
431 fn validate_backing_file(&self) -> VfsResult<()> {
432 let current = FileIdentity::from_file(&self.file)?;
433 if current != self.identity {
434 return Err(VfsError::new(
435 "ESTALE",
436 format!(
437 "tar archive backing file changed while mounted: {}",
438 self.path.display()
439 ),
440 ));
441 }
442 Ok(())
443 }
444}
445
446#[derive(Debug, Clone)]
447struct TarNode {
448 kind: TarNodeKind,
449 mode: u32,
450 uid: u32,
451 gid: u32,
452 mtime_ms: u64,
453 ino: u64,
454 dev: u64,
455}
456
457impl TarNode {
458 fn stat(&self) -> VirtualStat {
459 let size = match &self.kind {
460 TarNodeKind::File { size, .. } => *size,
461 TarNodeKind::Directory => 4096,
462 TarNodeKind::Symlink { target } => target.len() as u64,
463 };
464 VirtualStat {
465 mode: self.mode,
466 size,
467 blocks: size.div_ceil(512),
468 dev: self.dev,
469 rdev: 0,
470 is_directory: matches!(self.kind, TarNodeKind::Directory),
471 is_symbolic_link: matches!(self.kind, TarNodeKind::Symlink { .. }),
472 atime_ms: self.mtime_ms,
473 atime_nsec: 0,
474 mtime_ms: self.mtime_ms,
475 mtime_nsec: 0,
476 ctime_ms: self.mtime_ms,
477 ctime_nsec: 0,
478 birthtime_ms: self.mtime_ms,
479 ino: self.ino,
480 nlink: 1,
481 uid: self.uid,
482 gid: self.gid,
483 }
484 }
485}
486
487#[derive(Debug, Clone)]
488enum TarNodeKind {
489 File { offset: u64, size: u64 },
490 Directory,
491 Symlink { target: String },
492}
493
494#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
495struct FileIdentity {
496 len: u64,
497 dev: u64,
498 ino: u64,
499 mtime_nsec: i128,
500 ctime_nsec: i128,
501}
502
503impl FileIdentity {
504 fn from_file(file: &File) -> VfsResult<Self> {
505 Self::from_metadata(file.metadata().map_err(io_to_vfs)?)
506 }
507
508 fn from_metadata(metadata: std::fs::Metadata) -> VfsResult<Self> {
509 #[cfg(unix)]
510 let (dev, ino, mtime_nsec, ctime_nsec) = {
511 use std::os::unix::fs::MetadataExt;
512 (
513 metadata.dev(),
514 metadata.ino(),
515 unix_time_nsec(metadata.mtime(), metadata.mtime_nsec()),
516 unix_time_nsec(metadata.ctime(), metadata.ctime_nsec()),
517 )
518 };
519 #[cfg(not(unix))]
520 let (dev, ino, mtime_nsec, ctime_nsec) = {
521 let modified = metadata
522 .modified()
523 .ok()
524 .and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
525 .map(|duration| duration.as_nanos() as i128)
526 .unwrap_or_default();
527 (0, 0, modified, 0)
528 };
529 Ok(Self {
530 len: metadata.len(),
531 dev,
532 ino,
533 mtime_nsec,
534 ctime_nsec,
535 })
536 }
537}
538
539#[cfg(unix)]
540fn unix_time_nsec(sec: i64, nsec: i64) -> i128 {
541 i128::from(sec) * 1_000_000_000 + i128::from(nsec)
542}
543
544fn cached_archive(path: PathBuf) -> VfsResult<Arc<CachedTarArchive>> {
545 let file = File::open(&path).map_err(io_to_vfs)?;
546 let identity = FileIdentity::from_file(&file)?;
547 let cache = archive_cache();
548 let mut guard = cache
549 .lock()
550 .map_err(|_| VfsError::new("EIO", "tar archive cache mutex poisoned"))?;
551
552 if let Some(existing) = guard.archives.get(&identity).and_then(Weak::upgrade) {
559 if existing.path == path {
560 return Ok(existing);
561 }
562 return Err(VfsError::new(
563 "EINVAL",
564 format!(
565 "tar identity collision or moved source: identity {identity:?} already maps to {} not {}",
566 existing.path.display(),
567 path.display()
568 ),
569 ));
570 }
571
572 guard.archives.retain(|key, weak| {
573 let live = weak.strong_count() > 0;
574 if !live {
575 tracing::warn!(
576 identity = ?key,
577 "evicting unused tar archive cache entry"
578 );
579 }
580 live
581 });
582
583 if guard.archives.len() >= MAX_TAR_CACHE_ARCHIVES {
584 return Err(VfsError::new(
585 "ENOMEM",
586 format!(
587 "tar archive cache entries exceeded: {} entries > {} entries (raise via invariant.tarArchiveCacheEntries)",
588 guard.archives.len() + 1,
589 MAX_TAR_CACHE_ARCHIVES
590 ),
591 ));
592 }
593
594 let archive = Arc::new(load_archive(path, file, identity)?);
595 guard.archives.insert(identity, Arc::downgrade(&archive));
596 Ok(archive)
597}
598
599fn archive_cache() -> &'static Mutex<TarArchiveCache> {
600 static CACHE: OnceLock<Mutex<TarArchiveCache>> = OnceLock::new();
601 CACHE.get_or_init(|| Mutex::new(TarArchiveCache::default()))
602}
603
604#[derive(Default)]
605struct TarArchiveCache {
606 archives: BTreeMap<FileIdentity, Weak<CachedTarArchive>>,
607}
608
609fn load_archive(path: PathBuf, file: File, identity: FileIdentity) -> VfsResult<CachedTarArchive> {
610 let mmap = unsafe {
611 Mmap::map(&file)
617 }
618 .map_err(io_to_vfs)?;
619
620 let container = parse_aospkg_header(&mmap)?;
621 let index =
622 crate::package_format::versioned::decode_mount_index(&mmap[container.index.clone()])
623 .map_err(|error| {
624 VfsError::new("EINVAL", format!("decode .aospkg mount index: {error}"))
625 })?;
626 validate_sorted_entries(&index.tar_entries)?;
627
628 let mut nodes = HashMap::new();
629 let mut children = BTreeMap::<String, BTreeSet<String>>::new();
630 let dev = identity_device(&identity);
631 for (next_ino, entry) in (1u64..).zip(index.tar_entries) {
632 let path = entry.path;
633 ensure_archive_path(&path)?;
634 ensure_index_capacity(nodes.len() + 1)?;
635 if matches!(entry.kind, TarEntryKind::File) {
636 validate_mount_range(&container, entry.offset, entry.size)?;
637 }
638 let kind = match entry.kind {
639 TarEntryKind::File => TarNodeKind::File {
640 offset: entry.offset,
641 size: entry.size,
642 },
643 TarEntryKind::Directory => TarNodeKind::Directory,
644 TarEntryKind::Symlink => TarNodeKind::Symlink {
645 target: entry.link_target.ok_or_else(|| {
646 VfsError::new("EINVAL", format!("missing linkTarget for symlink {path}"))
647 })?,
648 },
649 };
650 let mtime_ms = u64::try_from(entry.mtime)
651 .map_err(|_| VfsError::new("EINVAL", format!("negative mtime for {path}")))?
652 .checked_mul(1_000)
653 .ok_or_else(|| VfsError::new("EOVERFLOW", format!("mtime overflows ms for {path}")))?;
654 nodes.insert(
655 path.clone(),
656 TarNode {
657 kind,
658 mode: entry.mode,
659 uid: entry.uid,
660 gid: entry.gid,
661 mtime_ms,
662 ino: next_ino,
663 dev,
664 },
665 );
666 add_child(&path, &mut children);
667 if matches!(
668 nodes.get(&path).map(|node| &node.kind),
669 Some(TarNodeKind::Directory)
670 ) {
671 children.entry(path).or_default();
672 }
673 }
674
675 Ok(CachedTarArchive {
676 path,
677 file,
678 mmap,
679 container,
680 identity,
681 nodes,
682 children,
683 realpath_cache: Mutex::new(HashMap::new()),
684 })
685}
686
687fn add_child(path: &str, children: &mut BTreeMap<String, BTreeSet<String>>) {
688 if path == "/" {
689 children.entry(String::from("/")).or_default();
690 return;
691 }
692 let parent = parent_path(path);
693 let name = basename(path);
694 children.entry(parent).or_default().insert(name);
695}
696
697fn ensure_index_capacity(observed: usize) -> VfsResult<()> {
698 if observed > MAX_TAR_INDEX_ENTRIES {
699 return Err(VfsError::new(
700 "ENOMEM",
701 format!(
702 "tar filesystem index entries exceeded: {observed} entries > {MAX_TAR_INDEX_ENTRIES} entries (raise via invariant.tarFilesystemIndexEntries)"
703 ),
704 ));
705 }
706 Ok(())
707}
708
709fn validate_sorted_entries(entries: &[v1::TarEntry]) -> VfsResult<()> {
710 for pair in entries.windows(2) {
711 let [previous, current] = pair else {
712 continue;
713 };
714 if previous.path >= current.path {
715 return Err(VfsError::new(
716 "EINVAL",
717 format!(
718 ".aospkg mount index is not sorted by canonical path: {:?} before {:?}",
719 previous.path, current.path
720 ),
721 ));
722 }
723 }
724 Ok(())
725}
726
727fn ensure_archive_path(path: &str) -> VfsResult<()> {
728 let normalized = normalize_path(path);
729 if normalized != path {
730 return Err(VfsError::new(
731 "EINVAL",
732 format!("path normalization mismatch in tar filesystem: {path}"),
733 ));
734 }
735 Ok(())
736}
737
738fn path_components(path: &str) -> std::collections::VecDeque<String> {
739 normalize_path(path)
740 .split('/')
741 .filter(|part| !part.is_empty())
742 .map(String::from)
743 .collect()
744}
745
746fn join_path(parent: &str, child: &str) -> String {
747 if parent == "/" {
748 format!("/{child}")
749 } else {
750 format!("{parent}/{child}")
751 }
752}
753
754fn parent_path(path: &str) -> String {
755 let normalized = normalize_path(path);
756 let parent = Path::new(&normalized)
757 .parent()
758 .unwrap_or_else(|| Path::new("/"));
759 let value = parent.to_string_lossy();
760 if value.is_empty() {
761 String::from("/")
762 } else {
763 value.into_owned()
764 }
765}
766
767fn basename(path: &str) -> String {
768 let normalized = normalize_path(path);
769 Path::new(&normalized)
770 .file_name()
771 .map(|name| name.to_string_lossy().into_owned())
772 .unwrap_or_else(|| String::from("/"))
773}
774
775fn identity_device(identity: &FileIdentity) -> u64 {
776 let mut hasher = std::collections::hash_map::DefaultHasher::new();
781 identity.dev.hash(&mut hasher);
782 identity.ino.hash(&mut hasher);
783 hasher.finish().max(1)
784}
785
786fn io_to_vfs(error: io::Error) -> VfsError {
787 let code = match error.kind() {
788 io::ErrorKind::NotFound => "ENOENT",
789 io::ErrorKind::PermissionDenied => "EACCES",
790 io::ErrorKind::AlreadyExists => "EEXIST",
791 io::ErrorKind::InvalidInput | io::ErrorKind::InvalidData => "EINVAL",
792 io::ErrorKind::UnexpectedEof => "EIO",
793 _ => "EIO",
794 };
795 VfsError::new(code, error.to_string())
796}