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 s = session.as_ref().unwrap();
910 let status = serde_json::json!({
911 "image_path": s.metadata.image_path,
912 "image_sha256": s.metadata.image_sha256,
913 "created": s.metadata.created,
914 });
915 let data = serde_json::to_string_pretty(&status)
916 .unwrap_or_default()
917 .into_bytes();
918 let fuse_ino = metadata_ino(100);
919 let attr = virtual_file_attr(fuse_ino, data.len() as u64);
920 reply.entry(&TTL, &attr, 0);
921 return;
922 }
923 reply.error(libc::ENOENT);
924 return;
925 }
926
927 if let Some(fs_parent) = self.rw_parent_to_fs(parent) {
929 let deleted_hit = self.lookup_deleted_in_place(fs_parent, name_bytes);
932
933 if let Some((id, counter, is_dir)) = self.find_created_by_name(fs_parent, name_bytes) {
935 let session = self.session.borrow();
936 let session = session.as_ref().unwrap();
937 let entry = if is_dir {
938 session.overlay.dirs.get(&id)
939 } else {
940 session.overlay.created.get(&id)
941 };
942 if let Some(entry) = entry {
943 let fuse_ino = rw_ino(counter + 9_000_000);
944 let attr = Self::overlay_created_attr(fuse_ino, entry.size, is_dir);
945 reply.entry(&TTL, &attr, 0);
946 return;
947 }
948 }
949
950 {
952 let mut fs = self.fs.borrow_mut();
953 match fs.lookup(fs_parent, name_bytes) {
954 Ok(Some(child_ino)) => {
955 if self.is_whiteout(child_ino) {
957 reply.error(libc::ENOENT);
958 return;
959 }
960
961 let session = self.session.borrow();
963 let overlay_id = Self::modified_overlay_id(child_ino);
964 if let Some(s) = session.as_ref() {
965 if s.overlay.modified.contains_key(&child_ino) {
966 if let Ok(meta) = fs.metadata(child_ino) {
967 let fuse_ino = rw_ino(child_ino);
968 let mut attr = fs_to_attr(fuse_ino, &meta);
969 if let Ok(data) = s.read_overlay_file(&overlay_id) {
970 attr.size = data.len() as u64;
971 attr.blocks = attr.size.div_ceil(512);
972 }
973 reply.entry(&TTL, &attr, 0);
974 return;
975 }
976 reply.error(libc::EIO);
977 return;
978 }
979 }
980
981 match fs.metadata(child_ino) {
983 Ok(meta) => {
984 let fuse_ino = rw_ino(child_ino);
985 reply.entry(&TTL, &fs_to_attr(fuse_ino, &meta), 0);
986 }
987 Err(_) => reply.error(libc::EIO),
988 }
989 return;
990 }
991 Ok(None) => {
992 if let Some((fino, size)) = deleted_hit {
993 reply.entry(&TTL, &virtual_file_attr(fino, size), 0);
994 return;
995 }
996 reply.error(libc::ENOENT);
997 return;
998 }
999 Err(_) => {
1000 reply.error(libc::EIO);
1001 return;
1002 }
1003 }
1004 }
1005 }
1006
1007 let fs_parent = match parent {
1010 FUSE_RO_INO => self.root_ino,
1011 FUSE_ROOT_INO if self.layout == crate::MountLayout::Raw => self.root_ino,
1012 _ => {
1013 if let InodeNamespace::Ro(ino) = decode_fuse_ino(parent) {
1014 ino
1015 } else {
1016 reply.error(libc::ENOENT);
1017 return;
1018 }
1019 }
1020 };
1021
1022 let deleted_hit = self.lookup_deleted_in_place(fs_parent, name_bytes);
1024
1025 let mut fs = self.fs.borrow_mut();
1026 match fs.lookup(fs_parent, name_bytes) {
1027 Ok(Some(child_ino)) => match fs.metadata(child_ino) {
1028 Ok(meta) => {
1029 let fuse_ino = ro_ino(child_ino);
1030 reply.entry(&TTL, &fs_to_attr(fuse_ino, &meta), 0);
1031 }
1032 Err(_) => reply.error(libc::EIO),
1033 },
1034 Ok(None) => {
1035 if let Some((fino, size)) = deleted_hit {
1036 reply.entry(&TTL, &virtual_file_attr(fino, size), 0);
1037 } else {
1038 reply.error(libc::ENOENT);
1039 }
1040 }
1041 Err(_) => reply.error(libc::EIO),
1042 }
1043 }
1044
1045 fn getattr(&mut self, _req: &Request, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
1046 if ino == FUSE_ROOT_INO {
1048 reply.attr(&TTL, &virtual_dir_attr(FUSE_ROOT_INO));
1049 return;
1050 }
1051
1052 if (FUSE_RO_INO..=FUSE_SESSION_INO).contains(&ino) || ino == FUSE_ORPHANS_INO {
1054 let mut attr = virtual_dir_attr(ino);
1055 if ino == FUSE_RW_INO && self.has_session() {
1056 attr.perm = 0o755;
1057 }
1058 reply.attr(&TTL, &attr);
1059 return;
1060 }
1061
1062 match decode_fuse_ino(ino) {
1063 InodeNamespace::Deleted(fs_ino) => {
1064 self.ensure_deleted_cache();
1065 let cache = self.deleted_cache.borrow();
1066 if let Some(entries) = cache.as_ref() {
1067 if let Some(entry) = entries.iter().find(|e| e.fs_ino == fs_ino) {
1068 let mut size = entry.size;
1071 let session = self.session.borrow();
1072 if let Some(s) = session.as_ref() {
1073 if s.overlay.modified.contains_key(&fs_ino) {
1074 let overlay_id = Self::modified_overlay_id(fs_ino);
1075 if let Ok(d) = s.read_overlay_file(&overlay_id) {
1076 size = d.len() as u64;
1077 }
1078 }
1079 }
1080 reply.attr(&TTL, &virtual_file_attr(ino, size));
1081 return;
1082 }
1083 }
1084 reply.error(libc::ENOENT);
1085 }
1086 InodeNamespace::Metadata(id) => {
1087 self.ensure_metadata_cache();
1088 let cache = self.metadata_cache.borrow();
1089 if let Some(mc) = cache.as_ref() {
1090 match id {
1091 1 => {
1092 reply.attr(
1093 &TTL,
1094 &virtual_file_attr(ino, mc.superblock_json.len() as u64),
1095 );
1096 }
1097 2 => {
1098 reply.attr(
1099 &TTL,
1100 &virtual_file_attr(ino, mc.timeline_jsonl.len() as u64),
1101 );
1102 }
1103 100 => {
1104 if self.has_session() {
1106 let session = self.session.borrow();
1107 let s = session.as_ref().unwrap();
1108 let status = serde_json::json!({
1109 "image_path": s.metadata.image_path,
1110 "image_sha256": s.metadata.image_sha256,
1111 "created": s.metadata.created,
1112 });
1113 let data =
1114 serde_json::to_string_pretty(&status).unwrap_or_default();
1115 reply.attr(&TTL, &virtual_file_attr(ino, data.len() as u64));
1116 } else {
1117 reply.error(libc::ENOENT);
1118 }
1119 }
1120 _ => reply.error(libc::ENOENT),
1121 }
1122 } else {
1123 reply.error(libc::ENOENT);
1124 }
1125 }
1126 InodeNamespace::Journal(seq) => {
1127 self.ensure_journal_cache();
1128 let cache = self.journal_cache.borrow();
1129 if let Some(entries) = cache.as_ref() {
1130 if entries.iter().any(|e| e.sequence == seq) {
1131 reply.attr(&TTL, &virtual_dir_attr(ino));
1132 } else {
1133 reply.error(libc::ENOENT);
1134 }
1135 } else {
1136 reply.error(libc::ENOENT);
1137 }
1138 }
1139 InodeNamespace::Unallocated(range_id) => {
1140 self.ensure_unallocated_cache();
1141 let cache = self.unallocated_cache.borrow();
1142 if let Some(entries) = cache.as_ref() {
1143 if let Some(entry) = entries.get(range_id as usize) {
1144 let block_size = self.fs.borrow().block_size();
1145 let size = entry.length * block_size;
1146 reply.attr(&TTL, &virtual_file_attr(ino, size));
1147 } else {
1148 reply.error(libc::ENOENT);
1149 }
1150 } else {
1151 reply.error(libc::ENOENT);
1152 }
1153 }
1154 InodeNamespace::Ro(fs_ino) => {
1155 let mut fs = self.fs.borrow_mut();
1156 match fs.metadata(fs_ino) {
1157 Ok(meta) => reply.attr(&TTL, &fs_to_attr(ino, &meta)),
1158 Err(_) => reply.error(libc::EIO),
1159 }
1160 }
1161 InodeNamespace::Rw(rw_id) => {
1162 if rw_id >= 9_000_000 {
1164 let counter = rw_id - 9_000_000;
1165 let created_id = Self::created_overlay_id(counter);
1166 let session = self.session.borrow();
1167 if let Some(s) = session.as_ref() {
1168 if let Some(entry) = s.overlay.created.get(&created_id) {
1169 let attr = Self::overlay_created_attr(ino, entry.size, false);
1170 reply.attr(&TTL, &attr);
1171 return;
1172 }
1173 if let Some(entry) = s.overlay.dirs.get(&created_id) {
1174 let attr = Self::overlay_created_attr(ino, entry.size, true);
1175 reply.attr(&TTL, &attr);
1176 return;
1177 }
1178 }
1179 reply.error(libc::ENOENT);
1180 return;
1181 }
1182
1183 let fs_ino = rw_id;
1185 let mut fs = self.fs.borrow_mut();
1186 match fs.metadata(fs_ino) {
1187 Ok(meta) => {
1188 let mut attr = fs_to_attr(ino, &meta);
1189 let session = self.session.borrow();
1191 if let Some(s) = session.as_ref() {
1192 let overlay_id = Self::modified_overlay_id(fs_ino);
1193 if s.overlay.modified.contains_key(&fs_ino) {
1194 if let Ok(data) = s.read_overlay_file(&overlay_id) {
1195 attr.size = data.len() as u64;
1196 attr.blocks = attr.size.div_ceil(512);
1197 }
1198 }
1199 }
1200 reply.attr(&TTL, &attr);
1201 }
1202 Err(_) => reply.error(libc::EIO),
1203 }
1204 }
1205 _ => reply.error(libc::ENOENT),
1206 }
1207 }
1208
1209 fn getxattr(&mut self, _req: &Request, ino: u64, name: &OsStr, size: u32, reply: ReplyXattr) {
1214 let value = if let InodeNamespace::Deleted(fs_ino) = decode_fuse_ino(ino) {
1215 self.ensure_deleted_cache();
1216 let cache = self.deleted_cache.borrow();
1217 let attr_name = name.to_str();
1218 cache.as_ref().and_then(|entries| {
1219 let n = attr_name?;
1220 let e = entries.iter().find(|e| e.fs_ino == fs_ino)?;
1221 deleted_xattr_value(e, n)
1222 })
1223 } else {
1224 None
1225 };
1226 match value {
1227 Some(v) => {
1228 if size == 0 {
1229 reply.size(v.len() as u32);
1230 } else if (v.len() as u32) <= size {
1231 reply.data(&v);
1232 } else {
1233 reply.error(libc::ERANGE);
1234 }
1235 }
1236 None => reply.error(libc::ENODATA),
1237 }
1238 }
1239
1240 fn listxattr(&mut self, _req: &Request, ino: u64, size: u32, reply: ReplyXattr) {
1244 let mut buf: Vec<u8> = Vec::new();
1245 if let InodeNamespace::Deleted(fs_ino) = decode_fuse_ino(ino) {
1246 self.ensure_deleted_cache();
1247 let cache = self.deleted_cache.borrow();
1248 let present = cache
1249 .as_ref()
1250 .is_some_and(|entries| entries.iter().any(|e| e.fs_ino == fs_ino));
1251 if present {
1252 for n in deleted_xattr_names() {
1253 buf.extend_from_slice(n.as_bytes());
1254 buf.push(0);
1255 }
1256 }
1257 }
1258 if size == 0 {
1259 reply.size(buf.len() as u32);
1260 } else if (buf.len() as u32) <= size {
1261 reply.data(&buf);
1262 } else {
1263 reply.error(libc::ERANGE);
1264 }
1265 }
1266
1267 fn readdir(
1268 &mut self,
1269 _req: &Request,
1270 ino: u64,
1271 _fh: u64,
1272 offset: i64,
1273 mut reply: ReplyDirectory,
1274 ) {
1275 let offset = offset as usize;
1276
1277 if ino == FUSE_ROOT_INO {
1280 let has_orphans = self.cache_orphans_present();
1281 let mut entries: Vec<(u64, FileType, String)> = vec![
1282 (FUSE_ROOT_INO, FileType::Directory, ".".to_string()),
1283 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1284 ];
1285 {
1286 let mut fs = self.fs.borrow_mut();
1287 let Ok(children) =
1288 root_children(self.layout, &mut **fs, self.root_ino, has_orphans)
1289 else {
1290 reply.error(libc::EIO);
1291 return;
1292 };
1293 for (fino, name, kind) in children {
1294 entries.push((fino, kind, String::from_utf8_lossy(&name).into_owned()));
1295 }
1296 }
1297 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1298 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1299 break;
1300 }
1301 }
1302 reply.ok();
1303 return;
1304 }
1305
1306 if let Some(fs_dir_ino) = match ino {
1308 FUSE_RW_INO => Some(self.root_ino),
1309 _ => match decode_fuse_ino(ino) {
1310 InodeNamespace::Rw(rw_id) if rw_id < 9_000_000 => Some(rw_id),
1311 _ => None,
1312 },
1313 } {
1314 self.ensure_deleted_cache();
1318 let injected: Vec<(u64, FileType, String)> = {
1319 let cache = self.deleted_cache.borrow();
1320 cache.as_ref().map_or_else(Vec::new, |c| {
1321 deleted_in_place_children(c, fs_dir_ino)
1322 .into_iter()
1323 .map(|(dino, name)| (dino, FileType::RegularFile, name))
1324 .collect()
1325 })
1326 };
1327 let mut fs = self.fs.borrow_mut();
1328 match fs.read_dir(fs_dir_ino) {
1329 Ok(entries) => {
1330 let session = self.session.borrow();
1331 let mut fuse_entries: Vec<(u64, FileType, String)> = Vec::new();
1332
1333 for e in &entries {
1334 let name = e.name_str();
1335 let child_ino = e.inode;
1336
1337 if let Some(s) = session.as_ref() {
1339 if name != "." && name != ".." && s.overlay.deleted.contains(&child_ino)
1340 {
1341 continue;
1342 }
1343 }
1344
1345 let fuse_ino = if name == "." || name == ".." {
1346 if fs_dir_ino == self.root_ino && name == "." {
1347 FUSE_RW_INO
1348 } else if fs_dir_ino == self.root_ino && name == ".." {
1349 FUSE_ROOT_INO
1350 } else {
1351 rw_ino(child_ino)
1352 }
1353 } else {
1354 rw_ino(child_ino)
1355 };
1356 let kind = fs_file_type_to_fuse(e.file_type);
1357 fuse_entries.push((fuse_ino, kind, name));
1358 }
1359
1360 if let Some(s) = session.as_ref() {
1362 for (id, entry) in &s.overlay.created {
1363 if entry.parent_ino == fs_dir_ino {
1364 if let Some(counter) =
1365 id.strip_prefix("new_").and_then(|s| s.parse::<u64>().ok())
1366 {
1367 let fuse_ino = rw_ino(counter + 9_000_000);
1368 fuse_entries.push((
1369 fuse_ino,
1370 FileType::RegularFile,
1371 entry.name.clone(),
1372 ));
1373 }
1374 }
1375 }
1376 for (id, entry) in &s.overlay.dirs {
1377 if entry.parent_ino == fs_dir_ino {
1378 if let Some(counter) =
1379 id.strip_prefix("new_").and_then(|s| s.parse::<u64>().ok())
1380 {
1381 let fuse_ino = rw_ino(counter + 9_000_000);
1382 fuse_entries.push((
1383 fuse_ino,
1384 FileType::Directory,
1385 entry.name.clone(),
1386 ));
1387 }
1388 }
1389 }
1390 }
1391
1392 fuse_entries.extend(injected);
1394
1395 for (i, (entry_ino, kind, name)) in fuse_entries.iter().enumerate().skip(offset)
1396 {
1397 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1398 break;
1399 }
1400 }
1401 reply.ok();
1402 }
1403 Err(_) => reply.error(libc::EIO),
1404 }
1405 return;
1406 }
1407
1408 if ino == FUSE_ORPHANS_INO {
1412 self.ensure_deleted_cache();
1413 let cache = self.deleted_cache.borrow();
1414 let mut entries: Vec<(u64, FileType, String)> = vec![
1415 (FUSE_ORPHANS_INO, FileType::Directory, ".".to_string()),
1416 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1417 ];
1418 if let Some(cached) = cache.as_ref() {
1419 for entry in cached.iter().filter(|e| e.orphan) {
1420 entries.push((
1421 deleted_ino(entry.fs_ino),
1422 FileType::RegularFile,
1423 entry.name.clone(),
1424 ));
1425 }
1426 }
1427 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1428 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1429 break;
1430 }
1431 }
1432 reply.ok();
1433 return;
1434 }
1435
1436 if ino == FUSE_JOURNAL_INO {
1438 self.ensure_journal_cache();
1439 let cache = self.journal_cache.borrow();
1440 let mut entries: Vec<(u64, FileType, String)> = vec![
1441 (FUSE_JOURNAL_INO, FileType::Directory, ".".to_string()),
1442 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1443 ];
1444 if let Some(cached) = cache.as_ref() {
1445 for entry in cached {
1446 entries.push((
1447 journal_ino(entry.sequence),
1448 FileType::Directory,
1449 entry.name.clone(),
1450 ));
1451 }
1452 }
1453 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1454 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1455 break;
1456 }
1457 }
1458 reply.ok();
1459 return;
1460 }
1461
1462 if let InodeNamespace::Journal(_seq) = decode_fuse_ino(ino) {
1464 if offset == 0 {
1465 let _ = reply.add(ino, 1, FileType::Directory, ".");
1466 let _ = reply.add(FUSE_JOURNAL_INO, 2, FileType::Directory, "..");
1467 }
1468 reply.ok();
1469 return;
1470 }
1471
1472 if ino == FUSE_METADATA_INO {
1474 self.ensure_metadata_cache();
1475 let cache = self.metadata_cache.borrow();
1476 let mut entries: Vec<(u64, FileType, String)> = vec![
1477 (FUSE_METADATA_INO, FileType::Directory, ".".to_string()),
1478 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1479 ];
1480 if cache.is_some() {
1481 entries.push((
1482 metadata_ino(1),
1483 FileType::RegularFile,
1484 "superblock.json".to_string(),
1485 ));
1486 entries.push((
1487 metadata_ino(2),
1488 FileType::RegularFile,
1489 "timeline.jsonl".to_string(),
1490 ));
1491 }
1492 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1493 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1494 break;
1495 }
1496 }
1497 reply.ok();
1498 return;
1499 }
1500
1501 if ino == FUSE_UNALLOCATED_INO {
1503 self.ensure_unallocated_cache();
1504 let cache = self.unallocated_cache.borrow();
1505 let mut entries: Vec<(u64, FileType, String)> = vec![
1506 (FUSE_UNALLOCATED_INO, FileType::Directory, ".".to_string()),
1507 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1508 ];
1509 if let Some(cached) = cache.as_ref() {
1510 for (i, entry) in cached.iter().enumerate() {
1511 entries.push((
1512 unallocated_ino(i as u64),
1513 FileType::RegularFile,
1514 entry.name.clone(),
1515 ));
1516 }
1517 }
1518 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1519 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1520 break;
1521 }
1522 }
1523 reply.ok();
1524 return;
1525 }
1526
1527 if ino == FUSE_SESSION_INO {
1529 let mut entries: Vec<(u64, FileType, String)> = vec![
1530 (FUSE_SESSION_INO, FileType::Directory, ".".to_string()),
1531 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1532 ];
1533 if self.has_session() {
1534 entries.push((
1535 metadata_ino(100),
1536 FileType::RegularFile,
1537 "status.json".to_string(),
1538 ));
1539 }
1540 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1541 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1542 break;
1543 }
1544 }
1545 reply.ok();
1546 return;
1547 }
1548
1549 let fs_dir_ino = match ino {
1551 FUSE_RO_INO => self.root_ino,
1552 _ => {
1553 if let InodeNamespace::Ro(fs_ino) = decode_fuse_ino(ino) {
1554 fs_ino
1555 } else {
1556 reply.error(libc::ENOENT);
1557 return;
1558 }
1559 }
1560 };
1561
1562 self.ensure_deleted_cache();
1565 let injected: Vec<(u64, FileType, String)> = {
1566 let cache = self.deleted_cache.borrow();
1567 cache.as_ref().map_or_else(Vec::new, |c| {
1568 deleted_in_place_children(c, fs_dir_ino)
1569 .into_iter()
1570 .map(|(dino, name)| (dino, FileType::RegularFile, name))
1571 .collect()
1572 })
1573 };
1574
1575 let mut fs = self.fs.borrow_mut();
1576 match fs.read_dir(fs_dir_ino) {
1577 Ok(entries) => {
1578 let root_ino = self.root_ino;
1579 let mut fuse_entries: Vec<(u64, FileType, String)> = entries
1580 .iter()
1581 .map(|e| {
1582 let name = e.name_str();
1583 let fuse_ino = if name == "." || name == ".." {
1584 if fs_dir_ino == root_ino && name == "." {
1585 FUSE_RO_INO
1586 } else if fs_dir_ino == root_ino && name == ".." {
1587 FUSE_ROOT_INO
1588 } else {
1589 ro_ino(e.inode)
1590 }
1591 } else {
1592 ro_ino(e.inode)
1593 };
1594 let kind = fs_file_type_to_fuse(e.file_type);
1595 (fuse_ino, kind, name)
1596 })
1597 .collect();
1598
1599 fuse_entries.extend(injected);
1600
1601 for (i, (entry_ino, kind, name)) in fuse_entries.iter().enumerate().skip(offset) {
1602 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1603 break;
1604 }
1605 }
1606 reply.ok();
1607 }
1608 Err(_) => reply.error(libc::EIO),
1609 }
1610 }
1611
1612 fn read(
1613 &mut self,
1614 _req: &Request,
1615 ino: u64,
1616 _fh: u64,
1617 offset: i64,
1618 size: u32,
1619 _flags: i32,
1620 _lock_owner: Option<u64>,
1621 reply: ReplyData,
1622 ) {
1623 match decode_fuse_ino(ino) {
1624 InodeNamespace::Deleted(fs_ino) => {
1625 {
1628 let session = self.session.borrow();
1629 if let Some(s) = session.as_ref() {
1630 if s.overlay.modified.contains_key(&fs_ino) {
1631 let overlay_id = Self::modified_overlay_id(fs_ino);
1632 if let Ok(d) = s.read_overlay_file(&overlay_id) {
1633 let off = offset as usize;
1634 if off >= d.len() {
1635 reply.data(&[]);
1636 } else {
1637 let end = (off + size as usize).min(d.len());
1638 reply.data(&d[off..end]);
1639 }
1640 return;
1641 }
1642 }
1643 }
1644 }
1645 self.ensure_deleted_cache();
1646 let cache = self.deleted_cache.borrow();
1647 if let Some(entries) = cache.as_ref() {
1648 if let Some(entry) = entries.iter().find(|e| e.fs_ino == fs_ino) {
1649 if !entry.readable {
1652 reply.error(libc::EIO);
1653 return;
1654 }
1655 let off = offset as usize;
1656 if off >= entry.data.len() {
1657 reply.data(&[]);
1658 } else {
1659 let end = (off + size as usize).min(entry.data.len());
1660 reply.data(&entry.data[off..end]);
1661 }
1662 return;
1663 }
1664 }
1665 reply.error(libc::ENOENT);
1666 }
1667 InodeNamespace::Metadata(id) => {
1668 self.ensure_metadata_cache();
1669 let data = match id {
1670 1 => {
1671 let cache = self.metadata_cache.borrow();
1672 cache.as_ref().map(|mc| mc.superblock_json.clone())
1673 }
1674 2 => {
1675 let cache = self.metadata_cache.borrow();
1676 cache.as_ref().map(|mc| mc.timeline_jsonl.clone())
1677 }
1678 100 => {
1679 let session = self.session.borrow();
1681 session.as_ref().map(|s| {
1682 let status = serde_json::json!({
1683 "image_path": s.metadata.image_path,
1684 "image_sha256": s.metadata.image_sha256,
1685 "created": s.metadata.created,
1686 });
1687 serde_json::to_string_pretty(&status)
1688 .unwrap_or_default()
1689 .into_bytes()
1690 })
1691 }
1692 _ => None,
1693 };
1694 match data {
1695 Some(buf) => {
1696 let off = offset as usize;
1697 if off >= buf.len() {
1698 reply.data(&[]);
1699 } else {
1700 let end = (off + size as usize).min(buf.len());
1701 reply.data(&buf[off..end]);
1702 }
1703 }
1704 None => reply.error(libc::ENOENT),
1705 }
1706 }
1707 InodeNamespace::Unallocated(range_id) => {
1708 self.ensure_unallocated_cache();
1709 let range_info = {
1710 let cache = self.unallocated_cache.borrow();
1711 cache.as_ref().and_then(|entries| {
1712 entries.get(range_id as usize).map(|e| FsBlockRange {
1713 start: e.start,
1714 length: e.length,
1715 })
1716 })
1717 };
1718 match range_info {
1719 Some(range) => {
1720 let mut fs = self.fs.borrow_mut();
1721 match fs.read_unallocated(&range) {
1722 Ok(data) => {
1723 let off = offset as usize;
1724 if off >= data.len() {
1725 reply.data(&[]);
1726 } else {
1727 let end = (off + size as usize).min(data.len());
1728 reply.data(&data[off..end]);
1729 }
1730 }
1731 Err(_) => reply.error(libc::EIO),
1732 }
1733 }
1734 None => reply.error(libc::ENOENT),
1735 }
1736 }
1737 InodeNamespace::Ro(fs_ino) => {
1738 let mut fs = self.fs.borrow_mut();
1739 match fs.read_file_range(fs_ino, offset as u64, u64::from(size)) {
1740 Ok(data) => reply.data(&data),
1741 Err(_) => reply.error(libc::EIO),
1742 }
1743 }
1744 InodeNamespace::Rw(rw_id) => {
1745 if rw_id >= 9_000_000 {
1747 let counter = rw_id - 9_000_000;
1748 let created_id = Self::created_overlay_id(counter);
1749 let session = self.session.borrow();
1750 if let Some(s) = session.as_ref() {
1751 if s.overlay.created.contains_key(&created_id)
1752 || s.overlay.dirs.contains_key(&created_id)
1753 {
1754 if let Ok(data) = s.read_overlay_file(&created_id) {
1755 let off = offset as usize;
1756 let end = (off + size as usize).min(data.len());
1757 if off >= data.len() {
1758 reply.data(&[]);
1759 } else {
1760 reply.data(&data[off..end]);
1761 }
1762 return;
1763 }
1764 reply.error(libc::EIO);
1765 return;
1766 }
1767 }
1768 reply.error(libc::ENOENT);
1769 return;
1770 }
1771
1772 let fs_ino = rw_id;
1774 let session = self.session.borrow();
1776 if let Some(s) = session.as_ref() {
1777 let overlay_id = Self::modified_overlay_id(fs_ino);
1778 if s.overlay.modified.contains_key(&fs_ino) {
1779 if let Ok(data) = s.read_overlay_file(&overlay_id) {
1780 let off = offset as usize;
1781 let end = (off + size as usize).min(data.len());
1782 if off >= data.len() {
1783 reply.data(&[]);
1784 } else {
1785 reply.data(&data[off..end]);
1786 }
1787 return;
1788 }
1789 reply.error(libc::EIO);
1790 return;
1791 }
1792 }
1793 drop(session);
1794
1795 let mut fs = self.fs.borrow_mut();
1797 match fs.read_file_range(fs_ino, offset as u64, u64::from(size)) {
1798 Ok(data) => reply.data(&data),
1799 Err(_) => reply.error(libc::EIO),
1800 }
1801 }
1802 _ => {
1803 reply.error(libc::ENOENT);
1804 }
1805 }
1806 }
1807
1808 fn readlink(&mut self, _req: &Request, ino: u64, reply: ReplyData) {
1809 let fs_ino = match decode_fuse_ino(ino) {
1810 InodeNamespace::Ro(fs_ino) => fs_ino,
1811 InodeNamespace::Rw(rw_id) if rw_id < 9_000_000 => rw_id,
1812 _ => {
1813 reply.error(libc::ENOENT);
1814 return;
1815 }
1816 };
1817
1818 let mut fs = self.fs.borrow_mut();
1819 match fs.read_link(fs_ino) {
1820 Ok(target) => reply.data(&target),
1821 Err(_) => reply.error(libc::EIO),
1822 }
1823 }
1824
1825 fn write(
1826 &mut self,
1827 _req: &Request,
1828 ino: u64,
1829 _fh: u64,
1830 offset: i64,
1831 data: &[u8],
1832 _write_flags: u32,
1833 _flags: i32,
1834 _lock_owner: Option<u64>,
1835 reply: ReplyWrite,
1836 ) {
1837 if !self.has_session() {
1838 reply.error(libc::EROFS);
1839 return;
1840 }
1841
1842 match decode_fuse_ino(ino) {
1843 InodeNamespace::Rw(rw_id) => {
1844 if rw_id >= 9_000_000 {
1846 let counter = rw_id - 9_000_000;
1847 let created_id = Self::created_overlay_id(counter);
1848 let mut session = self.session.borrow_mut();
1849 let s = session.as_mut().unwrap();
1850
1851 let is_known = s.overlay.created.contains_key(&created_id)
1852 || s.overlay.dirs.contains_key(&created_id);
1853 if !is_known {
1854 reply.error(libc::ENOENT);
1855 return;
1856 }
1857
1858 let mut buf = s.read_overlay_file(&created_id).unwrap_or_default();
1859 let off = offset as usize;
1860 let end = off + data.len();
1861 if end > buf.len() {
1862 buf.resize(end, 0);
1863 }
1864 buf[off..end].copy_from_slice(data);
1865
1866 if s.write_overlay_file(&created_id, &buf).is_err() {
1867 reply.error(libc::EIO);
1868 return;
1869 }
1870
1871 if let Some(entry) = s.overlay.created.get_mut(&created_id) {
1872 entry.size = buf.len() as u64;
1873 }
1874 if let Some(entry) = s.overlay.dirs.get_mut(&created_id) {
1875 entry.size = buf.len() as u64;
1876 }
1877
1878 if s.save().is_err() {
1879 reply.error(libc::EIO);
1880 return;
1881 }
1882
1883 reply.written(data.len() as u32);
1884 return;
1885 }
1886
1887 let fs_ino = rw_id;
1889 let overlay_id = Self::modified_overlay_id(fs_ino);
1890
1891 let mut session = self.session.borrow_mut();
1892 let s = session.as_mut().unwrap();
1893
1894 if !s.overlay.modified.contains_key(&fs_ino) {
1895 let mut fs = self.fs.borrow_mut();
1896 let Ok(original) = fs.read_file(fs_ino) else {
1897 reply.error(libc::EIO);
1898 return;
1899 };
1900 if s.write_overlay_file(&overlay_id, &original).is_err() {
1901 reply.error(libc::EIO);
1902 return;
1903 }
1904 s.overlay.modified.insert(fs_ino, overlay_id.clone());
1905 }
1906
1907 let mut buf = s.read_overlay_file(&overlay_id).unwrap_or_default();
1908 let off = offset as usize;
1909 let end = off + data.len();
1910 if end > buf.len() {
1911 buf.resize(end, 0);
1912 }
1913 buf[off..end].copy_from_slice(data);
1914
1915 if s.write_overlay_file(&overlay_id, &buf).is_err() {
1916 reply.error(libc::EIO);
1917 return;
1918 }
1919
1920 if s.save().is_err() {
1921 reply.error(libc::EIO);
1922 return;
1923 }
1924
1925 reply.written(data.len() as u32);
1926 }
1927 InodeNamespace::Deleted(fs_ino) => {
1931 self.ensure_deleted_cache();
1932 let overlay_id = Self::modified_overlay_id(fs_ino);
1933 let mut session = self.session.borrow_mut();
1934 let s = session.as_mut().unwrap();
1935
1936 if !s.overlay.modified.contains_key(&fs_ino) {
1937 let base = {
1938 let cache = self.deleted_cache.borrow();
1939 cache.as_ref().and_then(|e| deleted_cow_base(e, fs_ino))
1940 };
1941 let Some(base) = base else {
1944 reply.error(libc::EIO);
1945 return;
1946 };
1947 if s.write_overlay_file(&overlay_id, &base).is_err() {
1948 reply.error(libc::EIO);
1949 return;
1950 }
1951 s.overlay.modified.insert(fs_ino, overlay_id.clone());
1952 }
1953
1954 let mut buf = s.read_overlay_file(&overlay_id).unwrap_or_default();
1955 let off = offset as usize;
1956 let end = off + data.len();
1957 if end > buf.len() {
1958 buf.resize(end, 0);
1959 }
1960 buf[off..end].copy_from_slice(data);
1961
1962 if s.write_overlay_file(&overlay_id, &buf).is_err() {
1963 reply.error(libc::EIO);
1964 return;
1965 }
1966 if s.save().is_err() {
1967 reply.error(libc::EIO);
1968 return;
1969 }
1970 reply.written(data.len() as u32);
1971 }
1972 _ => reply.error(libc::EROFS),
1973 }
1974 }
1975
1976 fn create(
1977 &mut self,
1978 _req: &Request,
1979 parent: u64,
1980 name: &OsStr,
1981 _mode: u32,
1982 _umask: u32,
1983 _flags: i32,
1984 reply: ReplyCreate,
1985 ) {
1986 if !self.has_session() {
1987 reply.error(libc::EROFS);
1988 return;
1989 }
1990
1991 let Some(fs_parent) = self.rw_parent_to_fs(parent) else {
1992 reply.error(libc::EROFS);
1993 return;
1994 };
1995
1996 let Some(name_s) = name.to_str() else {
1997 reply.error(libc::EINVAL);
1998 return;
1999 };
2000 let name_str = name_s.to_string();
2001
2002 let counter = self.alloc_overlay_ino();
2003 let created_id = Self::created_overlay_id(counter);
2004 let fuse_ino = rw_ino(counter + 9_000_000);
2005
2006 let mut session = self.session.borrow_mut();
2007 let s = session.as_mut().unwrap();
2008
2009 if s.write_overlay_file(&created_id, &[]).is_err() {
2010 reply.error(libc::EIO);
2011 return;
2012 }
2013
2014 s.overlay.created.insert(
2015 created_id,
2016 crate::session::OverlayEntry {
2017 parent_ino: fs_parent,
2018 name: name_str,
2019 size: 0,
2020 },
2021 );
2022
2023 if s.save().is_err() {
2024 reply.error(libc::EIO);
2025 return;
2026 }
2027
2028 let attr = Self::overlay_created_attr(fuse_ino, 0, false);
2029 reply.created(&TTL, &attr, 0, 0, 0);
2030 }
2031
2032 fn mkdir(
2033 &mut self,
2034 _req: &Request,
2035 parent: u64,
2036 name: &OsStr,
2037 _mode: u32,
2038 _umask: u32,
2039 reply: ReplyEntry,
2040 ) {
2041 if !self.has_session() {
2042 reply.error(libc::EROFS);
2043 return;
2044 }
2045
2046 let Some(fs_parent) = self.rw_parent_to_fs(parent) else {
2047 reply.error(libc::EROFS);
2048 return;
2049 };
2050
2051 let Some(name_s) = name.to_str() else {
2052 reply.error(libc::EINVAL);
2053 return;
2054 };
2055 let name_str = name_s.to_string();
2056
2057 let counter = self.alloc_overlay_ino();
2058 let created_id = Self::created_overlay_id(counter);
2059 let fuse_ino = rw_ino(counter + 9_000_000);
2060
2061 let mut session = self.session.borrow_mut();
2062 let s = session.as_mut().unwrap();
2063
2064 s.overlay.dirs.insert(
2065 created_id,
2066 crate::session::OverlayEntry {
2067 parent_ino: fs_parent,
2068 name: name_str,
2069 size: 0,
2070 },
2071 );
2072
2073 if s.save().is_err() {
2074 reply.error(libc::EIO);
2075 return;
2076 }
2077
2078 let attr = Self::overlay_created_attr(fuse_ino, 0, true);
2079 reply.entry(&TTL, &attr, 0);
2080 }
2081
2082 fn unlink(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) {
2083 if !self.has_session() {
2084 reply.error(libc::EROFS);
2085 return;
2086 }
2087
2088 let Some(fs_parent) = self.rw_parent_to_fs(parent) else {
2089 reply.error(libc::EROFS);
2090 return;
2091 };
2092
2093 let name_bytes = name.as_encoded_bytes();
2094
2095 if let Some((id, _counter, _is_dir)) = self.find_created_by_name(fs_parent, name_bytes) {
2097 let mut session = self.session.borrow_mut();
2098 let s = session.as_mut().unwrap();
2099 s.overlay.created.remove(&id);
2100 s.overlay.dirs.remove(&id);
2101 let _ = std::fs::remove_file(s.overlay_file_path(&id));
2102 if s.save().is_err() {
2103 reply.error(libc::EIO);
2104 return;
2105 }
2106 reply.ok();
2107 return;
2108 }
2109
2110 let mut fs = self.fs.borrow_mut();
2112 match fs.lookup(fs_parent, name_bytes) {
2113 Ok(Some(child_ino)) => {
2114 let mut session = self.session.borrow_mut();
2115 let s = session.as_mut().unwrap();
2116 if !s.overlay.deleted.contains(&child_ino) {
2117 s.overlay.deleted.push(child_ino);
2118 }
2119 if s.save().is_err() {
2120 reply.error(libc::EIO);
2121 return;
2122 }
2123 reply.ok();
2124 }
2125 Ok(None) => reply.error(libc::ENOENT),
2126 Err(_) => reply.error(libc::EIO),
2127 }
2128 }
2129
2130 fn rmdir(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) {
2131 self.unlink(req, parent, name, reply);
2132 }
2133
2134 #[allow(clippy::too_many_arguments)]
2135 fn setattr(
2136 &mut self,
2137 _req: &Request,
2138 ino: u64,
2139 _mode: Option<u32>,
2140 _uid: Option<u32>,
2141 _gid: Option<u32>,
2142 size: Option<u64>,
2143 _atime: Option<TimeOrNow>,
2144 _mtime: Option<TimeOrNow>,
2145 _ctime: Option<SystemTime>,
2146 _fh: Option<u64>,
2147 _crtime: Option<SystemTime>,
2148 _chgtime: Option<SystemTime>,
2149 _bkuptime: Option<SystemTime>,
2150 _flags: Option<u32>,
2151 reply: ReplyAttr,
2152 ) {
2153 match decode_fuse_ino(ino) {
2154 InodeNamespace::Rw(rw_id) => {
2155 if !self.has_session() {
2156 reply.error(libc::EROFS);
2157 return;
2158 }
2159
2160 if let Some(new_size) = size {
2162 if rw_id >= 9_000_000 {
2164 let counter = rw_id - 9_000_000;
2165 let created_id = Self::created_overlay_id(counter);
2166 let mut session = self.session.borrow_mut();
2167 let s = session.as_mut().unwrap();
2168
2169 let mut buf = s.read_overlay_file(&created_id).unwrap_or_default();
2170 buf.resize(new_size as usize, 0);
2171 if s.write_overlay_file(&created_id, &buf).is_err() {
2172 reply.error(libc::EIO);
2173 return;
2174 }
2175
2176 if let Some(entry) = s.overlay.created.get_mut(&created_id) {
2177 entry.size = new_size;
2178 }
2179 if let Some(entry) = s.overlay.dirs.get_mut(&created_id) {
2180 entry.size = new_size;
2181 }
2182 if s.save().is_err() {
2183 reply.error(libc::EIO);
2184 return;
2185 }
2186
2187 let attr = Self::overlay_created_attr(ino, new_size, false);
2188 reply.attr(&TTL, &attr);
2189 return;
2190 }
2191
2192 let fs_ino = rw_id;
2194 let overlay_id = Self::modified_overlay_id(fs_ino);
2195
2196 let mut session = self.session.borrow_mut();
2197 let s = session.as_mut().unwrap();
2198
2199 if !s.overlay.modified.contains_key(&fs_ino) {
2200 let mut fs = self.fs.borrow_mut();
2201 let Ok(original) = fs.read_file(fs_ino) else {
2202 reply.error(libc::EIO);
2203 return;
2204 };
2205 if s.write_overlay_file(&overlay_id, &original).is_err() {
2206 reply.error(libc::EIO);
2207 return;
2208 }
2209 s.overlay.modified.insert(fs_ino, overlay_id.clone());
2210 }
2211
2212 let mut buf = s.read_overlay_file(&overlay_id).unwrap_or_default();
2213 buf.resize(new_size as usize, 0);
2214 if s.write_overlay_file(&overlay_id, &buf).is_err() {
2215 reply.error(libc::EIO);
2216 return;
2217 }
2218 if s.save().is_err() {
2219 reply.error(libc::EIO);
2220 return;
2221 }
2222
2223 let mut fs = self.fs.borrow_mut();
2225 match fs.metadata(fs_ino) {
2226 Ok(meta) => {
2227 let mut attr = fs_to_attr(ino, &meta);
2228 attr.size = new_size;
2229 attr.blocks = new_size.div_ceil(512);
2230 reply.attr(&TTL, &attr);
2231 }
2232 Err(_) => reply.error(libc::EIO),
2233 }
2234 return;
2235 }
2236
2237 if rw_id >= 9_000_000 {
2239 let counter = rw_id - 9_000_000;
2240 let created_id = Self::created_overlay_id(counter);
2241 let session = self.session.borrow();
2242 if let Some(s) = session.as_ref() {
2243 if let Some(entry) = s.overlay.created.get(&created_id) {
2244 let attr = Self::overlay_created_attr(ino, entry.size, false);
2245 reply.attr(&TTL, &attr);
2246 return;
2247 }
2248 if let Some(entry) = s.overlay.dirs.get(&created_id) {
2249 let attr = Self::overlay_created_attr(ino, entry.size, true);
2250 reply.attr(&TTL, &attr);
2251 return;
2252 }
2253 }
2254 reply.error(libc::ENOENT);
2255 } else {
2256 let fs_ino = rw_id;
2257 let mut fs = self.fs.borrow_mut();
2258 match fs.metadata(fs_ino) {
2259 Ok(meta) => {
2260 let mut attr = fs_to_attr(ino, &meta);
2261 let session = self.session.borrow();
2262 if let Some(s) = session.as_ref() {
2263 let overlay_id = Self::modified_overlay_id(fs_ino);
2264 if s.overlay.modified.contains_key(&fs_ino) {
2265 if let Ok(data) = s.read_overlay_file(&overlay_id) {
2266 attr.size = data.len() as u64;
2267 attr.blocks = attr.size.div_ceil(512);
2268 }
2269 }
2270 }
2271 reply.attr(&TTL, &attr);
2272 }
2273 Err(_) => reply.error(libc::EIO),
2274 }
2275 }
2276 }
2277 _ => reply.error(libc::EROFS),
2279 }
2280 }
2281}
2282
2283#[cfg(test)]
2284mod tests {
2285 use super::*;
2286 use crate::{FsDeletedInode, FsDirEntry, FsError, FsRecoveryResult, FsResult, FsTimelineEvent};
2287 use fuser::FileType;
2288 use std::time::{Duration, UNIX_EPOCH};
2289
2290 #[test]
2295 fn virtual_dir_attr_is_directory() {
2296 let attr = virtual_dir_attr(1);
2297 assert_eq!(attr.ino, 1);
2298 assert_eq!(attr.kind, FileType::Directory);
2299 assert_eq!(attr.perm, 0o555);
2300 assert_eq!(attr.nlink, 2);
2301 assert_eq!(attr.size, 0);
2302 assert_eq!(attr.blocks, 0);
2303 assert_eq!(attr.uid, 0);
2304 assert_eq!(attr.gid, 0);
2305 assert_eq!(attr.blksize, 4096);
2306 assert_eq!(attr.atime, UNIX_EPOCH);
2307 assert_eq!(attr.mtime, UNIX_EPOCH);
2308 assert_eq!(attr.ctime, UNIX_EPOCH);
2309 assert_eq!(attr.crtime, UNIX_EPOCH);
2310 }
2311
2312 #[test]
2313 fn virtual_dir_attr_preserves_ino() {
2314 for ino in [1, 42, FUSE_ROOT_INO, FUSE_ORPHANS_INO, 999_999] {
2315 assert_eq!(virtual_dir_attr(ino).ino, ino);
2316 }
2317 }
2318
2319 #[test]
2324 fn virtual_file_attr_regular() {
2325 let attr = virtual_file_attr(100, 4096);
2326 assert_eq!(attr.ino, 100);
2327 assert_eq!(attr.kind, FileType::RegularFile);
2328 assert_eq!(attr.perm, 0o444);
2329 assert_eq!(attr.nlink, 1);
2330 assert_eq!(attr.size, 4096);
2331 assert_eq!(attr.blocks, 8); }
2333
2334 #[test]
2335 fn virtual_file_attr_zero_size() {
2336 let attr = virtual_file_attr(1, 0);
2337 assert_eq!(attr.size, 0);
2338 assert_eq!(attr.blocks, 0);
2339 }
2340
2341 #[test]
2342 fn virtual_file_attr_non_512_aligned() {
2343 let attr = virtual_file_attr(1, 1000);
2345 assert_eq!(attr.blocks, 2);
2346 }
2347
2348 #[test]
2353 fn timestamp_conversion_positive() {
2354 let ts = FsTimestamp {
2355 seconds: 1_700_000_000,
2356 nanoseconds: 500_000_000,
2357 };
2358 let st = ts_to_systime(&ts);
2359 let dur = st.duration_since(UNIX_EPOCH).unwrap();
2360 assert_eq!(dur.as_secs(), 1_700_000_000);
2361 assert_eq!(dur.subsec_nanos(), 500_000_000);
2362 }
2363
2364 #[test]
2365 fn timestamp_zero() {
2366 let ts = FsTimestamp {
2367 seconds: 0,
2368 nanoseconds: 0,
2369 };
2370 let st = ts_to_systime(&ts);
2371 assert_eq!(st, UNIX_EPOCH);
2372 }
2373
2374 #[test]
2375 fn timestamp_negative_clamps_to_epoch() {
2376 let ts = FsTimestamp {
2377 seconds: -1,
2378 nanoseconds: 0,
2379 };
2380 let st = ts_to_systime(&ts);
2381 assert_eq!(st, UNIX_EPOCH);
2382 }
2383
2384 #[test]
2385 fn timestamp_negative_large_clamps_to_epoch() {
2386 let ts = FsTimestamp {
2387 seconds: -1_000_000,
2388 nanoseconds: 999_999_999,
2389 };
2390 let st = ts_to_systime(&ts);
2391 assert_eq!(st, UNIX_EPOCH);
2392 }
2393
2394 #[test]
2395 fn timestamp_epoch_plus_one_second() {
2396 let ts = FsTimestamp {
2397 seconds: 1,
2398 nanoseconds: 0,
2399 };
2400 let st = ts_to_systime(&ts);
2401 assert_eq!(st, UNIX_EPOCH + Duration::from_secs(1));
2402 }
2403
2404 #[test]
2409 fn file_type_mapping_regular() {
2410 assert_eq!(
2411 fs_file_type_to_fuse(FsFileType::RegularFile),
2412 FileType::RegularFile
2413 );
2414 }
2415
2416 #[test]
2417 fn file_type_mapping_directory() {
2418 assert_eq!(
2419 fs_file_type_to_fuse(FsFileType::Directory),
2420 FileType::Directory
2421 );
2422 }
2423
2424 #[test]
2425 fn file_type_mapping_symlink() {
2426 assert_eq!(fs_file_type_to_fuse(FsFileType::Symlink), FileType::Symlink);
2427 }
2428
2429 #[test]
2430 fn file_type_mapping_chardev() {
2431 assert_eq!(
2432 fs_file_type_to_fuse(FsFileType::CharDevice),
2433 FileType::CharDevice
2434 );
2435 }
2436
2437 #[test]
2438 fn file_type_mapping_blockdev() {
2439 assert_eq!(
2440 fs_file_type_to_fuse(FsFileType::BlockDevice),
2441 FileType::BlockDevice
2442 );
2443 }
2444
2445 #[test]
2446 fn file_type_mapping_fifo() {
2447 assert_eq!(fs_file_type_to_fuse(FsFileType::Fifo), FileType::NamedPipe);
2448 }
2449
2450 #[test]
2451 fn file_type_mapping_socket() {
2452 assert_eq!(fs_file_type_to_fuse(FsFileType::Socket), FileType::Socket);
2453 }
2454
2455 #[test]
2456 fn file_type_mapping_unknown() {
2457 assert_eq!(
2458 fs_file_type_to_fuse(FsFileType::Unknown),
2459 FileType::RegularFile
2460 );
2461 }
2462
2463 #[test]
2468 fn fs_to_attr_regular_file() {
2469 let meta = FsMetadata {
2470 ino: 42,
2471 file_type: FsFileType::RegularFile,
2472 mode: 0o100_644,
2473 uid: 1000,
2474 gid: 1000,
2475 size: 100,
2476 links_count: 1,
2477 atime: FsTimestamp {
2478 seconds: 1_700_000_000,
2479 nanoseconds: 0,
2480 },
2481 mtime: FsTimestamp {
2482 seconds: 1_700_000_000,
2483 nanoseconds: 0,
2484 },
2485 ctime: FsTimestamp {
2486 seconds: 1_700_000_000,
2487 nanoseconds: 0,
2488 },
2489 crtime: FsTimestamp {
2490 seconds: 1_700_000_000,
2491 nanoseconds: 0,
2492 },
2493 allocated: true,
2494 };
2495 let attr = fs_to_attr(1012, &meta);
2496 assert_eq!(attr.ino, 1012);
2497 assert_eq!(attr.size, 100);
2498 assert_eq!(attr.kind, FileType::RegularFile);
2499 assert_eq!(attr.nlink, 1);
2500 assert_eq!(attr.perm, 0o644);
2501 assert_eq!(attr.uid, 1000);
2502 assert_eq!(attr.gid, 1000);
2503 }
2504
2505 #[test]
2506 fn fs_to_attr_directory() {
2507 let meta = FsMetadata {
2508 ino: 2,
2509 file_type: FsFileType::Directory,
2510 mode: 0o40755,
2511 uid: 0,
2512 gid: 0,
2513 size: 4096,
2514 links_count: 3,
2515 atime: FsTimestamp::default(),
2516 mtime: FsTimestamp::default(),
2517 ctime: FsTimestamp::default(),
2518 crtime: FsTimestamp::default(),
2519 allocated: true,
2520 };
2521 let attr = fs_to_attr(2000, &meta);
2522 assert_eq!(attr.kind, FileType::Directory);
2523 assert_eq!(attr.nlink, 3);
2524 assert_eq!(attr.perm, 0o755);
2525 }
2526
2527 #[test]
2528 fn fs_to_attr_symlink() {
2529 let meta = FsMetadata {
2530 ino: 10,
2531 file_type: FsFileType::Symlink,
2532 mode: 0o120_777,
2533 uid: 0,
2534 gid: 0,
2535 size: 11,
2536 links_count: 1,
2537 atime: FsTimestamp::default(),
2538 mtime: FsTimestamp::default(),
2539 ctime: FsTimestamp::default(),
2540 crtime: FsTimestamp::default(),
2541 allocated: true,
2542 };
2543 let attr = fs_to_attr(3000, &meta);
2544 assert_eq!(attr.kind, FileType::Symlink);
2545 assert_eq!(attr.perm, 0o777);
2546 }
2547
2548 #[test]
2549 fn fs_to_attr_blocks_calculation() {
2550 let meta = FsMetadata {
2551 ino: 42,
2552 file_type: FsFileType::RegularFile,
2553 mode: 0o100_644,
2554 uid: 0,
2555 gid: 0,
2556 size: 1000,
2557 links_count: 1,
2558 atime: FsTimestamp::default(),
2559 mtime: FsTimestamp::default(),
2560 ctime: FsTimestamp::default(),
2561 crtime: FsTimestamp::default(),
2562 allocated: true,
2563 };
2564 let attr = fs_to_attr(42, &meta);
2565 assert_eq!(attr.size, 1000);
2566 assert_eq!(attr.blocks, 2);
2567 }
2568
2569 #[test]
2570 fn fs_to_attr_blksize_always_4096() {
2571 let meta = FsMetadata {
2572 ino: 1,
2573 file_type: FsFileType::RegularFile,
2574 mode: 0o100_644,
2575 uid: 0,
2576 gid: 0,
2577 size: 0,
2578 links_count: 1,
2579 atime: FsTimestamp::default(),
2580 mtime: FsTimestamp::default(),
2581 ctime: FsTimestamp::default(),
2582 crtime: FsTimestamp::default(),
2583 allocated: true,
2584 };
2585 let attr = fs_to_attr(1, &meta);
2586 assert_eq!(attr.blksize, 4096);
2587 }
2588
2589 #[test]
2594 fn overlay_created_attr_regular_file() {
2595 let attr = ForensicFuseFs::overlay_created_attr(999, 512, false);
2596 assert_eq!(attr.ino, 999);
2597 assert_eq!(attr.size, 512);
2598 assert_eq!(attr.kind, FileType::RegularFile);
2599 assert_eq!(attr.perm, 0o644);
2600 assert_eq!(attr.nlink, 1);
2601 assert_eq!(attr.blocks, 1);
2602 }
2603
2604 #[test]
2605 fn overlay_created_attr_directory() {
2606 let attr = ForensicFuseFs::overlay_created_attr(888, 0, true);
2607 assert_eq!(attr.kind, FileType::Directory);
2608 assert_eq!(attr.perm, 0o755);
2609 }
2610
2611 #[test]
2616 fn modified_overlay_id_format() {
2617 assert_eq!(ForensicFuseFs::modified_overlay_id(42), "ino_42");
2618 assert_eq!(ForensicFuseFs::modified_overlay_id(0), "ino_0");
2619 assert_eq!(
2620 ForensicFuseFs::modified_overlay_id(9_999_999),
2621 "ino_9999999"
2622 );
2623 }
2624
2625 #[test]
2626 fn created_overlay_id_format() {
2627 assert_eq!(ForensicFuseFs::created_overlay_id(1), "new_1");
2628 assert_eq!(ForensicFuseFs::created_overlay_id(0), "new_0");
2629 }
2630
2631 fn root_child_names(layout: crate::MountLayout) -> Vec<String> {
2636 let mut fs = MockForensicFs;
2637 let root = fs.root_ino();
2638 root_children(layout, &mut fs, root, false)
2639 .unwrap()
2640 .iter()
2641 .map(|(_, n, _)| String::from_utf8_lossy(n).to_string())
2642 .collect()
2643 }
2644
2645 #[test]
2646 fn root_children_raw_lists_fs_tree_without_overlay() {
2647 let names = root_child_names(crate::MountLayout::Raw);
2648 assert!(names.contains(&"hello.txt".to_string()), "got {names:?}");
2649 assert!(names.contains(&"subdir".to_string()), "got {names:?}");
2650 assert!(
2651 !names
2652 .iter()
2653 .any(|n| n == "rw" || n == "deleted" || n == "ro"),
2654 "Raw root must have no overlay dirs: {names:?}"
2655 );
2656 }
2657
2658 #[test]
2659 fn root_children_diskoverlay_lists_virtual_dirs() {
2660 let names = root_child_names(crate::MountLayout::DiskOverlay);
2661 for d in ["ro", "rw", "journal", "metadata", "unallocated", "session"] {
2662 assert!(names.contains(&d.to_string()), "missing {d}: {names:?}");
2663 }
2664 assert!(!names.contains(&"deleted".to_string()), "got {names:?}");
2666 }
2667
2668 #[test]
2669 fn root_children_diskoverlay_appends_orphans_when_present() {
2670 let mut fs = MockForensicFs;
2671 let root = fs.root_ino();
2672 let with: Vec<String> = root_children(crate::MountLayout::DiskOverlay, &mut fs, root, true)
2673 .unwrap()
2674 .iter()
2675 .map(|(_, n, _)| String::from_utf8_lossy(n).to_string())
2676 .collect();
2677 assert!(with.contains(&"$Orphans".to_string()), "got {with:?}");
2678 }
2679
2680 #[test]
2685 fn virtual_dirs_has_expected_entries() {
2686 assert_eq!(VIRTUAL_DIRS.len(), 6);
2688 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "ro"));
2689 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "rw"));
2690 assert!(!VIRTUAL_DIRS.iter().any(|(_, name)| *name == "deleted"));
2691 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "journal"));
2692 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "metadata"));
2693 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "unallocated"));
2694 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "session"));
2695 }
2696
2697 #[test]
2698 fn virtual_dirs_ino_matches_constants() {
2699 for &(ino, name) in VIRTUAL_DIRS {
2700 match name {
2701 "ro" => assert_eq!(ino, FUSE_RO_INO),
2702 "rw" => assert_eq!(ino, FUSE_RW_INO),
2703 "journal" => assert_eq!(ino, FUSE_JOURNAL_INO),
2704 "metadata" => assert_eq!(ino, FUSE_METADATA_INO),
2705 "unallocated" => assert_eq!(ino, FUSE_UNALLOCATED_INO),
2706 "session" => assert_eq!(ino, FUSE_SESSION_INO),
2707 _ => panic!("unexpected virtual dir: {name}"),
2708 }
2709 }
2710 }
2711
2712 struct MockForensicFs;
2717
2718 impl crate::ForensicFs for MockForensicFs {
2719 fn root_ino(&self) -> u64 {
2720 2
2721 }
2722
2723 fn read_dir(&mut self, ino: u64) -> FsResult<Vec<FsDirEntry>> {
2724 match ino {
2725 2 => Ok(vec![
2726 FsDirEntry {
2727 inode: 2,
2728 name: b".".to_vec(),
2729 file_type: FsFileType::Directory,
2730 },
2731 FsDirEntry {
2732 inode: 2,
2733 name: b"..".to_vec(),
2734 file_type: FsFileType::Directory,
2735 },
2736 FsDirEntry {
2737 inode: 10,
2738 name: b"hello.txt".to_vec(),
2739 file_type: FsFileType::RegularFile,
2740 },
2741 FsDirEntry {
2742 inode: 11,
2743 name: b"subdir".to_vec(),
2744 file_type: FsFileType::Directory,
2745 },
2746 ]),
2747 11 => Ok(vec![
2748 FsDirEntry {
2749 inode: 11,
2750 name: b".".to_vec(),
2751 file_type: FsFileType::Directory,
2752 },
2753 FsDirEntry {
2754 inode: 2,
2755 name: b"..".to_vec(),
2756 file_type: FsFileType::Directory,
2757 },
2758 FsDirEntry {
2759 inode: 12,
2760 name: b"nested.txt".to_vec(),
2761 file_type: FsFileType::RegularFile,
2762 },
2763 ]),
2764 _ => Err(FsError::NotFound(format!("inode {ino}"))),
2765 }
2766 }
2767
2768 fn lookup(&mut self, parent_ino: u64, name: &[u8]) -> FsResult<Option<u64>> {
2769 let entries = self.read_dir(parent_ino)?;
2770 Ok(entries.iter().find(|e| e.name == name).map(|e| e.inode))
2771 }
2772
2773 fn metadata(&mut self, ino: u64) -> FsResult<FsMetadata> {
2774 let (file_type, size) = match ino {
2775 2 | 11 => (FsFileType::Directory, 4096),
2776 10 => (FsFileType::RegularFile, 12),
2777 12 => (FsFileType::RegularFile, 11),
2778 _ => return Err(FsError::NotFound(format!("inode {ino}"))),
2779 };
2780 Ok(FsMetadata {
2781 ino,
2782 file_type,
2783 mode: if file_type == FsFileType::Directory {
2784 0o40755
2785 } else {
2786 0o100_644
2787 },
2788 uid: 1000,
2789 gid: 1000,
2790 size: size as u64,
2791 links_count: if file_type == FsFileType::Directory {
2792 2
2793 } else {
2794 1
2795 },
2796 atime: FsTimestamp {
2797 seconds: 1_700_000_000,
2798 nanoseconds: 0,
2799 },
2800 mtime: FsTimestamp {
2801 seconds: 1_700_000_000,
2802 nanoseconds: 0,
2803 },
2804 ctime: FsTimestamp {
2805 seconds: 1_700_000_000,
2806 nanoseconds: 0,
2807 },
2808 crtime: FsTimestamp {
2809 seconds: 1_699_000_000,
2810 nanoseconds: 0,
2811 },
2812 allocated: true,
2813 })
2814 }
2815
2816 fn read_file(&mut self, ino: u64) -> FsResult<Vec<u8>> {
2817 match ino {
2818 10 => Ok(b"Hello, mock!".to_vec()),
2819 12 => Ok(b"Nested file".to_vec()),
2820 100..=103 => Ok(vec![0xAB; 100]),
2822 _ => Err(FsError::NotFound(format!("inode {ino}"))),
2824 }
2825 }
2826
2827 fn read_file_range(&mut self, ino: u64, offset: u64, len: u64) -> FsResult<Vec<u8>> {
2828 let data = self.read_file(ino)?;
2829 let start = (offset as usize).min(data.len());
2830 let end = (start + len as usize).min(data.len());
2831 Ok(data[start..end].to_vec())
2832 }
2833
2834 fn read_link(&mut self, _ino: u64) -> FsResult<Vec<u8>> {
2835 Err(FsError::NotFound("no symlinks in mock".to_string()))
2836 }
2837
2838 fn deleted_inodes(&mut self) -> FsResult<Vec<FsDeletedInode>> {
2839 Ok(vec![FsDeletedInode {
2840 ino: 99,
2841 file_type: FsFileType::RegularFile,
2842 size: 100,
2843 dtime: 1_700_001_000,
2844 recoverability: 0.75,
2845 }])
2846 }
2847
2848 fn deleted_nodes(&mut self) -> FsResult<Vec<crate::FsDeletedNode>> {
2849 let mk = |ino: u64,
2850 name: &[u8],
2851 parent: Option<u64>,
2852 mtime: i64,
2853 record_id: u64,
2854 allocation: crate::FsAllocation| {
2855 crate::FsDeletedNode {
2856 ino,
2857 name: name.to_vec(),
2858 parent_ino: parent,
2859 size: 100,
2860 file_type: FsFileType::RegularFile,
2861 allocation,
2862 record_id,
2863 atime: FsTimestamp::default(),
2864 mtime: FsTimestamp {
2865 seconds: mtime,
2866 nanoseconds: 0,
2867 },
2868 ctime: FsTimestamp::default(),
2869 crtime: FsTimestamp::default(),
2870 }
2871 };
2872 Ok(vec![
2873 mk(
2876 100,
2877 b"report.txt",
2878 Some(2),
2879 200,
2880 100,
2881 crate::FsAllocation::Deleted,
2882 ),
2883 mk(
2884 101,
2885 b"report.txt",
2886 Some(2),
2887 100,
2888 101,
2889 crate::FsAllocation::Deleted,
2890 ),
2891 mk(
2893 102,
2894 b"hello.txt",
2895 Some(2),
2896 300,
2897 102,
2898 crate::FsAllocation::Deleted,
2899 ),
2900 mk(103, b"", None, 150, 103, crate::FsAllocation::Orphan),
2902 mk(
2905 104,
2906 b"gone.txt",
2907 Some(2),
2908 250,
2909 104,
2910 crate::FsAllocation::Deleted,
2911 ),
2912 ])
2913 }
2914
2915 fn recover_file(&mut self, ino: u64) -> FsResult<FsRecoveryResult> {
2916 match ino {
2917 99 => Ok(FsRecoveryResult {
2918 ino: 99,
2919 data: vec![0xDE; 100],
2920 expected_size: 100,
2921 recovered_bytes: 100,
2922 recovery_percentage: 1.0,
2923 }),
2924 _ => Err(FsError::NotFound(format!("inode {ino}"))),
2925 }
2926 }
2927
2928 fn timeline(&mut self) -> FsResult<Vec<FsTimelineEvent>> {
2929 Ok(vec![FsTimelineEvent {
2930 timestamp: FsTimestamp {
2931 seconds: 1_700_000_000,
2932 nanoseconds: 0,
2933 },
2934 event_type: FsEventType::Created,
2935 inode: 10,
2936 size: 12,
2937 uid: 1000,
2938 gid: 1000,
2939 }])
2940 }
2941
2942 fn fs_info(&self) -> FsResult<serde_json::Value> {
2943 Ok(serde_json::json!({ "filesystem": "mock", "block_size": 4096 }))
2944 }
2945
2946 fn block_size(&self) -> u64 {
2947 4096
2948 }
2949 }
2950
2951 fn make_mock_fuse() -> ForensicFuseFs {
2952 ForensicFuseFs::new(
2953 Box::new(MockForensicFs),
2954 None,
2955 crate::MountLayout::DiskOverlay,
2956 crate::DeletedMode::Latest,
2957 )
2958 }
2959
2960 #[test]
2961 fn mock_ensure_deleted_cache() {
2962 let fuse = make_mock_fuse();
2963 assert!(fuse.deleted_cache.borrow().is_none());
2964 fuse.ensure_deleted_cache();
2965 let cache = fuse.deleted_cache.borrow();
2966 let entries = cache.as_ref().expect("cache should be populated");
2967 assert_eq!(entries.len(), 5);
2969 assert!(entries
2970 .iter()
2971 .any(|e| e.fs_ino == 100 && e.name == "report.txt"));
2972 }
2973
2974 #[test]
2975 fn mock_ensure_metadata_cache() {
2976 let fuse = make_mock_fuse();
2977 assert!(fuse.metadata_cache.borrow().is_none());
2978 fuse.ensure_metadata_cache();
2979 let cache = fuse.metadata_cache.borrow();
2980 let mc = cache.as_ref().expect("cache should be populated");
2981 let sb_str = String::from_utf8_lossy(&mc.superblock_json);
2982 assert!(
2983 sb_str.contains("mock"),
2984 "superblock_json should contain 'mock': {sb_str}"
2985 );
2986 assert!(
2987 !mc.timeline_jsonl.is_empty(),
2988 "timeline_jsonl should not be empty"
2989 );
2990 }
2991
2992 #[test]
2993 fn mock_root_ino_stored() {
2994 let fuse = make_mock_fuse();
2995 assert_eq!(fuse.root_ino, 2);
2996 }
2997
2998 #[test]
2999 fn mock_has_session_false() {
3000 let fuse = make_mock_fuse();
3001 assert!(!fuse.has_session());
3002 }
3003
3004 #[test]
3005 fn mock_read_file_through_fs() {
3006 let fuse = make_mock_fuse();
3007 let mut fs = fuse.fs.borrow_mut();
3008 let data = fs.read_file(10).expect("read_file(10) should succeed");
3009 assert_eq!(data, b"Hello, mock!");
3010 }
3011
3012 #[test]
3013 fn mock_read_file_range_through_fs() {
3014 let fuse = make_mock_fuse();
3015 let mut fs = fuse.fs.borrow_mut();
3016 let data = fs
3017 .read_file_range(10, 0, 5)
3018 .expect("read_file_range should succeed");
3019 assert_eq!(data, b"Hello");
3020 }
3021
3022 #[test]
3023 fn mock_lookup_through_fs() {
3024 let fuse = make_mock_fuse();
3025 let mut fs = fuse.fs.borrow_mut();
3026 let result = fs.lookup(2, b"hello.txt").expect("lookup should succeed");
3027 assert_eq!(result, Some(10));
3028 }
3029
3030 #[test]
3031 fn mock_metadata_through_fs() {
3032 let fuse = make_mock_fuse();
3033 let mut fs = fuse.fs.borrow_mut();
3034 let meta = fs.metadata(10).expect("metadata(10) should succeed");
3035 assert_eq!(meta.file_type, FsFileType::RegularFile);
3036 assert_eq!(meta.size, 12);
3037 assert_eq!(meta.ino, 10);
3038 }
3039
3040 #[test]
3041 fn mock_fs_to_attr() {
3042 let fuse = make_mock_fuse();
3043 let meta = {
3044 let mut fs = fuse.fs.borrow_mut();
3045 fs.metadata(10).expect("metadata(10) should succeed")
3046 };
3047 let attr = fs_to_attr(ro_ino(10), &meta);
3048 assert_eq!(attr.ino, ro_ino(10));
3049 assert_eq!(attr.kind, FileType::RegularFile);
3050 assert_eq!(attr.size, 12);
3051 assert_eq!(attr.perm, 0o644);
3052 assert_eq!(attr.uid, 1000);
3053 assert_eq!(attr.gid, 1000);
3054 assert_eq!(attr.nlink, 1);
3055 assert_eq!(attr.blksize, 4096);
3056 let expected_atime = UNIX_EPOCH + Duration::from_secs(1_700_000_000);
3057 assert_eq!(attr.atime, expected_atime);
3058 let expected_crtime = UNIX_EPOCH + Duration::from_secs(1_699_000_000);
3059 assert_eq!(attr.crtime, expected_crtime);
3060 }
3061
3062 #[test]
3063 fn mock_timeline_through_fs() {
3064 let fuse = make_mock_fuse();
3065 let mut fs = fuse.fs.borrow_mut();
3066 let events = fs.timeline().expect("timeline should succeed");
3067 assert_eq!(events.len(), 1);
3068 assert_eq!(events[0].event_type, FsEventType::Created);
3069 assert_eq!(events[0].inode, 10);
3070 assert_eq!(events[0].size, 12);
3071 }
3072
3073 #[test]
3074 fn mock_ensure_journal_cache_empty() {
3075 let fuse = make_mock_fuse();
3076 assert!(fuse.journal_cache.borrow().is_none());
3077 fuse.ensure_journal_cache();
3078 let cache = fuse.journal_cache.borrow();
3079 let entries = cache.as_ref().expect("cache should be populated");
3080 assert!(
3081 entries.is_empty(),
3082 "mock has no journal_transactions override, should be empty"
3083 );
3084 }
3085
3086 fn make_mock_fuse_mode(mode: crate::DeletedMode) -> ForensicFuseFs {
3091 ForensicFuseFs::new(
3092 Box::new(MockForensicFs),
3093 None,
3094 crate::MountLayout::DiskOverlay,
3095 mode,
3096 )
3097 }
3098
3099 #[test]
3100 fn filename_safe_utc_has_no_colons() {
3101 assert_eq!(filename_safe_utc(1_700_000_000), "2023-11-14T22-13-20Z");
3103 }
3104
3105 #[test]
3106 fn deleted_cache_latest_places_newest_in_place() {
3107 let fuse = make_mock_fuse_mode(crate::DeletedMode::Latest);
3108 fuse.ensure_deleted_cache();
3109 let cache = fuse.deleted_cache.borrow();
3110 let entries = cache.as_ref().expect("cache populated");
3111 let by = |ino: u64| {
3112 entries
3113 .iter()
3114 .find(|e| e.fs_ino == ino)
3115 .unwrap_or_else(|| panic!("no cache entry for ino {ino}"))
3116 };
3117 let a = by(100);
3119 assert_eq!(a.name, "report.txt");
3120 assert!(!a.orphan);
3121 let b = by(101);
3123 assert!(b.orphan);
3124 assert!(b.name.starts_with("report.txt@"), "got {}", b.name);
3125 assert!(b.name.ends_with("~101"), "got {}", b.name);
3126 assert!(by(102).orphan);
3128 assert!(by(103).orphan);
3130 let g = by(104);
3132 assert_eq!(g.name, "gone.txt");
3133 assert!(!g.orphan);
3134 assert!(!g.readable);
3135 }
3136
3137 #[test]
3138 fn deleted_cache_off_is_empty() {
3139 let fuse = make_mock_fuse_mode(crate::DeletedMode::Off);
3140 fuse.ensure_deleted_cache();
3141 assert!(fuse
3142 .deleted_cache
3143 .borrow()
3144 .as_ref()
3145 .expect("cache populated")
3146 .is_empty());
3147 }
3148
3149 #[test]
3150 fn deleted_cache_all_routes_everything_to_orphans() {
3151 let fuse = make_mock_fuse_mode(crate::DeletedMode::All);
3152 fuse.ensure_deleted_cache();
3153 let cache = fuse.deleted_cache.borrow();
3154 let entries = cache.as_ref().expect("cache populated");
3155 assert_eq!(entries.len(), 5);
3156 assert!(
3157 entries.iter().all(|e| e.orphan),
3158 "All -> every instance orphan"
3159 );
3160 let a = entries.iter().find(|e| e.fs_ino == 100).unwrap();
3161 assert!(a.name.starts_with("report.txt@"), "got {}", a.name);
3162 }
3163
3164 #[test]
3165 fn deleted_cache_never_fabricates_unknown_names() {
3166 let fuse = make_mock_fuse_mode(crate::DeletedMode::Latest);
3167 fuse.ensure_deleted_cache();
3168 let cache = fuse.deleted_cache.borrow();
3169 for e in cache.as_ref().expect("cache populated") {
3170 assert!(!e.name.ends_with("_unknown"), "fabricated: {}", e.name);
3171 }
3172 }
3173
3174 #[test]
3175 fn timeline_jsonl_carries_every_deleted_instance() {
3176 let fuse = make_mock_fuse_mode(crate::DeletedMode::Latest);
3177 fuse.ensure_metadata_cache();
3178 let cache = fuse.metadata_cache.borrow();
3179 let mc = cache.as_ref().expect("metadata cache populated");
3180 let text = String::from_utf8_lossy(&mc.timeline_jsonl);
3181 let rows: Vec<serde_json::Value> = text
3183 .lines()
3184 .filter_map(|l| serde_json::from_str::<serde_json::Value>(l).ok())
3185 .filter(|v| v.get("placement").is_some())
3186 .collect();
3187 assert_eq!(rows.len(), 5, "one row per deleted instance");
3189 let report: Vec<_> = rows.iter().filter(|r| r["name"] == "report.txt").collect();
3191 assert_eq!(report.len(), 2, "both report.txt instances present");
3192 assert!(report.iter().any(|r| r["placement"] == "in-place"));
3193 assert!(report.iter().any(|r| r["placement"] == "orphan"));
3194 let r = &rows[0];
3196 for f in ["path", "name", "record_id", "allocation", "status", "macb"] {
3197 assert!(r.get(f).is_some(), "row missing {f}: {r}");
3198 }
3199 assert!(r["macb"].get("modified").is_some());
3200 assert!(rows
3202 .iter()
3203 .any(|r| r["name"] == "gone.txt" && r["status"] == "unreadable"));
3204 }
3205
3206 #[test]
3212 fn in_place_children_injected_under_parent() {
3213 let fuse = make_mock_fuse_mode(crate::DeletedMode::Latest);
3214 fuse.ensure_deleted_cache();
3215 let cache = fuse.deleted_cache.borrow();
3216 let entries = cache.as_ref().expect("cache populated");
3217 let kids = deleted_in_place_children(entries, 2);
3218 let names: Vec<&str> = kids.iter().map(|(_, n)| n.as_str()).collect();
3221 assert!(names.contains(&"report.txt"), "got {names:?}");
3222 assert!(names.contains(&"gone.txt"), "got {names:?}");
3223 assert_eq!(
3224 kids.len(),
3225 2,
3226 "only the two in-place deletes under parent 2"
3227 );
3228 assert!(
3230 !names.iter().any(|n| n.contains("(deleted)")),
3231 "got {names:?}"
3232 );
3233 assert!(kids.iter().any(|(ino, _)| *ino == deleted_ino(100)));
3235 assert!(deleted_in_place_children(entries, 11).is_empty());
3237 assert!(!kids.iter().any(|(ino, _)| *ino == deleted_ino(101)));
3239 }
3240
3241 #[test]
3247 fn orphans_dir_is_top_level_only_when_present() {
3248 use crate::inode_map::FUSE_ORPHANS_INO;
3249 let with = root_dir_listing(true);
3252 assert!(
3253 with.iter()
3254 .any(|&(ino, n)| ino == FUSE_ORPHANS_INO && n == "$Orphans"),
3255 "root should list $Orphans when orphans exist: {with:?}"
3256 );
3257 assert!(
3258 !with.iter().any(|&(_, n)| n == "deleted"),
3259 "the flat deleted/ dir is removed in v2: {with:?}"
3260 );
3261 for want in ["ro", "rw", "metadata", "session"] {
3263 assert!(
3264 with.iter().any(|&(_, n)| n == want),
3265 "missing {want}: {with:?}"
3266 );
3267 }
3268 let without = root_dir_listing(false);
3270 assert!(
3271 !without.iter().any(|&(_, n)| n == "$Orphans"),
3272 "no $Orphans without orphan entries: {without:?}"
3273 );
3274 }
3275
3276 #[test]
3277 fn cache_has_orphans_reflects_placement() {
3278 let fuse = make_mock_fuse_mode(crate::DeletedMode::Latest);
3279 fuse.ensure_deleted_cache();
3280 let cache = fuse.deleted_cache.borrow();
3281 let entries = cache.as_ref().expect("cache populated");
3282 assert!(cache_has_orphans(entries));
3284 assert!(!cache_has_orphans(&[]));
3285 }
3286
3287 #[test]
3293 fn deleted_xattr_names_are_the_marking_schema() {
3294 let names = deleted_xattr_names();
3295 for want in [
3296 "user.4n6.status",
3297 "user.4n6.macb.modified",
3298 "user.4n6.macb.accessed",
3299 "user.4n6.macb.changed",
3300 "user.4n6.macb.born",
3301 ] {
3302 assert!(names.contains(&want), "missing {want}: {names:?}");
3303 }
3304 assert_eq!(names.len(), 5);
3305 }
3306
3307 #[test]
3308 fn deleted_xattr_value_status_and_macb() {
3309 let fuse = make_mock_fuse_mode(crate::DeletedMode::Latest);
3310 fuse.ensure_deleted_cache();
3311 let cache = fuse.deleted_cache.borrow();
3312 let entries = cache.as_ref().expect("cache populated");
3313 let by = |ino: u64| entries.iter().find(|e| e.fs_ino == ino).unwrap();
3314
3315 let a = by(100);
3317 assert_eq!(
3318 deleted_xattr_value(a, "user.4n6.status").as_deref(),
3319 Some(b"deleted".as_ref())
3320 );
3321 assert_eq!(
3323 deleted_xattr_value(a, "user.4n6.macb.modified").as_deref(),
3324 Some(b"1970-01-01T00:03:20Z".as_ref())
3325 );
3326 assert_eq!(
3328 deleted_xattr_value(by(103), "user.4n6.status").as_deref(),
3329 Some(b"orphan".as_ref())
3330 );
3331 assert!(deleted_xattr_value(a, "user.4n6.nope").is_none());
3333 }
3334
3335 #[test]
3341 fn deleted_cow_base_yields_recovered_bytes() {
3342 let fuse = make_mock_fuse_mode(crate::DeletedMode::Latest);
3343 fuse.ensure_deleted_cache();
3344 let cache = fuse.deleted_cache.borrow();
3345 let entries = cache.as_ref().expect("cache populated");
3346
3347 let base = deleted_cow_base(entries, 100).expect("readable -> copy-up base");
3350 assert_eq!(base, vec![0xAB; 100]);
3351
3352 assert!(deleted_cow_base(entries, 104).is_none());
3355
3356 assert!(deleted_cow_base(entries, 999).is_none());
3358 }
3359}