1#![forbid(unsafe_code)]
2
3use crate::inode_map::{
4 decode_fuse_ino, deleted_ino, journal_ino, metadata_ino, ro_ino, rw_ino, unallocated_ino,
5 InodeNamespace, FUSE_JOURNAL_INO, FUSE_METADATA_INO, FUSE_ORPHANS_INO, FUSE_ROOT_INO,
6 FUSE_RO_INO, FUSE_RW_INO, FUSE_SESSION_INO, FUSE_UNALLOCATED_INO,
7};
8use crate::session::Session;
9use crate::ForensicFs;
10use crate::{
11 DeletedMode, FsAllocation, FsBlockRange, FsEventType, FsFileType, FsMetadata, FsTimestamp,
12};
13use fuser::{
14 FileAttr, FileType, Filesystem, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty,
15 ReplyEntry, ReplyWrite, ReplyXattr, Request, TimeOrNow,
16};
17use std::cell::RefCell;
18use std::ffi::OsStr;
19use std::time::{Duration, SystemTime, UNIX_EPOCH};
20
21const TTL: Duration = Duration::from_secs(1);
22
23const VIRTUAL_DIRS: &[(u64, &str)] = &[
28 (FUSE_RO_INO, "ro"),
29 (FUSE_RW_INO, "rw"),
30 (FUSE_JOURNAL_INO, "journal"),
31 (FUSE_METADATA_INO, "metadata"),
32 (FUSE_UNALLOCATED_INO, "unallocated"),
33 (FUSE_SESSION_INO, "session"),
34];
35
36fn root_dir_listing(has_orphans: bool) -> Vec<(u64, &'static str)> {
40 let mut v: Vec<(u64, &'static str)> = VIRTUAL_DIRS.to_vec();
41 if has_orphans {
42 v.push((FUSE_ORPHANS_INO, "$Orphans"));
43 }
44 v
45}
46
47fn cache_has_orphans(entries: &[DeletedEntry]) -> bool {
50 entries.iter().any(|e| e.orphan)
51}
52
53#[derive(Default, Clone, Copy)]
55struct DeletedMacb {
56 modified: FsTimestamp,
57 accessed: FsTimestamp,
58 changed: FsTimestamp,
59 born: FsTimestamp,
60}
61
62struct DeletedEntry {
70 fs_ino: u64,
72 name: String,
74 real_name: String,
76 size: u64,
77 data: Vec<u8>,
78 readable: bool,
79 orphan: bool,
81 parent_ino: Option<u64>,
83 record_id: u64,
85 allocation: FsAllocation,
86 macb: DeletedMacb,
87}
88
89struct JournalTxnEntry {
91 sequence: u64,
92 name: String,
93}
94
95struct MetadataCache {
97 superblock_json: Vec<u8>,
98 timeline_jsonl: Vec<u8>,
99}
100
101struct UnallocatedEntry {
103 #[allow(dead_code)]
104 range_id: u64,
105 name: String,
106 start: u64,
107 length: u64,
108}
109
110pub struct ForensicFuseFs {
111 fs: RefCell<Box<dyn ForensicFs + Send>>,
112 session: RefCell<Option<Session>>,
113 overlay_ino_counter: RefCell<u64>,
115 root_ino: u64,
117 deleted_cache: RefCell<Option<Vec<DeletedEntry>>>,
119 journal_cache: RefCell<Option<Vec<JournalTxnEntry>>>,
121 metadata_cache: RefCell<Option<MetadataCache>>,
123 unallocated_cache: RefCell<Option<Vec<UnallocatedEntry>>>,
125 layout: crate::MountLayout,
127 deleted_mode: DeletedMode,
129}
130
131impl ForensicFuseFs {
132 pub fn new(
133 fs: Box<dyn ForensicFs + Send>,
134 session: Option<Session>,
135 layout: crate::MountLayout,
136 deleted_mode: DeletedMode,
137 ) -> Self {
138 let root_ino = fs.root_ino();
139 Self {
140 fs: RefCell::new(fs),
141 session: RefCell::new(session),
142 overlay_ino_counter: RefCell::new(1),
143 root_ino,
144 deleted_cache: RefCell::new(None),
145 journal_cache: RefCell::new(None),
146 metadata_cache: RefCell::new(None),
147 unallocated_cache: RefCell::new(None),
148 layout,
149 deleted_mode,
150 }
151 }
152
153 fn has_session(&self) -> bool {
155 self.session.borrow().is_some()
156 }
157
158 fn modified_overlay_id(fs_ino: u64) -> String {
160 format!("ino_{fs_ino}")
161 }
162
163 fn alloc_overlay_ino(&self) -> u64 {
165 let mut counter = self.overlay_ino_counter.borrow_mut();
166 let ino = *counter;
167 *counter += 1;
168 ino
169 }
170
171 fn created_overlay_id(counter: u64) -> String {
173 format!("new_{counter}")
174 }
175
176 fn overlay_created_attr(fuse_ino: u64, size: u64, is_dir: bool) -> FileAttr {
178 let kind = if is_dir {
179 FileType::Directory
180 } else {
181 FileType::RegularFile
182 };
183 FileAttr {
184 ino: fuse_ino,
185 size,
186 blocks: size.div_ceil(512),
187 atime: SystemTime::now(),
188 mtime: SystemTime::now(),
189 ctime: SystemTime::now(),
190 crtime: SystemTime::now(),
191 kind,
192 perm: if is_dir { 0o755 } else { 0o644 },
193 nlink: 1,
194 uid: 0,
195 gid: 0,
196 rdev: 0,
197 blksize: 4096,
198 flags: 0,
199 }
200 }
201
202 fn rw_parent_to_fs(&self, parent: u64) -> Option<u64> {
204 match parent {
205 FUSE_RW_INO => Some(self.root_ino),
206 _ => match decode_fuse_ino(parent) {
207 InodeNamespace::Rw(ino) => Some(ino),
208 _ => None,
209 },
210 }
211 }
212
213 fn is_whiteout(&self, fs_ino: u64) -> bool {
215 let session = self.session.borrow();
216 match session.as_ref() {
217 Some(s) => s.overlay.deleted.contains(&fs_ino),
218 None => false,
219 }
220 }
221
222 fn ensure_deleted_cache(&self) {
227 if self.deleted_cache.borrow().is_some() {
228 return;
229 }
230 if self.deleted_mode == DeletedMode::Off {
233 *self.deleted_cache.borrow_mut() = Some(Vec::new());
234 return;
235 }
236
237 let mut fs = self.fs.borrow_mut();
238 let nodes = fs.deleted_nodes().unwrap_or_default();
239
240 let plans: Vec<DeletedPlan> = nodes
243 .iter()
244 .map(|n| {
245 let has_live_collision = match n.parent_ino {
246 Some(p) if !n.name.is_empty() => fs.lookup(p, &n.name).ok().flatten().is_some(),
247 _ => false,
248 };
249 DeletedPlan {
250 ino: n.ino,
251 real_name: String::from_utf8_lossy(&n.name).into_owned(),
252 parent_ino: n.parent_ino,
253 mtime_secs: n.mtime.seconds,
254 record_id: n.record_id,
255 has_live_collision,
256 allocation: n.allocation,
257 }
258 })
259 .collect();
260
261 let placed = plan_deleted(&plans, self.deleted_mode);
262
263 let mut entries = Vec::with_capacity(placed.len());
264 for p in placed {
265 let node = nodes.iter().find(|n| n.ino == p.ino);
266 let (meta_size, macb) = node.map_or((0, DeletedMacb::default()), |n| {
267 (
268 n.size,
269 DeletedMacb {
270 modified: n.mtime,
271 accessed: n.atime,
272 changed: n.ctime,
273 born: n.crtime,
274 },
275 )
276 });
277 let (data, readable) = match fs.read_file(p.ino) {
281 Ok(d) => (d, true),
282 Err(_) => (Vec::new(), false),
283 };
284 let size = if readable {
285 data.len() as u64
286 } else {
287 meta_size
288 };
289 entries.push(DeletedEntry {
290 fs_ino: p.ino,
291 name: p.display_name,
292 real_name: p.real_name,
293 size,
294 data,
295 readable,
296 orphan: p.orphan,
297 parent_ino: p.parent_ino,
298 record_id: p.record_id,
299 allocation: p.allocation,
300 macb,
301 });
302 }
303 *self.deleted_cache.borrow_mut() = Some(entries);
304 }
305
306 fn ensure_journal_cache(&self) {
308 if self.journal_cache.borrow().is_some() {
309 return;
310 }
311 let mut fs = self.fs.borrow_mut();
312 let entries = match fs.journal_transactions() {
313 Ok(txns) => txns
314 .iter()
315 .map(|txn| JournalTxnEntry {
316 sequence: txn.sequence,
317 name: format!("txn_{}", txn.sequence),
318 })
319 .collect(),
320 Err(_) => Vec::new(),
321 };
322 *self.journal_cache.borrow_mut() = Some(entries);
323 }
324
325 fn ensure_metadata_cache(&self) {
327 if self.metadata_cache.borrow().is_some() {
328 return;
329 }
330 let fs = self.fs.borrow();
331
332 let superblock_json = match fs.fs_info() {
334 Ok(info) => serde_json::to_string_pretty(&info)
335 .unwrap_or_default()
336 .into_bytes(),
337 Err(_) => b"{}".to_vec(),
338 };
339 drop(fs);
340
341 let mut timeline_jsonl = {
344 let mut fs = self.fs.borrow_mut();
345 match fs.timeline() {
346 Ok(events) => {
347 let mut buf = Vec::new();
348 for event in &events {
349 let event_type = match event.event_type {
350 FsEventType::Created => "Created",
351 FsEventType::Modified => "Modified",
352 FsEventType::Accessed => "Accessed",
353 FsEventType::Changed => "Changed",
354 FsEventType::Deleted => "Deleted",
355 FsEventType::Mounted => "Mounted",
356 };
357 let line = serde_json::json!({
358 "timestamp_secs": event.timestamp.seconds,
359 "timestamp_nsecs": event.timestamp.nanoseconds,
360 "event_type": event_type,
361 "inode": event.inode,
362 "size": event.size,
363 "uid": event.uid,
364 "gid": event.gid,
365 });
366 let line_str = serde_json::to_string(&line).unwrap_or_default();
367 buf.extend_from_slice(line_str.as_bytes());
368 buf.push(b'\n');
369 }
370 buf
371 }
372 Err(_) => Vec::new(),
373 }
374 };
375
376 self.ensure_deleted_cache();
379 if let Some(entries) = self.deleted_cache.borrow().as_ref() {
380 for e in entries {
381 timeline_jsonl.extend_from_slice(deleted_timeline_row(e).as_bytes());
382 timeline_jsonl.push(b'\n');
383 }
384 }
385
386 *self.metadata_cache.borrow_mut() = Some(MetadataCache {
387 superblock_json,
388 timeline_jsonl,
389 });
390 }
391
392 fn ensure_unallocated_cache(&self) {
394 if self.unallocated_cache.borrow().is_some() {
395 return;
396 }
397 let mut fs = self.fs.borrow_mut();
398 let entries = match fs.unallocated_blocks() {
399 Ok(ranges) => ranges
400 .iter()
401 .enumerate()
402 .map(|(i, r)| UnallocatedEntry {
403 range_id: i as u64,
404 name: format!("blocks_{}-{}.raw", r.start, r.start + r.length),
405 start: r.start,
406 length: r.length,
407 })
408 .collect(),
409 Err(_) => Vec::new(),
410 };
411 *self.unallocated_cache.borrow_mut() = Some(entries);
412 }
413
414 fn find_created_by_name(&self, parent_ino: u64, name: &[u8]) -> Option<(String, u64, bool)> {
416 let session = self.session.borrow();
417 let session = session.as_ref()?;
418 let name_str = std::str::from_utf8(name).ok()?;
419 for (id, entry) in &session.overlay.created {
420 if entry.parent_ino == parent_ino && entry.name == name_str {
421 let counter: u64 = id.strip_prefix("new_").and_then(|s| s.parse().ok())?;
422 return Some((id.clone(), counter, false));
423 }
424 }
425 for (id, entry) in &session.overlay.dirs {
426 if entry.parent_ino == parent_ino && entry.name == name_str {
427 let counter: u64 = id.strip_prefix("new_").and_then(|s| s.parse().ok())?;
428 return Some((id.clone(), counter, true));
429 }
430 }
431 None
432 }
433
434 fn lookup_deleted_in_place(&self, parent_fs_ino: u64, name: &[u8]) -> Option<(u64, u64)> {
439 self.ensure_deleted_cache();
440 let cache = self.deleted_cache.borrow();
441 cache.as_ref()?.iter().find_map(|e| {
442 (!e.orphan && e.parent_ino == Some(parent_fs_ino) && e.name.as_bytes() == name)
443 .then_some((deleted_ino(e.fs_ino), e.size))
444 })
445 }
446
447 fn cache_orphans_present(&self) -> bool {
450 self.ensure_deleted_cache();
451 self.deleted_cache
452 .borrow()
453 .as_ref()
454 .is_some_and(|e| cache_has_orphans(e))
455 }
456}
457
458fn ts_to_systime(t: &FsTimestamp) -> SystemTime {
460 if t.seconds >= 0 {
461 UNIX_EPOCH + Duration::new(t.seconds as u64, t.nanoseconds)
462 } else {
463 UNIX_EPOCH
464 }
465}
466
467fn fs_to_attr(fuse_ino: u64, meta: &FsMetadata) -> FileAttr {
469 let kind = match meta.file_type {
470 FsFileType::RegularFile | FsFileType::Unknown => FileType::RegularFile,
471 FsFileType::Directory => FileType::Directory,
472 FsFileType::Symlink => FileType::Symlink,
473 FsFileType::CharDevice => FileType::CharDevice,
474 FsFileType::BlockDevice => FileType::BlockDevice,
475 FsFileType::Fifo => FileType::NamedPipe,
476 FsFileType::Socket => FileType::Socket,
477 };
478
479 FileAttr {
480 ino: fuse_ino,
481 size: meta.size,
482 blocks: meta.size.div_ceil(512),
483 atime: ts_to_systime(&meta.atime),
484 mtime: ts_to_systime(&meta.mtime),
485 ctime: ts_to_systime(&meta.ctime),
486 crtime: ts_to_systime(&meta.crtime),
487 kind,
488 perm: meta.mode & 0o7777,
489 nlink: u32::from(meta.links_count),
490 uid: meta.uid,
491 gid: meta.gid,
492 rdev: 0,
493 blksize: 4096,
494 flags: 0,
495 }
496}
497
498fn virtual_dir_attr(ino: u64) -> FileAttr {
500 FileAttr {
501 ino,
502 size: 0,
503 blocks: 0,
504 atime: UNIX_EPOCH,
505 mtime: UNIX_EPOCH,
506 ctime: UNIX_EPOCH,
507 crtime: UNIX_EPOCH,
508 kind: FileType::Directory,
509 perm: 0o555,
510 nlink: 2,
511 uid: 0,
512 gid: 0,
513 rdev: 0,
514 blksize: 4096,
515 flags: 0,
516 }
517}
518
519fn virtual_file_attr(ino: u64, size: u64) -> FileAttr {
521 FileAttr {
522 ino,
523 size,
524 blocks: size.div_ceil(512),
525 atime: UNIX_EPOCH,
526 mtime: UNIX_EPOCH,
527 ctime: UNIX_EPOCH,
528 crtime: UNIX_EPOCH,
529 kind: FileType::RegularFile,
530 perm: 0o444,
531 nlink: 1,
532 uid: 0,
533 gid: 0,
534 rdev: 0,
535 blksize: 4096,
536 flags: 0,
537 }
538}
539
540fn fs_file_type_to_fuse(t: FsFileType) -> FileType {
542 match t {
543 FsFileType::RegularFile | FsFileType::Unknown => FileType::RegularFile,
544 FsFileType::Directory => FileType::Directory,
545 FsFileType::Symlink => FileType::Symlink,
546 FsFileType::CharDevice => FileType::CharDevice,
547 FsFileType::BlockDevice => FileType::BlockDevice,
548 FsFileType::Fifo => FileType::NamedPipe,
549 FsFileType::Socket => FileType::Socket,
550 }
551}
552
553fn root_children(
561 layout: crate::MountLayout,
562 fs: &mut dyn ForensicFs,
563 root_ino: u64,
564 has_orphans: bool,
565) -> crate::FsResult<Vec<(u64, Vec<u8>, FileType)>> {
566 match layout {
567 crate::MountLayout::DiskOverlay => Ok(root_dir_listing(has_orphans)
568 .into_iter()
569 .map(|(ino, name)| (ino, name.as_bytes().to_vec(), FileType::Directory))
570 .collect()),
571 crate::MountLayout::Raw => {
572 let mut out = Vec::new();
573 for e in fs.read_dir(root_ino)? {
574 if e.name == b"." || e.name == b".." {
575 continue;
576 }
577 out.push((ro_ino(e.inode), e.name, fs_file_type_to_fuse(e.file_type)));
580 }
581 Ok(out)
582 }
583 }
584}
585
586struct DeletedPlan {
588 ino: u64,
589 real_name: String,
590 parent_ino: Option<u64>,
591 mtime_secs: i64,
592 record_id: u64,
593 has_live_collision: bool,
594 allocation: FsAllocation,
595}
596
597struct PlacedDeleted {
599 ino: u64,
600 display_name: String,
601 real_name: String,
602 orphan: bool,
603 parent_ino: Option<u64>,
604 record_id: u64,
605 allocation: FsAllocation,
606}
607
608fn plan_deleted(plans: &[DeletedPlan], mode: DeletedMode) -> Vec<PlacedDeleted> {
619 match mode {
620 DeletedMode::Off => Vec::new(),
621 DeletedMode::All => plans.iter().map(orphan_placed).collect(),
622 DeletedMode::Latest => {
623 use std::collections::{HashMap, HashSet};
624 let mut winner: HashMap<(u64, &str), usize> = HashMap::new();
626 for (i, p) in plans.iter().enumerate() {
627 if !eligible_in_place(p) {
628 continue;
629 }
630 let key = (p.parent_ino.unwrap_or_default(), p.real_name.as_str());
631 match winner.get(&key) {
632 Some(&j) if !outranks(p, &plans[j]) => {}
633 _ => {
634 winner.insert(key, i);
635 }
636 }
637 }
638 let winners: HashSet<usize> = winner.into_values().collect();
639 plans
640 .iter()
641 .enumerate()
642 .map(|(i, p)| {
643 if winners.contains(&i) {
644 PlacedDeleted {
645 ino: p.ino,
646 display_name: p.real_name.clone(),
647 real_name: p.real_name.clone(),
648 orphan: false,
649 parent_ino: p.parent_ino,
650 record_id: p.record_id,
651 allocation: p.allocation,
652 }
653 } else {
654 orphan_placed(p)
655 }
656 })
657 .collect()
658 }
659 }
660}
661
662fn eligible_in_place(p: &DeletedPlan) -> bool {
665 p.allocation == FsAllocation::Deleted
666 && p.parent_ino.is_some()
667 && !p.real_name.is_empty()
668 && !p.has_live_collision
669}
670
671fn outranks(a: &DeletedPlan, b: &DeletedPlan) -> bool {
673 (a.mtime_secs, a.record_id) > (b.mtime_secs, b.record_id)
674}
675
676fn orphan_placed(p: &DeletedPlan) -> PlacedDeleted {
677 PlacedDeleted {
678 ino: p.ino,
679 display_name: orphan_name(&p.real_name, p.mtime_secs, p.record_id),
680 real_name: p.real_name.clone(),
681 orphan: true,
682 parent_ino: p.parent_ino,
683 record_id: p.record_id,
684 allocation: p.allocation,
685 }
686}
687
688fn orphan_name(real_name: &str, mtime_secs: i64, record_id: u64) -> String {
692 let ts = (mtime_secs > 0).then(|| filename_safe_utc(mtime_secs));
693 match (real_name.is_empty(), ts) {
694 (false, Some(ts)) => format!("{real_name}@{ts}~{record_id}"),
695 (false, None) => format!("{real_name}~{record_id}"),
696 (true, Some(ts)) => format!("record-{record_id}@{ts}"),
697 (true, None) => format!("record-{record_id}"),
698 }
699}
700
701#[allow(clippy::many_single_char_names)] fn filename_safe_utc(secs: i64) -> String {
706 let days = secs.div_euclid(86_400);
707 let tod = secs.rem_euclid(86_400);
708 let (h, m, s) = (tod / 3600, (tod % 3600) / 60, tod % 60);
709 let (y, mon, d) = crate::marking::civil_from_days(days);
710 format!("{y:04}-{mon:02}-{d:02}T{h:02}-{m:02}-{s:02}Z")
711}
712
713fn deleted_timeline_row(e: &DeletedEntry) -> String {
718 let path = if e.orphan {
722 format!("$Orphans/{}", e.name)
723 } else {
724 e.real_name.clone()
725 };
726 let allocation = crate::marking::status_str(e.allocation);
727 let row = serde_json::json!({
728 "path": path,
729 "name": e.real_name,
730 "parent_ino": e.parent_ino,
731 "macb": {
732 "modified": e.macb.modified.seconds,
733 "accessed": e.macb.accessed.seconds,
734 "changed": e.macb.changed.seconds,
735 "born": e.macb.born.seconds,
736 },
737 "record_id": e.record_id,
738 "allocation": allocation,
739 "status": if e.readable { "recovered" } else { "unreadable" },
740 "placement": if e.orphan { "orphan" } else { "in-place" },
741 });
742 serde_json::to_string(&row).unwrap_or_default()
743}
744
745fn deleted_in_place_children(entries: &[DeletedEntry], parent_fs_ino: u64) -> Vec<(u64, String)> {
753 entries
754 .iter()
755 .filter(|e| !e.orphan && e.parent_ino == Some(parent_fs_ino))
756 .map(|e| (deleted_ino(e.fs_ino), e.name.clone()))
757 .collect()
758}
759
760fn entry_mark(entry: &DeletedEntry) -> crate::marking::Mark {
764 crate::marking::Mark {
765 allocation: entry.allocation,
766 macb: crate::marking::Macb {
767 modified: entry.macb.modified.seconds,
768 accessed: entry.macb.accessed.seconds,
769 changed: entry.macb.changed.seconds,
770 born: entry.macb.born.seconds,
771 },
772 }
773}
774
775fn deleted_xattr_names() -> &'static [&'static str] {
780 &crate::marking::UNIX_XATTR_NAMES
781}
782
783fn deleted_xattr_value(entry: &DeletedEntry, name: &str) -> Option<Vec<u8>> {
788 crate::marking::unix_xattr_value(&entry_mark(entry), name)
789}
790
791fn deleted_cow_base(entries: &[DeletedEntry], fs_ino: u64) -> Option<Vec<u8>> {
797 entries
798 .iter()
799 .find(|e| e.fs_ino == fs_ino && e.readable)
800 .map(|e| e.data.clone())
801}
802
803impl Filesystem for ForensicFuseFs {
804 fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
805 let name_bytes = name.as_encoded_bytes();
806
807 if parent == FUSE_ROOT_INO && self.layout == crate::MountLayout::DiskOverlay {
810 for (ino, dir_name) in root_dir_listing(self.cache_orphans_present()) {
811 if name_bytes == dir_name.as_bytes() {
812 let attr = if ino == FUSE_RW_INO && self.has_session() {
813 let mut a = virtual_dir_attr(ino);
814 a.perm = 0o755;
815 a
816 } else {
817 virtual_dir_attr(ino)
818 };
819 reply.entry(&TTL, &attr, 0);
820 return;
821 }
822 }
823 reply.error(libc::ENOENT);
824 return;
825 }
826
827 if parent == FUSE_ORPHANS_INO {
830 self.ensure_deleted_cache();
831 let cache = self.deleted_cache.borrow();
832 if let Some(entries) = cache.as_ref() {
833 for entry in entries.iter().filter(|e| e.orphan) {
834 if name_bytes == entry.name.as_bytes() {
835 let fuse_ino = deleted_ino(entry.fs_ino);
836 reply.entry(&TTL, &virtual_file_attr(fuse_ino, entry.size), 0);
837 return;
838 }
839 }
840 }
841 reply.error(libc::ENOENT);
842 return;
843 }
844
845 if parent == FUSE_JOURNAL_INO {
847 self.ensure_journal_cache();
848 let cache = self.journal_cache.borrow();
849 if let Some(entries) = cache.as_ref() {
850 for entry in entries {
851 if name_bytes == entry.name.as_bytes() {
852 let fuse_ino = journal_ino(entry.sequence);
853 let attr = virtual_dir_attr(fuse_ino);
854 reply.entry(&TTL, &attr, 0);
855 return;
856 }
857 }
858 }
859 reply.error(libc::ENOENT);
860 return;
861 }
862
863 if parent == FUSE_METADATA_INO {
865 self.ensure_metadata_cache();
866 let cache = self.metadata_cache.borrow();
867 if let Some(mc) = cache.as_ref() {
868 if name_bytes == b"superblock.json" {
869 let fuse_ino = metadata_ino(1);
870 let attr = virtual_file_attr(fuse_ino, mc.superblock_json.len() as u64);
871 reply.entry(&TTL, &attr, 0);
872 return;
873 }
874 if name_bytes == b"timeline.jsonl" {
875 let fuse_ino = metadata_ino(2);
876 let attr = virtual_file_attr(fuse_ino, mc.timeline_jsonl.len() as u64);
877 reply.entry(&TTL, &attr, 0);
878 return;
879 }
880 }
881 reply.error(libc::ENOENT);
882 return;
883 }
884
885 if parent == FUSE_UNALLOCATED_INO {
887 self.ensure_unallocated_cache();
888 let cache = self.unallocated_cache.borrow();
889 if let Some(entries) = cache.as_ref() {
890 for (i, entry) in entries.iter().enumerate() {
891 if name_bytes == entry.name.as_bytes() {
892 let fuse_ino = unallocated_ino(i as u64);
893 let block_size = self.fs.borrow().block_size();
894 let size = entry.length * block_size;
895 let attr = virtual_file_attr(fuse_ino, size);
896 reply.entry(&TTL, &attr, 0);
897 return;
898 }
899 }
900 }
901 reply.error(libc::ENOENT);
902 return;
903 }
904
905 if parent == FUSE_SESSION_INO {
907 if name_bytes == b"status.json" && self.has_session() {
908 let session = self.session.borrow();
909 let Some(s) = session.as_ref() else {
910 reply.error(libc::ENOENT);
912 return;
913 };
914 let status = serde_json::json!({
915 "image_path": s.metadata.image_path,
916 "image_sha256": s.metadata.image_sha256,
917 "created": s.metadata.created,
918 });
919 let data = serde_json::to_string_pretty(&status)
920 .unwrap_or_default()
921 .into_bytes();
922 let fuse_ino = metadata_ino(100);
923 let attr = virtual_file_attr(fuse_ino, data.len() as u64);
924 reply.entry(&TTL, &attr, 0);
925 return;
926 }
927 reply.error(libc::ENOENT);
928 return;
929 }
930
931 if let Some(fs_parent) = self.rw_parent_to_fs(parent) {
933 let deleted_hit = self.lookup_deleted_in_place(fs_parent, name_bytes);
936
937 if let Some((id, counter, is_dir)) = self.find_created_by_name(fs_parent, name_bytes) {
939 let session = self.session.borrow();
940 let Some(session) = session.as_ref() else {
941 reply.error(libc::ENOENT);
944 return;
945 };
946 let entry = if is_dir {
947 session.overlay.dirs.get(&id)
948 } else {
949 session.overlay.created.get(&id)
950 };
951 if let Some(entry) = entry {
952 let fuse_ino = rw_ino(counter + 9_000_000);
953 let attr = Self::overlay_created_attr(fuse_ino, entry.size, is_dir);
954 reply.entry(&TTL, &attr, 0);
955 return;
956 }
957 }
958
959 {
961 let mut fs = self.fs.borrow_mut();
962 match fs.lookup(fs_parent, name_bytes) {
963 Ok(Some(child_ino)) => {
964 if self.is_whiteout(child_ino) {
966 reply.error(libc::ENOENT);
967 return;
968 }
969
970 let session = self.session.borrow();
972 let overlay_id = Self::modified_overlay_id(child_ino);
973 if let Some(s) = session.as_ref() {
974 if s.overlay.modified.contains_key(&child_ino) {
975 if let Ok(meta) = fs.metadata(child_ino) {
976 let fuse_ino = rw_ino(child_ino);
977 let mut attr = fs_to_attr(fuse_ino, &meta);
978 if let Ok(data) = s.read_overlay_file(&overlay_id) {
979 attr.size = data.len() as u64;
980 attr.blocks = attr.size.div_ceil(512);
981 }
982 reply.entry(&TTL, &attr, 0);
983 return;
984 }
985 reply.error(libc::EIO);
986 return;
987 }
988 }
989
990 match fs.metadata(child_ino) {
992 Ok(meta) => {
993 let fuse_ino = rw_ino(child_ino);
994 reply.entry(&TTL, &fs_to_attr(fuse_ino, &meta), 0);
995 }
996 Err(_) => reply.error(libc::EIO),
997 }
998 return;
999 }
1000 Ok(None) => {
1001 if let Some((fino, size)) = deleted_hit {
1002 reply.entry(&TTL, &virtual_file_attr(fino, size), 0);
1003 return;
1004 }
1005 reply.error(libc::ENOENT);
1006 return;
1007 }
1008 Err(_) => {
1009 reply.error(libc::EIO);
1010 return;
1011 }
1012 }
1013 }
1014 }
1015
1016 let fs_parent = match parent {
1019 FUSE_RO_INO => self.root_ino,
1020 FUSE_ROOT_INO if self.layout == crate::MountLayout::Raw => self.root_ino,
1021 _ => {
1022 if let InodeNamespace::Ro(ino) = decode_fuse_ino(parent) {
1023 ino
1024 } else {
1025 reply.error(libc::ENOENT);
1026 return;
1027 }
1028 }
1029 };
1030
1031 let deleted_hit = self.lookup_deleted_in_place(fs_parent, name_bytes);
1033
1034 let mut fs = self.fs.borrow_mut();
1035 match fs.lookup(fs_parent, name_bytes) {
1036 Ok(Some(child_ino)) => match fs.metadata(child_ino) {
1037 Ok(meta) => {
1038 let fuse_ino = ro_ino(child_ino);
1039 reply.entry(&TTL, &fs_to_attr(fuse_ino, &meta), 0);
1040 }
1041 Err(_) => reply.error(libc::EIO),
1042 },
1043 Ok(None) => {
1044 if let Some((fino, size)) = deleted_hit {
1045 reply.entry(&TTL, &virtual_file_attr(fino, size), 0);
1046 } else {
1047 reply.error(libc::ENOENT);
1048 }
1049 }
1050 Err(_) => reply.error(libc::EIO),
1051 }
1052 }
1053
1054 fn getattr(&mut self, _req: &Request, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
1055 if ino == FUSE_ROOT_INO {
1057 reply.attr(&TTL, &virtual_dir_attr(FUSE_ROOT_INO));
1058 return;
1059 }
1060
1061 if (FUSE_RO_INO..=FUSE_SESSION_INO).contains(&ino) || ino == FUSE_ORPHANS_INO {
1063 let mut attr = virtual_dir_attr(ino);
1064 if ino == FUSE_RW_INO && self.has_session() {
1065 attr.perm = 0o755;
1066 }
1067 reply.attr(&TTL, &attr);
1068 return;
1069 }
1070
1071 match decode_fuse_ino(ino) {
1072 InodeNamespace::Deleted(fs_ino) => {
1073 self.ensure_deleted_cache();
1074 let cache = self.deleted_cache.borrow();
1075 if let Some(entries) = cache.as_ref() {
1076 if let Some(entry) = entries.iter().find(|e| e.fs_ino == fs_ino) {
1077 let mut size = entry.size;
1080 let session = self.session.borrow();
1081 if let Some(s) = session.as_ref() {
1082 if s.overlay.modified.contains_key(&fs_ino) {
1083 let overlay_id = Self::modified_overlay_id(fs_ino);
1084 if let Ok(d) = s.read_overlay_file(&overlay_id) {
1085 size = d.len() as u64;
1086 }
1087 }
1088 }
1089 reply.attr(&TTL, &virtual_file_attr(ino, size));
1090 return;
1091 }
1092 }
1093 reply.error(libc::ENOENT);
1094 }
1095 InodeNamespace::Metadata(id) => {
1096 self.ensure_metadata_cache();
1097 let cache = self.metadata_cache.borrow();
1098 if let Some(mc) = cache.as_ref() {
1099 match id {
1100 1 => {
1101 reply.attr(
1102 &TTL,
1103 &virtual_file_attr(ino, mc.superblock_json.len() as u64),
1104 );
1105 }
1106 2 => {
1107 reply.attr(
1108 &TTL,
1109 &virtual_file_attr(ino, mc.timeline_jsonl.len() as u64),
1110 );
1111 }
1112 100 => {
1113 if self.has_session() {
1115 let session = self.session.borrow();
1116 let Some(s) = session.as_ref() else {
1117 reply.error(libc::ENOENT);
1119 return;
1120 };
1121 let status = serde_json::json!({
1122 "image_path": s.metadata.image_path,
1123 "image_sha256": s.metadata.image_sha256,
1124 "created": s.metadata.created,
1125 });
1126 let data =
1127 serde_json::to_string_pretty(&status).unwrap_or_default();
1128 reply.attr(&TTL, &virtual_file_attr(ino, data.len() as u64));
1129 } else {
1130 reply.error(libc::ENOENT);
1131 }
1132 }
1133 _ => reply.error(libc::ENOENT),
1134 }
1135 } else {
1136 reply.error(libc::ENOENT);
1137 }
1138 }
1139 InodeNamespace::Journal(seq) => {
1140 self.ensure_journal_cache();
1141 let cache = self.journal_cache.borrow();
1142 if let Some(entries) = cache.as_ref() {
1143 if entries.iter().any(|e| e.sequence == seq) {
1144 reply.attr(&TTL, &virtual_dir_attr(ino));
1145 } else {
1146 reply.error(libc::ENOENT);
1147 }
1148 } else {
1149 reply.error(libc::ENOENT);
1150 }
1151 }
1152 InodeNamespace::Unallocated(range_id) => {
1153 self.ensure_unallocated_cache();
1154 let cache = self.unallocated_cache.borrow();
1155 if let Some(entries) = cache.as_ref() {
1156 if let Some(entry) = entries.get(range_id as usize) {
1157 let block_size = self.fs.borrow().block_size();
1158 let size = entry.length * block_size;
1159 reply.attr(&TTL, &virtual_file_attr(ino, size));
1160 } else {
1161 reply.error(libc::ENOENT);
1162 }
1163 } else {
1164 reply.error(libc::ENOENT);
1165 }
1166 }
1167 InodeNamespace::Ro(fs_ino) => {
1168 let mut fs = self.fs.borrow_mut();
1169 match fs.metadata(fs_ino) {
1170 Ok(meta) => reply.attr(&TTL, &fs_to_attr(ino, &meta)),
1171 Err(_) => reply.error(libc::EIO),
1172 }
1173 }
1174 InodeNamespace::Rw(rw_id) => {
1175 if rw_id >= 9_000_000 {
1177 let counter = rw_id - 9_000_000;
1178 let created_id = Self::created_overlay_id(counter);
1179 let session = self.session.borrow();
1180 if let Some(s) = session.as_ref() {
1181 if let Some(entry) = s.overlay.created.get(&created_id) {
1182 let attr = Self::overlay_created_attr(ino, entry.size, false);
1183 reply.attr(&TTL, &attr);
1184 return;
1185 }
1186 if let Some(entry) = s.overlay.dirs.get(&created_id) {
1187 let attr = Self::overlay_created_attr(ino, entry.size, true);
1188 reply.attr(&TTL, &attr);
1189 return;
1190 }
1191 }
1192 reply.error(libc::ENOENT);
1193 return;
1194 }
1195
1196 let fs_ino = rw_id;
1198 let mut fs = self.fs.borrow_mut();
1199 match fs.metadata(fs_ino) {
1200 Ok(meta) => {
1201 let mut attr = fs_to_attr(ino, &meta);
1202 let session = self.session.borrow();
1204 if let Some(s) = session.as_ref() {
1205 let overlay_id = Self::modified_overlay_id(fs_ino);
1206 if s.overlay.modified.contains_key(&fs_ino) {
1207 if let Ok(data) = s.read_overlay_file(&overlay_id) {
1208 attr.size = data.len() as u64;
1209 attr.blocks = attr.size.div_ceil(512);
1210 }
1211 }
1212 }
1213 reply.attr(&TTL, &attr);
1214 }
1215 Err(_) => reply.error(libc::EIO),
1216 }
1217 }
1218 _ => reply.error(libc::ENOENT),
1219 }
1220 }
1221
1222 fn getxattr(&mut self, _req: &Request, ino: u64, name: &OsStr, size: u32, reply: ReplyXattr) {
1227 let value = if let InodeNamespace::Deleted(fs_ino) = decode_fuse_ino(ino) {
1228 self.ensure_deleted_cache();
1229 let cache = self.deleted_cache.borrow();
1230 let attr_name = name.to_str();
1231 cache.as_ref().and_then(|entries| {
1232 let n = attr_name?;
1233 let e = entries.iter().find(|e| e.fs_ino == fs_ino)?;
1234 deleted_xattr_value(e, n)
1235 })
1236 } else {
1237 None
1238 };
1239 match value {
1240 Some(v) => {
1241 if size == 0 {
1242 reply.size(v.len() as u32);
1243 } else if (v.len() as u32) <= size {
1244 reply.data(&v);
1245 } else {
1246 reply.error(libc::ERANGE);
1247 }
1248 }
1249 None => reply.error(libc::ENODATA),
1250 }
1251 }
1252
1253 fn listxattr(&mut self, _req: &Request, ino: u64, size: u32, reply: ReplyXattr) {
1257 let mut buf: Vec<u8> = Vec::new();
1258 if let InodeNamespace::Deleted(fs_ino) = decode_fuse_ino(ino) {
1259 self.ensure_deleted_cache();
1260 let cache = self.deleted_cache.borrow();
1261 let present = cache
1262 .as_ref()
1263 .is_some_and(|entries| entries.iter().any(|e| e.fs_ino == fs_ino));
1264 if present {
1265 for n in deleted_xattr_names() {
1266 buf.extend_from_slice(n.as_bytes());
1267 buf.push(0);
1268 }
1269 }
1270 }
1271 if size == 0 {
1272 reply.size(buf.len() as u32);
1273 } else if (buf.len() as u32) <= size {
1274 reply.data(&buf);
1275 } else {
1276 reply.error(libc::ERANGE);
1277 }
1278 }
1279
1280 fn readdir(
1281 &mut self,
1282 _req: &Request,
1283 ino: u64,
1284 _fh: u64,
1285 offset: i64,
1286 mut reply: ReplyDirectory,
1287 ) {
1288 let offset = offset as usize;
1289
1290 if ino == FUSE_ROOT_INO {
1293 let has_orphans = self.cache_orphans_present();
1294 let mut entries: Vec<(u64, FileType, String)> = vec![
1295 (FUSE_ROOT_INO, FileType::Directory, ".".to_string()),
1296 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1297 ];
1298 {
1299 let mut fs = self.fs.borrow_mut();
1300 let Ok(children) =
1301 root_children(self.layout, &mut **fs, self.root_ino, has_orphans)
1302 else {
1303 reply.error(libc::EIO);
1304 return;
1305 };
1306 for (fino, name, kind) in children {
1307 entries.push((fino, kind, String::from_utf8_lossy(&name).into_owned()));
1308 }
1309 }
1310 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1311 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1312 break;
1313 }
1314 }
1315 reply.ok();
1316 return;
1317 }
1318
1319 if let Some(fs_dir_ino) = match ino {
1321 FUSE_RW_INO => Some(self.root_ino),
1322 _ => match decode_fuse_ino(ino) {
1323 InodeNamespace::Rw(rw_id) if rw_id < 9_000_000 => Some(rw_id),
1324 _ => None,
1325 },
1326 } {
1327 self.ensure_deleted_cache();
1331 let injected: Vec<(u64, FileType, String)> = {
1332 let cache = self.deleted_cache.borrow();
1333 cache.as_ref().map_or_else(Vec::new, |c| {
1334 deleted_in_place_children(c, fs_dir_ino)
1335 .into_iter()
1336 .map(|(dino, name)| (dino, FileType::RegularFile, name))
1337 .collect()
1338 })
1339 };
1340 let mut fs = self.fs.borrow_mut();
1341 match fs.read_dir(fs_dir_ino) {
1342 Ok(entries) => {
1343 let session = self.session.borrow();
1344 let mut fuse_entries: Vec<(u64, FileType, String)> = Vec::new();
1345
1346 for e in &entries {
1347 let name = e.name_str();
1348 let child_ino = e.inode;
1349
1350 if let Some(s) = session.as_ref() {
1352 if name != "." && name != ".." && s.overlay.deleted.contains(&child_ino)
1353 {
1354 continue;
1355 }
1356 }
1357
1358 let fuse_ino = if name == "." || name == ".." {
1359 if fs_dir_ino == self.root_ino && name == "." {
1360 FUSE_RW_INO
1361 } else if fs_dir_ino == self.root_ino && name == ".." {
1362 FUSE_ROOT_INO
1363 } else {
1364 rw_ino(child_ino)
1365 }
1366 } else {
1367 rw_ino(child_ino)
1368 };
1369 let kind = fs_file_type_to_fuse(e.file_type);
1370 fuse_entries.push((fuse_ino, kind, name));
1371 }
1372
1373 if let Some(s) = session.as_ref() {
1375 for (id, entry) in &s.overlay.created {
1376 if entry.parent_ino == fs_dir_ino {
1377 if let Some(counter) =
1378 id.strip_prefix("new_").and_then(|s| s.parse::<u64>().ok())
1379 {
1380 let fuse_ino = rw_ino(counter + 9_000_000);
1381 fuse_entries.push((
1382 fuse_ino,
1383 FileType::RegularFile,
1384 entry.name.clone(),
1385 ));
1386 }
1387 }
1388 }
1389 for (id, entry) in &s.overlay.dirs {
1390 if entry.parent_ino == fs_dir_ino {
1391 if let Some(counter) =
1392 id.strip_prefix("new_").and_then(|s| s.parse::<u64>().ok())
1393 {
1394 let fuse_ino = rw_ino(counter + 9_000_000);
1395 fuse_entries.push((
1396 fuse_ino,
1397 FileType::Directory,
1398 entry.name.clone(),
1399 ));
1400 }
1401 }
1402 }
1403 }
1404
1405 fuse_entries.extend(injected);
1407
1408 for (i, (entry_ino, kind, name)) in fuse_entries.iter().enumerate().skip(offset)
1409 {
1410 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1411 break;
1412 }
1413 }
1414 reply.ok();
1415 }
1416 Err(_) => reply.error(libc::EIO),
1417 }
1418 return;
1419 }
1420
1421 if ino == FUSE_ORPHANS_INO {
1425 self.ensure_deleted_cache();
1426 let cache = self.deleted_cache.borrow();
1427 let mut entries: Vec<(u64, FileType, String)> = vec![
1428 (FUSE_ORPHANS_INO, FileType::Directory, ".".to_string()),
1429 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1430 ];
1431 if let Some(cached) = cache.as_ref() {
1432 for entry in cached.iter().filter(|e| e.orphan) {
1433 entries.push((
1434 deleted_ino(entry.fs_ino),
1435 FileType::RegularFile,
1436 entry.name.clone(),
1437 ));
1438 }
1439 }
1440 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1441 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1442 break;
1443 }
1444 }
1445 reply.ok();
1446 return;
1447 }
1448
1449 if ino == FUSE_JOURNAL_INO {
1451 self.ensure_journal_cache();
1452 let cache = self.journal_cache.borrow();
1453 let mut entries: Vec<(u64, FileType, String)> = vec![
1454 (FUSE_JOURNAL_INO, FileType::Directory, ".".to_string()),
1455 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1456 ];
1457 if let Some(cached) = cache.as_ref() {
1458 for entry in cached {
1459 entries.push((
1460 journal_ino(entry.sequence),
1461 FileType::Directory,
1462 entry.name.clone(),
1463 ));
1464 }
1465 }
1466 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1467 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1468 break;
1469 }
1470 }
1471 reply.ok();
1472 return;
1473 }
1474
1475 if let InodeNamespace::Journal(_seq) = decode_fuse_ino(ino) {
1477 if offset == 0 {
1478 let _ = reply.add(ino, 1, FileType::Directory, ".");
1479 let _ = reply.add(FUSE_JOURNAL_INO, 2, FileType::Directory, "..");
1480 }
1481 reply.ok();
1482 return;
1483 }
1484
1485 if ino == FUSE_METADATA_INO {
1487 self.ensure_metadata_cache();
1488 let cache = self.metadata_cache.borrow();
1489 let mut entries: Vec<(u64, FileType, String)> = vec![
1490 (FUSE_METADATA_INO, FileType::Directory, ".".to_string()),
1491 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1492 ];
1493 if cache.is_some() {
1494 entries.push((
1495 metadata_ino(1),
1496 FileType::RegularFile,
1497 "superblock.json".to_string(),
1498 ));
1499 entries.push((
1500 metadata_ino(2),
1501 FileType::RegularFile,
1502 "timeline.jsonl".to_string(),
1503 ));
1504 }
1505 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1506 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1507 break;
1508 }
1509 }
1510 reply.ok();
1511 return;
1512 }
1513
1514 if ino == FUSE_UNALLOCATED_INO {
1516 self.ensure_unallocated_cache();
1517 let cache = self.unallocated_cache.borrow();
1518 let mut entries: Vec<(u64, FileType, String)> = vec![
1519 (FUSE_UNALLOCATED_INO, FileType::Directory, ".".to_string()),
1520 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1521 ];
1522 if let Some(cached) = cache.as_ref() {
1523 for (i, entry) in cached.iter().enumerate() {
1524 entries.push((
1525 unallocated_ino(i as u64),
1526 FileType::RegularFile,
1527 entry.name.clone(),
1528 ));
1529 }
1530 }
1531 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1532 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1533 break;
1534 }
1535 }
1536 reply.ok();
1537 return;
1538 }
1539
1540 if ino == FUSE_SESSION_INO {
1542 let mut entries: Vec<(u64, FileType, String)> = vec![
1543 (FUSE_SESSION_INO, FileType::Directory, ".".to_string()),
1544 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1545 ];
1546 if self.has_session() {
1547 entries.push((
1548 metadata_ino(100),
1549 FileType::RegularFile,
1550 "status.json".to_string(),
1551 ));
1552 }
1553 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1554 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1555 break;
1556 }
1557 }
1558 reply.ok();
1559 return;
1560 }
1561
1562 let fs_dir_ino = match ino {
1564 FUSE_RO_INO => self.root_ino,
1565 _ => {
1566 if let InodeNamespace::Ro(fs_ino) = decode_fuse_ino(ino) {
1567 fs_ino
1568 } else {
1569 reply.error(libc::ENOENT);
1570 return;
1571 }
1572 }
1573 };
1574
1575 self.ensure_deleted_cache();
1578 let injected: Vec<(u64, FileType, String)> = {
1579 let cache = self.deleted_cache.borrow();
1580 cache.as_ref().map_or_else(Vec::new, |c| {
1581 deleted_in_place_children(c, fs_dir_ino)
1582 .into_iter()
1583 .map(|(dino, name)| (dino, FileType::RegularFile, name))
1584 .collect()
1585 })
1586 };
1587
1588 let mut fs = self.fs.borrow_mut();
1589 match fs.read_dir(fs_dir_ino) {
1590 Ok(entries) => {
1591 let root_ino = self.root_ino;
1592 let mut fuse_entries: Vec<(u64, FileType, String)> = entries
1593 .iter()
1594 .map(|e| {
1595 let name = e.name_str();
1596 let fuse_ino = if name == "." || name == ".." {
1597 if fs_dir_ino == root_ino && name == "." {
1598 FUSE_RO_INO
1599 } else if fs_dir_ino == root_ino && name == ".." {
1600 FUSE_ROOT_INO
1601 } else {
1602 ro_ino(e.inode)
1603 }
1604 } else {
1605 ro_ino(e.inode)
1606 };
1607 let kind = fs_file_type_to_fuse(e.file_type);
1608 (fuse_ino, kind, name)
1609 })
1610 .collect();
1611
1612 fuse_entries.extend(injected);
1613
1614 for (i, (entry_ino, kind, name)) in fuse_entries.iter().enumerate().skip(offset) {
1615 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1616 break;
1617 }
1618 }
1619 reply.ok();
1620 }
1621 Err(_) => reply.error(libc::EIO),
1622 }
1623 }
1624
1625 fn read(
1626 &mut self,
1627 _req: &Request,
1628 ino: u64,
1629 _fh: u64,
1630 offset: i64,
1631 size: u32,
1632 _flags: i32,
1633 _lock_owner: Option<u64>,
1634 reply: ReplyData,
1635 ) {
1636 match decode_fuse_ino(ino) {
1637 InodeNamespace::Deleted(fs_ino) => {
1638 {
1641 let session = self.session.borrow();
1642 if let Some(s) = session.as_ref() {
1643 if s.overlay.modified.contains_key(&fs_ino) {
1644 let overlay_id = Self::modified_overlay_id(fs_ino);
1645 if let Ok(d) = s.read_overlay_file(&overlay_id) {
1646 let off = offset as usize;
1647 if off >= d.len() {
1648 reply.data(&[]);
1649 } else {
1650 let end = (off + size as usize).min(d.len());
1651 reply.data(&d[off..end]);
1652 }
1653 return;
1654 }
1655 }
1656 }
1657 }
1658 self.ensure_deleted_cache();
1659 let cache = self.deleted_cache.borrow();
1660 if let Some(entries) = cache.as_ref() {
1661 if let Some(entry) = entries.iter().find(|e| e.fs_ino == fs_ino) {
1662 if !entry.readable {
1665 reply.error(libc::EIO);
1666 return;
1667 }
1668 let off = offset as usize;
1669 if off >= entry.data.len() {
1670 reply.data(&[]);
1671 } else {
1672 let end = (off + size as usize).min(entry.data.len());
1673 reply.data(&entry.data[off..end]);
1674 }
1675 return;
1676 }
1677 }
1678 reply.error(libc::ENOENT);
1679 }
1680 InodeNamespace::Metadata(id) => {
1681 self.ensure_metadata_cache();
1682 let data = match id {
1683 1 => {
1684 let cache = self.metadata_cache.borrow();
1685 cache.as_ref().map(|mc| mc.superblock_json.clone())
1686 }
1687 2 => {
1688 let cache = self.metadata_cache.borrow();
1689 cache.as_ref().map(|mc| mc.timeline_jsonl.clone())
1690 }
1691 100 => {
1692 let session = self.session.borrow();
1694 session.as_ref().map(|s| {
1695 let status = serde_json::json!({
1696 "image_path": s.metadata.image_path,
1697 "image_sha256": s.metadata.image_sha256,
1698 "created": s.metadata.created,
1699 });
1700 serde_json::to_string_pretty(&status)
1701 .unwrap_or_default()
1702 .into_bytes()
1703 })
1704 }
1705 _ => None,
1706 };
1707 match data {
1708 Some(buf) => {
1709 let off = offset as usize;
1710 if off >= buf.len() {
1711 reply.data(&[]);
1712 } else {
1713 let end = (off + size as usize).min(buf.len());
1714 reply.data(&buf[off..end]);
1715 }
1716 }
1717 None => reply.error(libc::ENOENT),
1718 }
1719 }
1720 InodeNamespace::Unallocated(range_id) => {
1721 self.ensure_unallocated_cache();
1722 let range_info = {
1723 let cache = self.unallocated_cache.borrow();
1724 cache.as_ref().and_then(|entries| {
1725 entries.get(range_id as usize).map(|e| FsBlockRange {
1726 start: e.start,
1727 length: e.length,
1728 })
1729 })
1730 };
1731 match range_info {
1732 Some(range) => {
1733 let mut fs = self.fs.borrow_mut();
1734 match fs.read_unallocated(&range) {
1735 Ok(data) => {
1736 let off = offset as usize;
1737 if off >= data.len() {
1738 reply.data(&[]);
1739 } else {
1740 let end = (off + size as usize).min(data.len());
1741 reply.data(&data[off..end]);
1742 }
1743 }
1744 Err(_) => reply.error(libc::EIO),
1745 }
1746 }
1747 None => reply.error(libc::ENOENT),
1748 }
1749 }
1750 InodeNamespace::Ro(fs_ino) => {
1751 let mut fs = self.fs.borrow_mut();
1752 match fs.read_file_range(fs_ino, offset as u64, u64::from(size)) {
1753 Ok(data) => reply.data(&data),
1754 Err(_) => reply.error(libc::EIO),
1755 }
1756 }
1757 InodeNamespace::Rw(rw_id) => {
1758 if rw_id >= 9_000_000 {
1760 let counter = rw_id - 9_000_000;
1761 let created_id = Self::created_overlay_id(counter);
1762 let session = self.session.borrow();
1763 if let Some(s) = session.as_ref() {
1764 if s.overlay.created.contains_key(&created_id)
1765 || s.overlay.dirs.contains_key(&created_id)
1766 {
1767 if let Ok(data) = s.read_overlay_file(&created_id) {
1768 let off = offset as usize;
1769 let end = (off + size as usize).min(data.len());
1770 if off >= data.len() {
1771 reply.data(&[]);
1772 } else {
1773 reply.data(&data[off..end]);
1774 }
1775 return;
1776 }
1777 reply.error(libc::EIO);
1778 return;
1779 }
1780 }
1781 reply.error(libc::ENOENT);
1782 return;
1783 }
1784
1785 let fs_ino = rw_id;
1787 let session = self.session.borrow();
1789 if let Some(s) = session.as_ref() {
1790 let overlay_id = Self::modified_overlay_id(fs_ino);
1791 if s.overlay.modified.contains_key(&fs_ino) {
1792 if let Ok(data) = s.read_overlay_file(&overlay_id) {
1793 let off = offset as usize;
1794 let end = (off + size as usize).min(data.len());
1795 if off >= data.len() {
1796 reply.data(&[]);
1797 } else {
1798 reply.data(&data[off..end]);
1799 }
1800 return;
1801 }
1802 reply.error(libc::EIO);
1803 return;
1804 }
1805 }
1806 drop(session);
1807
1808 let mut fs = self.fs.borrow_mut();
1810 match fs.read_file_range(fs_ino, offset as u64, u64::from(size)) {
1811 Ok(data) => reply.data(&data),
1812 Err(_) => reply.error(libc::EIO),
1813 }
1814 }
1815 _ => {
1816 reply.error(libc::ENOENT);
1817 }
1818 }
1819 }
1820
1821 fn readlink(&mut self, _req: &Request, ino: u64, reply: ReplyData) {
1822 let fs_ino = match decode_fuse_ino(ino) {
1823 InodeNamespace::Ro(fs_ino) => fs_ino,
1824 InodeNamespace::Rw(rw_id) if rw_id < 9_000_000 => rw_id,
1825 _ => {
1826 reply.error(libc::ENOENT);
1827 return;
1828 }
1829 };
1830
1831 let mut fs = self.fs.borrow_mut();
1832 match fs.read_link(fs_ino) {
1833 Ok(target) => reply.data(&target),
1834 Err(_) => reply.error(libc::EIO),
1835 }
1836 }
1837
1838 fn write(
1839 &mut self,
1840 _req: &Request,
1841 ino: u64,
1842 _fh: u64,
1843 offset: i64,
1844 data: &[u8],
1845 _write_flags: u32,
1846 _flags: i32,
1847 _lock_owner: Option<u64>,
1848 reply: ReplyWrite,
1849 ) {
1850 if !self.has_session() {
1851 reply.error(libc::EROFS);
1852 return;
1853 }
1854
1855 match decode_fuse_ino(ino) {
1856 InodeNamespace::Rw(rw_id) => {
1857 if rw_id >= 9_000_000 {
1859 let counter = rw_id - 9_000_000;
1860 let created_id = Self::created_overlay_id(counter);
1861 let mut session = self.session.borrow_mut();
1862 let Some(s) = session.as_mut() else {
1863 reply.error(libc::EROFS);
1866 return;
1867 };
1868
1869 let is_known = s.overlay.created.contains_key(&created_id)
1870 || s.overlay.dirs.contains_key(&created_id);
1871 if !is_known {
1872 reply.error(libc::ENOENT);
1873 return;
1874 }
1875
1876 let mut buf = s.read_overlay_file(&created_id).unwrap_or_default();
1877 let off = offset as usize;
1878 let end = off + data.len();
1879 if end > buf.len() {
1880 buf.resize(end, 0);
1881 }
1882 buf[off..end].copy_from_slice(data);
1883
1884 if s.write_overlay_file(&created_id, &buf).is_err() {
1885 reply.error(libc::EIO);
1886 return;
1887 }
1888
1889 if let Some(entry) = s.overlay.created.get_mut(&created_id) {
1890 entry.size = buf.len() as u64;
1891 }
1892 if let Some(entry) = s.overlay.dirs.get_mut(&created_id) {
1893 entry.size = buf.len() as u64;
1894 }
1895
1896 if s.save().is_err() {
1897 reply.error(libc::EIO);
1898 return;
1899 }
1900
1901 reply.written(data.len() as u32);
1902 return;
1903 }
1904
1905 let fs_ino = rw_id;
1907 let overlay_id = Self::modified_overlay_id(fs_ino);
1908
1909 let mut session = self.session.borrow_mut();
1910 let Some(s) = session.as_mut() else {
1911 reply.error(libc::EROFS);
1914 return;
1915 };
1916
1917 if !s.overlay.modified.contains_key(&fs_ino) {
1918 let mut fs = self.fs.borrow_mut();
1919 let Ok(original) = fs.read_file(fs_ino) else {
1920 reply.error(libc::EIO);
1921 return;
1922 };
1923 if s.write_overlay_file(&overlay_id, &original).is_err() {
1924 reply.error(libc::EIO);
1925 return;
1926 }
1927 s.overlay.modified.insert(fs_ino, overlay_id.clone());
1928 }
1929
1930 let mut buf = s.read_overlay_file(&overlay_id).unwrap_or_default();
1931 let off = offset as usize;
1932 let end = off + data.len();
1933 if end > buf.len() {
1934 buf.resize(end, 0);
1935 }
1936 buf[off..end].copy_from_slice(data);
1937
1938 if s.write_overlay_file(&overlay_id, &buf).is_err() {
1939 reply.error(libc::EIO);
1940 return;
1941 }
1942
1943 if s.save().is_err() {
1944 reply.error(libc::EIO);
1945 return;
1946 }
1947
1948 reply.written(data.len() as u32);
1949 }
1950 InodeNamespace::Deleted(fs_ino) => {
1954 self.ensure_deleted_cache();
1955 let overlay_id = Self::modified_overlay_id(fs_ino);
1956 let mut session = self.session.borrow_mut();
1957 let Some(s) = session.as_mut() else {
1958 reply.error(libc::EROFS);
1961 return;
1962 };
1963
1964 if !s.overlay.modified.contains_key(&fs_ino) {
1965 let base = {
1966 let cache = self.deleted_cache.borrow();
1967 cache.as_ref().and_then(|e| deleted_cow_base(e, fs_ino))
1968 };
1969 let Some(base) = base else {
1972 reply.error(libc::EIO);
1973 return;
1974 };
1975 if s.write_overlay_file(&overlay_id, &base).is_err() {
1976 reply.error(libc::EIO);
1977 return;
1978 }
1979 s.overlay.modified.insert(fs_ino, overlay_id.clone());
1980 }
1981
1982 let mut buf = s.read_overlay_file(&overlay_id).unwrap_or_default();
1983 let off = offset as usize;
1984 let end = off + data.len();
1985 if end > buf.len() {
1986 buf.resize(end, 0);
1987 }
1988 buf[off..end].copy_from_slice(data);
1989
1990 if s.write_overlay_file(&overlay_id, &buf).is_err() {
1991 reply.error(libc::EIO);
1992 return;
1993 }
1994 if s.save().is_err() {
1995 reply.error(libc::EIO);
1996 return;
1997 }
1998 reply.written(data.len() as u32);
1999 }
2000 _ => reply.error(libc::EROFS),
2001 }
2002 }
2003
2004 fn create(
2005 &mut self,
2006 _req: &Request,
2007 parent: u64,
2008 name: &OsStr,
2009 _mode: u32,
2010 _umask: u32,
2011 _flags: i32,
2012 reply: ReplyCreate,
2013 ) {
2014 if !self.has_session() {
2015 reply.error(libc::EROFS);
2016 return;
2017 }
2018
2019 let Some(fs_parent) = self.rw_parent_to_fs(parent) else {
2020 reply.error(libc::EROFS);
2021 return;
2022 };
2023
2024 let Some(name_s) = name.to_str() else {
2025 reply.error(libc::EINVAL);
2026 return;
2027 };
2028 let name_str = name_s.to_string();
2029
2030 let counter = self.alloc_overlay_ino();
2031 let created_id = Self::created_overlay_id(counter);
2032 let fuse_ino = rw_ino(counter + 9_000_000);
2033
2034 let mut session = self.session.borrow_mut();
2035 let Some(s) = session.as_mut() else {
2036 reply.error(libc::EROFS);
2039 return;
2040 };
2041
2042 if s.write_overlay_file(&created_id, &[]).is_err() {
2043 reply.error(libc::EIO);
2044 return;
2045 }
2046
2047 s.overlay.created.insert(
2048 created_id,
2049 crate::session::OverlayEntry {
2050 parent_ino: fs_parent,
2051 name: name_str,
2052 size: 0,
2053 },
2054 );
2055
2056 if s.save().is_err() {
2057 reply.error(libc::EIO);
2058 return;
2059 }
2060
2061 let attr = Self::overlay_created_attr(fuse_ino, 0, false);
2062 reply.created(&TTL, &attr, 0, 0, 0);
2063 }
2064
2065 fn mkdir(
2066 &mut self,
2067 _req: &Request,
2068 parent: u64,
2069 name: &OsStr,
2070 _mode: u32,
2071 _umask: u32,
2072 reply: ReplyEntry,
2073 ) {
2074 if !self.has_session() {
2075 reply.error(libc::EROFS);
2076 return;
2077 }
2078
2079 let Some(fs_parent) = self.rw_parent_to_fs(parent) else {
2080 reply.error(libc::EROFS);
2081 return;
2082 };
2083
2084 let Some(name_s) = name.to_str() else {
2085 reply.error(libc::EINVAL);
2086 return;
2087 };
2088 let name_str = name_s.to_string();
2089
2090 let counter = self.alloc_overlay_ino();
2091 let created_id = Self::created_overlay_id(counter);
2092 let fuse_ino = rw_ino(counter + 9_000_000);
2093
2094 let mut session = self.session.borrow_mut();
2095 let Some(s) = session.as_mut() else {
2096 reply.error(libc::EROFS);
2099 return;
2100 };
2101
2102 s.overlay.dirs.insert(
2103 created_id,
2104 crate::session::OverlayEntry {
2105 parent_ino: fs_parent,
2106 name: name_str,
2107 size: 0,
2108 },
2109 );
2110
2111 if s.save().is_err() {
2112 reply.error(libc::EIO);
2113 return;
2114 }
2115
2116 let attr = Self::overlay_created_attr(fuse_ino, 0, true);
2117 reply.entry(&TTL, &attr, 0);
2118 }
2119
2120 fn unlink(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) {
2121 if !self.has_session() {
2122 reply.error(libc::EROFS);
2123 return;
2124 }
2125
2126 let Some(fs_parent) = self.rw_parent_to_fs(parent) else {
2127 reply.error(libc::EROFS);
2128 return;
2129 };
2130
2131 let name_bytes = name.as_encoded_bytes();
2132
2133 if let Some((id, _counter, _is_dir)) = self.find_created_by_name(fs_parent, name_bytes) {
2135 let mut session = self.session.borrow_mut();
2136 let Some(s) = session.as_mut() else {
2137 reply.error(libc::EROFS);
2140 return;
2141 };
2142 s.overlay.created.remove(&id);
2143 s.overlay.dirs.remove(&id);
2144 let _ = std::fs::remove_file(s.overlay_file_path(&id));
2145 if s.save().is_err() {
2146 reply.error(libc::EIO);
2147 return;
2148 }
2149 reply.ok();
2150 return;
2151 }
2152
2153 let mut fs = self.fs.borrow_mut();
2155 match fs.lookup(fs_parent, name_bytes) {
2156 Ok(Some(child_ino)) => {
2157 let mut session = self.session.borrow_mut();
2158 let Some(s) = session.as_mut() else {
2159 reply.error(libc::EROFS);
2162 return;
2163 };
2164 if !s.overlay.deleted.contains(&child_ino) {
2165 s.overlay.deleted.push(child_ino);
2166 }
2167 if s.save().is_err() {
2168 reply.error(libc::EIO);
2169 return;
2170 }
2171 reply.ok();
2172 }
2173 Ok(None) => reply.error(libc::ENOENT),
2174 Err(_) => reply.error(libc::EIO),
2175 }
2176 }
2177
2178 fn rmdir(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) {
2179 self.unlink(req, parent, name, reply);
2180 }
2181
2182 #[allow(clippy::too_many_arguments)]
2183 fn setattr(
2184 &mut self,
2185 _req: &Request,
2186 ino: u64,
2187 _mode: Option<u32>,
2188 _uid: Option<u32>,
2189 _gid: Option<u32>,
2190 size: Option<u64>,
2191 _atime: Option<TimeOrNow>,
2192 _mtime: Option<TimeOrNow>,
2193 _ctime: Option<SystemTime>,
2194 _fh: Option<u64>,
2195 _crtime: Option<SystemTime>,
2196 _chgtime: Option<SystemTime>,
2197 _bkuptime: Option<SystemTime>,
2198 _flags: Option<u32>,
2199 reply: ReplyAttr,
2200 ) {
2201 match decode_fuse_ino(ino) {
2202 InodeNamespace::Rw(rw_id) => {
2203 if !self.has_session() {
2204 reply.error(libc::EROFS);
2205 return;
2206 }
2207
2208 if let Some(new_size) = size {
2210 if rw_id >= 9_000_000 {
2212 let counter = rw_id - 9_000_000;
2213 let created_id = Self::created_overlay_id(counter);
2214 let mut session = self.session.borrow_mut();
2215 let Some(s) = session.as_mut() else {
2216 reply.error(libc::EROFS);
2219 return;
2220 };
2221
2222 let mut buf = s.read_overlay_file(&created_id).unwrap_or_default();
2223 buf.resize(new_size as usize, 0);
2224 if s.write_overlay_file(&created_id, &buf).is_err() {
2225 reply.error(libc::EIO);
2226 return;
2227 }
2228
2229 if let Some(entry) = s.overlay.created.get_mut(&created_id) {
2230 entry.size = new_size;
2231 }
2232 if let Some(entry) = s.overlay.dirs.get_mut(&created_id) {
2233 entry.size = new_size;
2234 }
2235 if s.save().is_err() {
2236 reply.error(libc::EIO);
2237 return;
2238 }
2239
2240 let attr = Self::overlay_created_attr(ino, new_size, false);
2241 reply.attr(&TTL, &attr);
2242 return;
2243 }
2244
2245 let fs_ino = rw_id;
2247 let overlay_id = Self::modified_overlay_id(fs_ino);
2248
2249 let mut session = self.session.borrow_mut();
2250 let Some(s) = session.as_mut() else {
2251 reply.error(libc::EROFS);
2254 return;
2255 };
2256
2257 if !s.overlay.modified.contains_key(&fs_ino) {
2258 let mut fs = self.fs.borrow_mut();
2259 let Ok(original) = fs.read_file(fs_ino) else {
2260 reply.error(libc::EIO);
2261 return;
2262 };
2263 if s.write_overlay_file(&overlay_id, &original).is_err() {
2264 reply.error(libc::EIO);
2265 return;
2266 }
2267 s.overlay.modified.insert(fs_ino, overlay_id.clone());
2268 }
2269
2270 let mut buf = s.read_overlay_file(&overlay_id).unwrap_or_default();
2271 buf.resize(new_size as usize, 0);
2272 if s.write_overlay_file(&overlay_id, &buf).is_err() {
2273 reply.error(libc::EIO);
2274 return;
2275 }
2276 if s.save().is_err() {
2277 reply.error(libc::EIO);
2278 return;
2279 }
2280
2281 let mut fs = self.fs.borrow_mut();
2283 match fs.metadata(fs_ino) {
2284 Ok(meta) => {
2285 let mut attr = fs_to_attr(ino, &meta);
2286 attr.size = new_size;
2287 attr.blocks = new_size.div_ceil(512);
2288 reply.attr(&TTL, &attr);
2289 }
2290 Err(_) => reply.error(libc::EIO),
2291 }
2292 return;
2293 }
2294
2295 if rw_id >= 9_000_000 {
2297 let counter = rw_id - 9_000_000;
2298 let created_id = Self::created_overlay_id(counter);
2299 let session = self.session.borrow();
2300 if let Some(s) = session.as_ref() {
2301 if let Some(entry) = s.overlay.created.get(&created_id) {
2302 let attr = Self::overlay_created_attr(ino, entry.size, false);
2303 reply.attr(&TTL, &attr);
2304 return;
2305 }
2306 if let Some(entry) = s.overlay.dirs.get(&created_id) {
2307 let attr = Self::overlay_created_attr(ino, entry.size, true);
2308 reply.attr(&TTL, &attr);
2309 return;
2310 }
2311 }
2312 reply.error(libc::ENOENT);
2313 } else {
2314 let fs_ino = rw_id;
2315 let mut fs = self.fs.borrow_mut();
2316 match fs.metadata(fs_ino) {
2317 Ok(meta) => {
2318 let mut attr = fs_to_attr(ino, &meta);
2319 let session = self.session.borrow();
2320 if let Some(s) = session.as_ref() {
2321 let overlay_id = Self::modified_overlay_id(fs_ino);
2322 if s.overlay.modified.contains_key(&fs_ino) {
2323 if let Ok(data) = s.read_overlay_file(&overlay_id) {
2324 attr.size = data.len() as u64;
2325 attr.blocks = attr.size.div_ceil(512);
2326 }
2327 }
2328 }
2329 reply.attr(&TTL, &attr);
2330 }
2331 Err(_) => reply.error(libc::EIO),
2332 }
2333 }
2334 }
2335 _ => reply.error(libc::EROFS),
2337 }
2338 }
2339}
2340
2341#[cfg(test)]
2342mod tests {
2343 use super::*;
2344 use crate::{FsDeletedInode, FsDirEntry, FsError, FsRecoveryResult, FsResult, FsTimelineEvent};
2345 use fuser::FileType;
2346 use std::time::{Duration, UNIX_EPOCH};
2347
2348 #[test]
2353 fn virtual_dir_attr_is_directory() {
2354 let attr = virtual_dir_attr(1);
2355 assert_eq!(attr.ino, 1);
2356 assert_eq!(attr.kind, FileType::Directory);
2357 assert_eq!(attr.perm, 0o555);
2358 assert_eq!(attr.nlink, 2);
2359 assert_eq!(attr.size, 0);
2360 assert_eq!(attr.blocks, 0);
2361 assert_eq!(attr.uid, 0);
2362 assert_eq!(attr.gid, 0);
2363 assert_eq!(attr.blksize, 4096);
2364 assert_eq!(attr.atime, UNIX_EPOCH);
2365 assert_eq!(attr.mtime, UNIX_EPOCH);
2366 assert_eq!(attr.ctime, UNIX_EPOCH);
2367 assert_eq!(attr.crtime, UNIX_EPOCH);
2368 }
2369
2370 #[test]
2371 fn virtual_dir_attr_preserves_ino() {
2372 for ino in [1, 42, FUSE_ROOT_INO, FUSE_ORPHANS_INO, 999_999] {
2373 assert_eq!(virtual_dir_attr(ino).ino, ino);
2374 }
2375 }
2376
2377 #[test]
2382 fn virtual_file_attr_regular() {
2383 let attr = virtual_file_attr(100, 4096);
2384 assert_eq!(attr.ino, 100);
2385 assert_eq!(attr.kind, FileType::RegularFile);
2386 assert_eq!(attr.perm, 0o444);
2387 assert_eq!(attr.nlink, 1);
2388 assert_eq!(attr.size, 4096);
2389 assert_eq!(attr.blocks, 8); }
2391
2392 #[test]
2393 fn virtual_file_attr_zero_size() {
2394 let attr = virtual_file_attr(1, 0);
2395 assert_eq!(attr.size, 0);
2396 assert_eq!(attr.blocks, 0);
2397 }
2398
2399 #[test]
2400 fn virtual_file_attr_non_512_aligned() {
2401 let attr = virtual_file_attr(1, 1000);
2403 assert_eq!(attr.blocks, 2);
2404 }
2405
2406 #[test]
2411 fn timestamp_conversion_positive() {
2412 let ts = FsTimestamp {
2413 seconds: 1_700_000_000,
2414 nanoseconds: 500_000_000,
2415 };
2416 let st = ts_to_systime(&ts);
2417 let dur = st.duration_since(UNIX_EPOCH).unwrap();
2418 assert_eq!(dur.as_secs(), 1_700_000_000);
2419 assert_eq!(dur.subsec_nanos(), 500_000_000);
2420 }
2421
2422 #[test]
2423 fn timestamp_zero() {
2424 let ts = FsTimestamp {
2425 seconds: 0,
2426 nanoseconds: 0,
2427 };
2428 let st = ts_to_systime(&ts);
2429 assert_eq!(st, UNIX_EPOCH);
2430 }
2431
2432 #[test]
2433 fn timestamp_negative_clamps_to_epoch() {
2434 let ts = FsTimestamp {
2435 seconds: -1,
2436 nanoseconds: 0,
2437 };
2438 let st = ts_to_systime(&ts);
2439 assert_eq!(st, UNIX_EPOCH);
2440 }
2441
2442 #[test]
2443 fn timestamp_negative_large_clamps_to_epoch() {
2444 let ts = FsTimestamp {
2445 seconds: -1_000_000,
2446 nanoseconds: 999_999_999,
2447 };
2448 let st = ts_to_systime(&ts);
2449 assert_eq!(st, UNIX_EPOCH);
2450 }
2451
2452 #[test]
2453 fn timestamp_epoch_plus_one_second() {
2454 let ts = FsTimestamp {
2455 seconds: 1,
2456 nanoseconds: 0,
2457 };
2458 let st = ts_to_systime(&ts);
2459 assert_eq!(st, UNIX_EPOCH + Duration::from_secs(1));
2460 }
2461
2462 #[test]
2467 fn file_type_mapping_regular() {
2468 assert_eq!(
2469 fs_file_type_to_fuse(FsFileType::RegularFile),
2470 FileType::RegularFile
2471 );
2472 }
2473
2474 #[test]
2475 fn file_type_mapping_directory() {
2476 assert_eq!(
2477 fs_file_type_to_fuse(FsFileType::Directory),
2478 FileType::Directory
2479 );
2480 }
2481
2482 #[test]
2483 fn file_type_mapping_symlink() {
2484 assert_eq!(fs_file_type_to_fuse(FsFileType::Symlink), FileType::Symlink);
2485 }
2486
2487 #[test]
2488 fn file_type_mapping_chardev() {
2489 assert_eq!(
2490 fs_file_type_to_fuse(FsFileType::CharDevice),
2491 FileType::CharDevice
2492 );
2493 }
2494
2495 #[test]
2496 fn file_type_mapping_blockdev() {
2497 assert_eq!(
2498 fs_file_type_to_fuse(FsFileType::BlockDevice),
2499 FileType::BlockDevice
2500 );
2501 }
2502
2503 #[test]
2504 fn file_type_mapping_fifo() {
2505 assert_eq!(fs_file_type_to_fuse(FsFileType::Fifo), FileType::NamedPipe);
2506 }
2507
2508 #[test]
2509 fn file_type_mapping_socket() {
2510 assert_eq!(fs_file_type_to_fuse(FsFileType::Socket), FileType::Socket);
2511 }
2512
2513 #[test]
2514 fn file_type_mapping_unknown() {
2515 assert_eq!(
2516 fs_file_type_to_fuse(FsFileType::Unknown),
2517 FileType::RegularFile
2518 );
2519 }
2520
2521 #[test]
2526 fn fs_to_attr_regular_file() {
2527 let meta = FsMetadata {
2528 ino: 42,
2529 file_type: FsFileType::RegularFile,
2530 mode: 0o100_644,
2531 uid: 1000,
2532 gid: 1000,
2533 size: 100,
2534 links_count: 1,
2535 atime: FsTimestamp {
2536 seconds: 1_700_000_000,
2537 nanoseconds: 0,
2538 },
2539 mtime: FsTimestamp {
2540 seconds: 1_700_000_000,
2541 nanoseconds: 0,
2542 },
2543 ctime: FsTimestamp {
2544 seconds: 1_700_000_000,
2545 nanoseconds: 0,
2546 },
2547 crtime: FsTimestamp {
2548 seconds: 1_700_000_000,
2549 nanoseconds: 0,
2550 },
2551 allocated: true,
2552 };
2553 let attr = fs_to_attr(1012, &meta);
2554 assert_eq!(attr.ino, 1012);
2555 assert_eq!(attr.size, 100);
2556 assert_eq!(attr.kind, FileType::RegularFile);
2557 assert_eq!(attr.nlink, 1);
2558 assert_eq!(attr.perm, 0o644);
2559 assert_eq!(attr.uid, 1000);
2560 assert_eq!(attr.gid, 1000);
2561 }
2562
2563 #[test]
2564 fn fs_to_attr_directory() {
2565 let meta = FsMetadata {
2566 ino: 2,
2567 file_type: FsFileType::Directory,
2568 mode: 0o40755,
2569 uid: 0,
2570 gid: 0,
2571 size: 4096,
2572 links_count: 3,
2573 atime: FsTimestamp::default(),
2574 mtime: FsTimestamp::default(),
2575 ctime: FsTimestamp::default(),
2576 crtime: FsTimestamp::default(),
2577 allocated: true,
2578 };
2579 let attr = fs_to_attr(2000, &meta);
2580 assert_eq!(attr.kind, FileType::Directory);
2581 assert_eq!(attr.nlink, 3);
2582 assert_eq!(attr.perm, 0o755);
2583 }
2584
2585 #[test]
2586 fn fs_to_attr_symlink() {
2587 let meta = FsMetadata {
2588 ino: 10,
2589 file_type: FsFileType::Symlink,
2590 mode: 0o120_777,
2591 uid: 0,
2592 gid: 0,
2593 size: 11,
2594 links_count: 1,
2595 atime: FsTimestamp::default(),
2596 mtime: FsTimestamp::default(),
2597 ctime: FsTimestamp::default(),
2598 crtime: FsTimestamp::default(),
2599 allocated: true,
2600 };
2601 let attr = fs_to_attr(3000, &meta);
2602 assert_eq!(attr.kind, FileType::Symlink);
2603 assert_eq!(attr.perm, 0o777);
2604 }
2605
2606 #[test]
2607 fn fs_to_attr_blocks_calculation() {
2608 let meta = FsMetadata {
2609 ino: 42,
2610 file_type: FsFileType::RegularFile,
2611 mode: 0o100_644,
2612 uid: 0,
2613 gid: 0,
2614 size: 1000,
2615 links_count: 1,
2616 atime: FsTimestamp::default(),
2617 mtime: FsTimestamp::default(),
2618 ctime: FsTimestamp::default(),
2619 crtime: FsTimestamp::default(),
2620 allocated: true,
2621 };
2622 let attr = fs_to_attr(42, &meta);
2623 assert_eq!(attr.size, 1000);
2624 assert_eq!(attr.blocks, 2);
2625 }
2626
2627 #[test]
2628 fn fs_to_attr_blksize_always_4096() {
2629 let meta = FsMetadata {
2630 ino: 1,
2631 file_type: FsFileType::RegularFile,
2632 mode: 0o100_644,
2633 uid: 0,
2634 gid: 0,
2635 size: 0,
2636 links_count: 1,
2637 atime: FsTimestamp::default(),
2638 mtime: FsTimestamp::default(),
2639 ctime: FsTimestamp::default(),
2640 crtime: FsTimestamp::default(),
2641 allocated: true,
2642 };
2643 let attr = fs_to_attr(1, &meta);
2644 assert_eq!(attr.blksize, 4096);
2645 }
2646
2647 #[test]
2652 fn overlay_created_attr_regular_file() {
2653 let attr = ForensicFuseFs::overlay_created_attr(999, 512, false);
2654 assert_eq!(attr.ino, 999);
2655 assert_eq!(attr.size, 512);
2656 assert_eq!(attr.kind, FileType::RegularFile);
2657 assert_eq!(attr.perm, 0o644);
2658 assert_eq!(attr.nlink, 1);
2659 assert_eq!(attr.blocks, 1);
2660 }
2661
2662 #[test]
2663 fn overlay_created_attr_directory() {
2664 let attr = ForensicFuseFs::overlay_created_attr(888, 0, true);
2665 assert_eq!(attr.kind, FileType::Directory);
2666 assert_eq!(attr.perm, 0o755);
2667 }
2668
2669 #[test]
2674 fn modified_overlay_id_format() {
2675 assert_eq!(ForensicFuseFs::modified_overlay_id(42), "ino_42");
2676 assert_eq!(ForensicFuseFs::modified_overlay_id(0), "ino_0");
2677 assert_eq!(
2678 ForensicFuseFs::modified_overlay_id(9_999_999),
2679 "ino_9999999"
2680 );
2681 }
2682
2683 #[test]
2684 fn created_overlay_id_format() {
2685 assert_eq!(ForensicFuseFs::created_overlay_id(1), "new_1");
2686 assert_eq!(ForensicFuseFs::created_overlay_id(0), "new_0");
2687 }
2688
2689 fn root_child_names(layout: crate::MountLayout) -> Vec<String> {
2694 let mut fs = MockForensicFs;
2695 let root = fs.root_ino();
2696 root_children(layout, &mut fs, root, false)
2697 .unwrap()
2698 .iter()
2699 .map(|(_, n, _)| String::from_utf8_lossy(n).to_string())
2700 .collect()
2701 }
2702
2703 #[test]
2704 fn root_children_raw_lists_fs_tree_without_overlay() {
2705 let names = root_child_names(crate::MountLayout::Raw);
2706 assert!(names.contains(&"hello.txt".to_string()), "got {names:?}");
2707 assert!(names.contains(&"subdir".to_string()), "got {names:?}");
2708 assert!(
2709 !names
2710 .iter()
2711 .any(|n| n == "rw" || n == "deleted" || n == "ro"),
2712 "Raw root must have no overlay dirs: {names:?}"
2713 );
2714 }
2715
2716 #[test]
2717 fn root_children_diskoverlay_lists_virtual_dirs() {
2718 let names = root_child_names(crate::MountLayout::DiskOverlay);
2719 for d in ["ro", "rw", "journal", "metadata", "unallocated", "session"] {
2720 assert!(names.contains(&d.to_string()), "missing {d}: {names:?}");
2721 }
2722 assert!(!names.contains(&"deleted".to_string()), "got {names:?}");
2724 }
2725
2726 #[test]
2727 fn root_children_diskoverlay_appends_orphans_when_present() {
2728 let mut fs = MockForensicFs;
2729 let root = fs.root_ino();
2730 let with: Vec<String> = root_children(crate::MountLayout::DiskOverlay, &mut fs, root, true)
2731 .unwrap()
2732 .iter()
2733 .map(|(_, n, _)| String::from_utf8_lossy(n).to_string())
2734 .collect();
2735 assert!(with.contains(&"$Orphans".to_string()), "got {with:?}");
2736 }
2737
2738 #[test]
2743 fn virtual_dirs_has_expected_entries() {
2744 assert_eq!(VIRTUAL_DIRS.len(), 6);
2746 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "ro"));
2747 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "rw"));
2748 assert!(!VIRTUAL_DIRS.iter().any(|(_, name)| *name == "deleted"));
2749 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "journal"));
2750 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "metadata"));
2751 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "unallocated"));
2752 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "session"));
2753 }
2754
2755 #[test]
2756 fn virtual_dirs_ino_matches_constants() {
2757 for &(ino, name) in VIRTUAL_DIRS {
2758 match name {
2759 "ro" => assert_eq!(ino, FUSE_RO_INO),
2760 "rw" => assert_eq!(ino, FUSE_RW_INO),
2761 "journal" => assert_eq!(ino, FUSE_JOURNAL_INO),
2762 "metadata" => assert_eq!(ino, FUSE_METADATA_INO),
2763 "unallocated" => assert_eq!(ino, FUSE_UNALLOCATED_INO),
2764 "session" => assert_eq!(ino, FUSE_SESSION_INO),
2765 _ => panic!("unexpected virtual dir: {name}"),
2766 }
2767 }
2768 }
2769
2770 struct MockForensicFs;
2775
2776 impl crate::ForensicFs for MockForensicFs {
2777 fn root_ino(&self) -> u64 {
2778 2
2779 }
2780
2781 fn read_dir(&mut self, ino: u64) -> FsResult<Vec<FsDirEntry>> {
2782 match ino {
2783 2 => Ok(vec![
2784 FsDirEntry {
2785 inode: 2,
2786 name: b".".to_vec(),
2787 file_type: FsFileType::Directory,
2788 },
2789 FsDirEntry {
2790 inode: 2,
2791 name: b"..".to_vec(),
2792 file_type: FsFileType::Directory,
2793 },
2794 FsDirEntry {
2795 inode: 10,
2796 name: b"hello.txt".to_vec(),
2797 file_type: FsFileType::RegularFile,
2798 },
2799 FsDirEntry {
2800 inode: 11,
2801 name: b"subdir".to_vec(),
2802 file_type: FsFileType::Directory,
2803 },
2804 ]),
2805 11 => Ok(vec![
2806 FsDirEntry {
2807 inode: 11,
2808 name: b".".to_vec(),
2809 file_type: FsFileType::Directory,
2810 },
2811 FsDirEntry {
2812 inode: 2,
2813 name: b"..".to_vec(),
2814 file_type: FsFileType::Directory,
2815 },
2816 FsDirEntry {
2817 inode: 12,
2818 name: b"nested.txt".to_vec(),
2819 file_type: FsFileType::RegularFile,
2820 },
2821 ]),
2822 _ => Err(FsError::NotFound(format!("inode {ino}"))),
2823 }
2824 }
2825
2826 fn lookup(&mut self, parent_ino: u64, name: &[u8]) -> FsResult<Option<u64>> {
2827 let entries = self.read_dir(parent_ino)?;
2828 Ok(entries.iter().find(|e| e.name == name).map(|e| e.inode))
2829 }
2830
2831 fn metadata(&mut self, ino: u64) -> FsResult<FsMetadata> {
2832 let (file_type, size) = match ino {
2833 2 | 11 => (FsFileType::Directory, 4096),
2834 10 => (FsFileType::RegularFile, 12),
2835 12 => (FsFileType::RegularFile, 11),
2836 _ => return Err(FsError::NotFound(format!("inode {ino}"))),
2837 };
2838 Ok(FsMetadata {
2839 ino,
2840 file_type,
2841 mode: if file_type == FsFileType::Directory {
2842 0o40755
2843 } else {
2844 0o100_644
2845 },
2846 uid: 1000,
2847 gid: 1000,
2848 size: size as u64,
2849 links_count: if file_type == FsFileType::Directory {
2850 2
2851 } else {
2852 1
2853 },
2854 atime: FsTimestamp {
2855 seconds: 1_700_000_000,
2856 nanoseconds: 0,
2857 },
2858 mtime: FsTimestamp {
2859 seconds: 1_700_000_000,
2860 nanoseconds: 0,
2861 },
2862 ctime: FsTimestamp {
2863 seconds: 1_700_000_000,
2864 nanoseconds: 0,
2865 },
2866 crtime: FsTimestamp {
2867 seconds: 1_699_000_000,
2868 nanoseconds: 0,
2869 },
2870 allocated: true,
2871 })
2872 }
2873
2874 fn read_file(&mut self, ino: u64) -> FsResult<Vec<u8>> {
2875 match ino {
2876 10 => Ok(b"Hello, mock!".to_vec()),
2877 12 => Ok(b"Nested file".to_vec()),
2878 100..=103 => Ok(vec![0xAB; 100]),
2880 _ => Err(FsError::NotFound(format!("inode {ino}"))),
2882 }
2883 }
2884
2885 fn read_file_range(&mut self, ino: u64, offset: u64, len: u64) -> FsResult<Vec<u8>> {
2886 let data = self.read_file(ino)?;
2887 let start = (offset as usize).min(data.len());
2888 let end = (start + len as usize).min(data.len());
2889 Ok(data[start..end].to_vec())
2890 }
2891
2892 fn read_link(&mut self, _ino: u64) -> FsResult<Vec<u8>> {
2893 Err(FsError::NotFound("no symlinks in mock".to_string()))
2894 }
2895
2896 fn deleted_inodes(&mut self) -> FsResult<Vec<FsDeletedInode>> {
2897 Ok(vec![FsDeletedInode {
2898 ino: 99,
2899 file_type: FsFileType::RegularFile,
2900 size: 100,
2901 dtime: 1_700_001_000,
2902 recoverability: 0.75,
2903 }])
2904 }
2905
2906 fn deleted_nodes(&mut self) -> FsResult<Vec<crate::FsDeletedNode>> {
2907 let mk = |ino: u64,
2908 name: &[u8],
2909 parent: Option<u64>,
2910 mtime: i64,
2911 record_id: u64,
2912 allocation: crate::FsAllocation| {
2913 crate::FsDeletedNode {
2914 ino,
2915 name: name.to_vec(),
2916 parent_ino: parent,
2917 size: 100,
2918 file_type: FsFileType::RegularFile,
2919 allocation,
2920 record_id,
2921 atime: FsTimestamp::default(),
2922 mtime: FsTimestamp {
2923 seconds: mtime,
2924 nanoseconds: 0,
2925 },
2926 ctime: FsTimestamp::default(),
2927 crtime: FsTimestamp::default(),
2928 }
2929 };
2930 Ok(vec![
2931 mk(
2934 100,
2935 b"report.txt",
2936 Some(2),
2937 200,
2938 100,
2939 crate::FsAllocation::Deleted,
2940 ),
2941 mk(
2942 101,
2943 b"report.txt",
2944 Some(2),
2945 100,
2946 101,
2947 crate::FsAllocation::Deleted,
2948 ),
2949 mk(
2951 102,
2952 b"hello.txt",
2953 Some(2),
2954 300,
2955 102,
2956 crate::FsAllocation::Deleted,
2957 ),
2958 mk(103, b"", None, 150, 103, crate::FsAllocation::Orphan),
2960 mk(
2963 104,
2964 b"gone.txt",
2965 Some(2),
2966 250,
2967 104,
2968 crate::FsAllocation::Deleted,
2969 ),
2970 ])
2971 }
2972
2973 fn recover_file(&mut self, ino: u64) -> FsResult<FsRecoveryResult> {
2974 match ino {
2975 99 => Ok(FsRecoveryResult {
2976 ino: 99,
2977 data: vec![0xDE; 100],
2978 expected_size: 100,
2979 recovered_bytes: 100,
2980 recovery_percentage: 1.0,
2981 }),
2982 _ => Err(FsError::NotFound(format!("inode {ino}"))),
2983 }
2984 }
2985
2986 fn timeline(&mut self) -> FsResult<Vec<FsTimelineEvent>> {
2987 Ok(vec![FsTimelineEvent {
2988 timestamp: FsTimestamp {
2989 seconds: 1_700_000_000,
2990 nanoseconds: 0,
2991 },
2992 event_type: FsEventType::Created,
2993 inode: 10,
2994 size: 12,
2995 uid: 1000,
2996 gid: 1000,
2997 }])
2998 }
2999
3000 fn fs_info(&self) -> FsResult<serde_json::Value> {
3001 Ok(serde_json::json!({ "filesystem": "mock", "block_size": 4096 }))
3002 }
3003
3004 fn block_size(&self) -> u64 {
3005 4096
3006 }
3007 }
3008
3009 fn make_mock_fuse() -> ForensicFuseFs {
3010 ForensicFuseFs::new(
3011 Box::new(MockForensicFs),
3012 None,
3013 crate::MountLayout::DiskOverlay,
3014 crate::DeletedMode::Latest,
3015 )
3016 }
3017
3018 #[test]
3019 fn mock_ensure_deleted_cache() {
3020 let fuse = make_mock_fuse();
3021 assert!(fuse.deleted_cache.borrow().is_none());
3022 fuse.ensure_deleted_cache();
3023 let cache = fuse.deleted_cache.borrow();
3024 let entries = cache.as_ref().expect("cache should be populated");
3025 assert_eq!(entries.len(), 5);
3027 assert!(entries
3028 .iter()
3029 .any(|e| e.fs_ino == 100 && e.name == "report.txt"));
3030 }
3031
3032 #[test]
3033 fn mock_ensure_metadata_cache() {
3034 let fuse = make_mock_fuse();
3035 assert!(fuse.metadata_cache.borrow().is_none());
3036 fuse.ensure_metadata_cache();
3037 let cache = fuse.metadata_cache.borrow();
3038 let mc = cache.as_ref().expect("cache should be populated");
3039 let sb_str = String::from_utf8_lossy(&mc.superblock_json);
3040 assert!(
3041 sb_str.contains("mock"),
3042 "superblock_json should contain 'mock': {sb_str}"
3043 );
3044 assert!(
3045 !mc.timeline_jsonl.is_empty(),
3046 "timeline_jsonl should not be empty"
3047 );
3048 }
3049
3050 #[test]
3051 fn mock_root_ino_stored() {
3052 let fuse = make_mock_fuse();
3053 assert_eq!(fuse.root_ino, 2);
3054 }
3055
3056 #[test]
3057 fn mock_has_session_false() {
3058 let fuse = make_mock_fuse();
3059 assert!(!fuse.has_session());
3060 }
3061
3062 #[test]
3063 fn mock_read_file_through_fs() {
3064 let fuse = make_mock_fuse();
3065 let mut fs = fuse.fs.borrow_mut();
3066 let data = fs.read_file(10).expect("read_file(10) should succeed");
3067 assert_eq!(data, b"Hello, mock!");
3068 }
3069
3070 #[test]
3071 fn mock_read_file_range_through_fs() {
3072 let fuse = make_mock_fuse();
3073 let mut fs = fuse.fs.borrow_mut();
3074 let data = fs
3075 .read_file_range(10, 0, 5)
3076 .expect("read_file_range should succeed");
3077 assert_eq!(data, b"Hello");
3078 }
3079
3080 #[test]
3081 fn mock_lookup_through_fs() {
3082 let fuse = make_mock_fuse();
3083 let mut fs = fuse.fs.borrow_mut();
3084 let result = fs.lookup(2, b"hello.txt").expect("lookup should succeed");
3085 assert_eq!(result, Some(10));
3086 }
3087
3088 #[test]
3089 fn mock_metadata_through_fs() {
3090 let fuse = make_mock_fuse();
3091 let mut fs = fuse.fs.borrow_mut();
3092 let meta = fs.metadata(10).expect("metadata(10) should succeed");
3093 assert_eq!(meta.file_type, FsFileType::RegularFile);
3094 assert_eq!(meta.size, 12);
3095 assert_eq!(meta.ino, 10);
3096 }
3097
3098 #[test]
3099 fn mock_fs_to_attr() {
3100 let fuse = make_mock_fuse();
3101 let meta = {
3102 let mut fs = fuse.fs.borrow_mut();
3103 fs.metadata(10).expect("metadata(10) should succeed")
3104 };
3105 let attr = fs_to_attr(ro_ino(10), &meta);
3106 assert_eq!(attr.ino, ro_ino(10));
3107 assert_eq!(attr.kind, FileType::RegularFile);
3108 assert_eq!(attr.size, 12);
3109 assert_eq!(attr.perm, 0o644);
3110 assert_eq!(attr.uid, 1000);
3111 assert_eq!(attr.gid, 1000);
3112 assert_eq!(attr.nlink, 1);
3113 assert_eq!(attr.blksize, 4096);
3114 let expected_atime = UNIX_EPOCH + Duration::from_secs(1_700_000_000);
3115 assert_eq!(attr.atime, expected_atime);
3116 let expected_crtime = UNIX_EPOCH + Duration::from_secs(1_699_000_000);
3117 assert_eq!(attr.crtime, expected_crtime);
3118 }
3119
3120 #[test]
3121 fn mock_timeline_through_fs() {
3122 let fuse = make_mock_fuse();
3123 let mut fs = fuse.fs.borrow_mut();
3124 let events = fs.timeline().expect("timeline should succeed");
3125 assert_eq!(events.len(), 1);
3126 assert_eq!(events[0].event_type, FsEventType::Created);
3127 assert_eq!(events[0].inode, 10);
3128 assert_eq!(events[0].size, 12);
3129 }
3130
3131 #[test]
3132 fn mock_ensure_journal_cache_empty() {
3133 let fuse = make_mock_fuse();
3134 assert!(fuse.journal_cache.borrow().is_none());
3135 fuse.ensure_journal_cache();
3136 let cache = fuse.journal_cache.borrow();
3137 let entries = cache.as_ref().expect("cache should be populated");
3138 assert!(
3139 entries.is_empty(),
3140 "mock has no journal_transactions override, should be empty"
3141 );
3142 }
3143
3144 fn make_mock_fuse_mode(mode: crate::DeletedMode) -> ForensicFuseFs {
3149 ForensicFuseFs::new(
3150 Box::new(MockForensicFs),
3151 None,
3152 crate::MountLayout::DiskOverlay,
3153 mode,
3154 )
3155 }
3156
3157 #[test]
3158 fn filename_safe_utc_has_no_colons() {
3159 assert_eq!(filename_safe_utc(1_700_000_000), "2023-11-14T22-13-20Z");
3161 }
3162
3163 #[test]
3164 fn deleted_cache_latest_places_newest_in_place() {
3165 let fuse = make_mock_fuse_mode(crate::DeletedMode::Latest);
3166 fuse.ensure_deleted_cache();
3167 let cache = fuse.deleted_cache.borrow();
3168 let entries = cache.as_ref().expect("cache populated");
3169 let by = |ino: u64| {
3170 entries
3171 .iter()
3172 .find(|e| e.fs_ino == ino)
3173 .unwrap_or_else(|| panic!("no cache entry for ino {ino}"))
3174 };
3175 let a = by(100);
3177 assert_eq!(a.name, "report.txt");
3178 assert!(!a.orphan);
3179 let b = by(101);
3181 assert!(b.orphan);
3182 assert!(b.name.starts_with("report.txt@"), "got {}", b.name);
3183 assert!(b.name.ends_with("~101"), "got {}", b.name);
3184 assert!(by(102).orphan);
3186 assert!(by(103).orphan);
3188 let g = by(104);
3190 assert_eq!(g.name, "gone.txt");
3191 assert!(!g.orphan);
3192 assert!(!g.readable);
3193 }
3194
3195 #[test]
3196 fn deleted_cache_off_is_empty() {
3197 let fuse = make_mock_fuse_mode(crate::DeletedMode::Off);
3198 fuse.ensure_deleted_cache();
3199 assert!(fuse
3200 .deleted_cache
3201 .borrow()
3202 .as_ref()
3203 .expect("cache populated")
3204 .is_empty());
3205 }
3206
3207 #[test]
3208 fn deleted_cache_all_routes_everything_to_orphans() {
3209 let fuse = make_mock_fuse_mode(crate::DeletedMode::All);
3210 fuse.ensure_deleted_cache();
3211 let cache = fuse.deleted_cache.borrow();
3212 let entries = cache.as_ref().expect("cache populated");
3213 assert_eq!(entries.len(), 5);
3214 assert!(
3215 entries.iter().all(|e| e.orphan),
3216 "All -> every instance orphan"
3217 );
3218 let a = entries.iter().find(|e| e.fs_ino == 100).unwrap();
3219 assert!(a.name.starts_with("report.txt@"), "got {}", a.name);
3220 }
3221
3222 #[test]
3223 fn deleted_cache_never_fabricates_unknown_names() {
3224 let fuse = make_mock_fuse_mode(crate::DeletedMode::Latest);
3225 fuse.ensure_deleted_cache();
3226 let cache = fuse.deleted_cache.borrow();
3227 for e in cache.as_ref().expect("cache populated") {
3228 assert!(!e.name.ends_with("_unknown"), "fabricated: {}", e.name);
3229 }
3230 }
3231
3232 #[test]
3233 fn timeline_jsonl_carries_every_deleted_instance() {
3234 let fuse = make_mock_fuse_mode(crate::DeletedMode::Latest);
3235 fuse.ensure_metadata_cache();
3236 let cache = fuse.metadata_cache.borrow();
3237 let mc = cache.as_ref().expect("metadata cache populated");
3238 let text = String::from_utf8_lossy(&mc.timeline_jsonl);
3239 let rows: Vec<serde_json::Value> = text
3241 .lines()
3242 .filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
3243 .filter(|v| v.get("placement").is_some())
3244 .collect();
3245 assert_eq!(rows.len(), 5, "one row per deleted instance");
3247 let report: Vec<_> = rows.iter().filter(|r| r["name"] == "report.txt").collect();
3249 assert_eq!(report.len(), 2, "both report.txt instances present");
3250 assert!(report.iter().any(|r| r["placement"] == "in-place"));
3251 assert!(report.iter().any(|r| r["placement"] == "orphan"));
3252 let r = &rows[0];
3254 for f in ["path", "name", "record_id", "allocation", "status", "macb"] {
3255 assert!(r.get(f).is_some(), "row missing {f}: {r}");
3256 }
3257 assert!(r["macb"].get("modified").is_some());
3258 assert!(rows
3260 .iter()
3261 .any(|r| r["name"] == "gone.txt" && r["status"] == "unreadable"));
3262 }
3263
3264 #[test]
3270 fn in_place_children_injected_under_parent() {
3271 let fuse = make_mock_fuse_mode(crate::DeletedMode::Latest);
3272 fuse.ensure_deleted_cache();
3273 let cache = fuse.deleted_cache.borrow();
3274 let entries = cache.as_ref().expect("cache populated");
3275 let kids = deleted_in_place_children(entries, 2);
3276 let names: Vec<&str> = kids.iter().map(|(_, n)| n.as_str()).collect();
3279 assert!(names.contains(&"report.txt"), "got {names:?}");
3280 assert!(names.contains(&"gone.txt"), "got {names:?}");
3281 assert_eq!(
3282 kids.len(),
3283 2,
3284 "only the two in-place deletes under parent 2"
3285 );
3286 assert!(
3288 !names.iter().any(|n| n.contains("(deleted)")),
3289 "got {names:?}"
3290 );
3291 assert!(kids.iter().any(|(ino, _)| *ino == deleted_ino(100)));
3293 assert!(deleted_in_place_children(entries, 11).is_empty());
3295 assert!(!kids.iter().any(|(ino, _)| *ino == deleted_ino(101)));
3297 }
3298
3299 #[test]
3305 fn orphans_dir_is_top_level_only_when_present() {
3306 use crate::inode_map::FUSE_ORPHANS_INO;
3307 let with = root_dir_listing(true);
3310 assert!(
3311 with.iter()
3312 .any(|&(ino, n)| ino == FUSE_ORPHANS_INO && n == "$Orphans"),
3313 "root should list $Orphans when orphans exist: {with:?}"
3314 );
3315 assert!(
3316 !with.iter().any(|&(_, n)| n == "deleted"),
3317 "the flat deleted/ dir is removed in v2: {with:?}"
3318 );
3319 for want in ["ro", "rw", "metadata", "session"] {
3321 assert!(
3322 with.iter().any(|&(_, n)| n == want),
3323 "missing {want}: {with:?}"
3324 );
3325 }
3326 let without = root_dir_listing(false);
3328 assert!(
3329 !without.iter().any(|&(_, n)| n == "$Orphans"),
3330 "no $Orphans without orphan entries: {without:?}"
3331 );
3332 }
3333
3334 #[test]
3335 fn cache_has_orphans_reflects_placement() {
3336 let fuse = make_mock_fuse_mode(crate::DeletedMode::Latest);
3337 fuse.ensure_deleted_cache();
3338 let cache = fuse.deleted_cache.borrow();
3339 let entries = cache.as_ref().expect("cache populated");
3340 assert!(cache_has_orphans(entries));
3342 assert!(!cache_has_orphans(&[]));
3343 }
3344
3345 #[test]
3351 fn deleted_xattr_names_are_the_marking_schema() {
3352 let names = deleted_xattr_names();
3353 for want in [
3354 "user.4n6.status",
3355 "user.4n6.macb.modified",
3356 "user.4n6.macb.accessed",
3357 "user.4n6.macb.changed",
3358 "user.4n6.macb.born",
3359 ] {
3360 assert!(names.contains(&want), "missing {want}: {names:?}");
3361 }
3362 assert_eq!(names.len(), 5);
3363 }
3364
3365 #[test]
3366 fn deleted_xattr_value_status_and_macb() {
3367 let fuse = make_mock_fuse_mode(crate::DeletedMode::Latest);
3368 fuse.ensure_deleted_cache();
3369 let cache = fuse.deleted_cache.borrow();
3370 let entries = cache.as_ref().expect("cache populated");
3371 let by = |ino: u64| entries.iter().find(|e| e.fs_ino == ino).unwrap();
3372
3373 let a = by(100);
3375 assert_eq!(
3376 deleted_xattr_value(a, "user.4n6.status").as_deref(),
3377 Some(b"deleted".as_ref())
3378 );
3379 assert_eq!(
3381 deleted_xattr_value(a, "user.4n6.macb.modified").as_deref(),
3382 Some(b"1970-01-01T00:03:20Z".as_ref())
3383 );
3384 assert_eq!(
3386 deleted_xattr_value(by(103), "user.4n6.status").as_deref(),
3387 Some(b"orphan".as_ref())
3388 );
3389 assert!(deleted_xattr_value(a, "user.4n6.nope").is_none());
3391 }
3392
3393 #[test]
3399 fn deleted_cow_base_yields_recovered_bytes() {
3400 let fuse = make_mock_fuse_mode(crate::DeletedMode::Latest);
3401 fuse.ensure_deleted_cache();
3402 let cache = fuse.deleted_cache.borrow();
3403 let entries = cache.as_ref().expect("cache populated");
3404
3405 let base = deleted_cow_base(entries, 100).expect("readable -> copy-up base");
3408 assert_eq!(base, vec![0xAB; 100]);
3409
3410 assert!(deleted_cow_base(entries, 104).is_none());
3413
3414 assert!(deleted_cow_base(entries, 999).is_none());
3416 }
3417}