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