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_DELETED_INO, FUSE_JOURNAL_INO, FUSE_METADATA_INO, FUSE_ROOT_INO,
6 FUSE_RO_INO, FUSE_RW_INO, FUSE_SESSION_INO, FUSE_UNALLOCATED_INO,
7};
8use crate::session::Session;
9use crate::types::{FsBlockRange, FsEventType, FsFileType, FsMetadata, FsTimestamp};
10use crate::ForensicFs;
11use fuser::{
12 FileAttr, FileType, Filesystem, ReplyAttr, ReplyCreate, ReplyData, ReplyDirectory, ReplyEmpty,
13 ReplyEntry, ReplyWrite, Request, TimeOrNow,
14};
15use std::cell::RefCell;
16use std::ffi::OsStr;
17use std::time::{Duration, SystemTime, UNIX_EPOCH};
18
19const TTL: Duration = Duration::from_secs(1);
20
21const VIRTUAL_DIRS: &[(u64, &str)] = &[
23 (FUSE_RO_INO, "ro"),
24 (FUSE_RW_INO, "rw"),
25 (FUSE_DELETED_INO, "deleted"),
26 (FUSE_JOURNAL_INO, "journal"),
27 (FUSE_METADATA_INO, "metadata"),
28 (FUSE_UNALLOCATED_INO, "unallocated"),
29 (FUSE_SESSION_INO, "session"),
30];
31
32struct DeletedEntry {
34 fs_ino: u64,
35 name: String,
36 size: u64,
37 data: Vec<u8>,
38}
39
40struct JournalTxnEntry {
42 sequence: u64,
43 name: String,
44}
45
46struct MetadataCache {
48 superblock_json: Vec<u8>,
49 timeline_jsonl: Vec<u8>,
50}
51
52struct UnallocatedEntry {
54 #[allow(dead_code)]
55 range_id: u64,
56 name: String,
57 start: u64,
58 length: u64,
59}
60
61pub struct ForensicFuseFs {
62 fs: RefCell<Box<dyn ForensicFs + Send>>,
63 session: RefCell<Option<Session>>,
64 overlay_ino_counter: RefCell<u64>,
66 root_ino: u64,
68 deleted_cache: RefCell<Option<Vec<DeletedEntry>>>,
70 journal_cache: RefCell<Option<Vec<JournalTxnEntry>>>,
72 metadata_cache: RefCell<Option<MetadataCache>>,
74 unallocated_cache: RefCell<Option<Vec<UnallocatedEntry>>>,
76 layout: crate::MountLayout,
78}
79
80impl ForensicFuseFs {
81 pub fn new(
82 fs: Box<dyn ForensicFs + Send>,
83 session: Option<Session>,
84 layout: crate::MountLayout,
85 ) -> Self {
86 let root_ino = fs.root_ino();
87 Self {
88 fs: RefCell::new(fs),
89 session: RefCell::new(session),
90 overlay_ino_counter: RefCell::new(1),
91 root_ino,
92 deleted_cache: RefCell::new(None),
93 journal_cache: RefCell::new(None),
94 metadata_cache: RefCell::new(None),
95 unallocated_cache: RefCell::new(None),
96 layout,
97 }
98 }
99
100 fn has_session(&self) -> bool {
102 self.session.borrow().is_some()
103 }
104
105 fn modified_overlay_id(fs_ino: u64) -> String {
107 format!("ino_{fs_ino}")
108 }
109
110 fn alloc_overlay_ino(&self) -> u64 {
112 let mut counter = self.overlay_ino_counter.borrow_mut();
113 let ino = *counter;
114 *counter += 1;
115 ino
116 }
117
118 fn created_overlay_id(counter: u64) -> String {
120 format!("new_{counter}")
121 }
122
123 fn overlay_created_attr(fuse_ino: u64, size: u64, is_dir: bool) -> FileAttr {
125 let kind = if is_dir {
126 FileType::Directory
127 } else {
128 FileType::RegularFile
129 };
130 FileAttr {
131 ino: fuse_ino,
132 size,
133 blocks: size.div_ceil(512),
134 atime: SystemTime::now(),
135 mtime: SystemTime::now(),
136 ctime: SystemTime::now(),
137 crtime: SystemTime::now(),
138 kind,
139 perm: if is_dir { 0o755 } else { 0o644 },
140 nlink: 1,
141 uid: 0,
142 gid: 0,
143 rdev: 0,
144 blksize: 4096,
145 flags: 0,
146 }
147 }
148
149 fn rw_parent_to_fs(&self, parent: u64) -> Option<u64> {
151 match parent {
152 FUSE_RW_INO => Some(self.root_ino),
153 _ => match decode_fuse_ino(parent) {
154 InodeNamespace::Rw(ino) => Some(ino),
155 _ => None,
156 },
157 }
158 }
159
160 fn is_whiteout(&self, fs_ino: u64) -> bool {
162 let session = self.session.borrow();
163 match session.as_ref() {
164 Some(s) => s.overlay.deleted.contains(&fs_ino),
165 None => false,
166 }
167 }
168
169 fn ensure_deleted_cache(&self) {
171 if self.deleted_cache.borrow().is_some() {
172 return;
173 }
174 let mut fs = self.fs.borrow_mut();
175 let deleted_inodes = fs.deleted_inodes().unwrap_or_default();
176 let mut entries = Vec::new();
177 for di in &deleted_inodes {
178 let name = format!("{}_unknown", di.ino);
179 let result = fs.recover_file(di.ino);
180 let (size, data) = match result {
181 Ok(r) => (r.data.len() as u64, r.data),
182 Err(_) => (0, Vec::new()),
183 };
184 entries.push(DeletedEntry {
185 fs_ino: di.ino,
186 name,
187 size,
188 data,
189 });
190 }
191 *self.deleted_cache.borrow_mut() = Some(entries);
192 }
193
194 fn ensure_journal_cache(&self) {
196 if self.journal_cache.borrow().is_some() {
197 return;
198 }
199 let mut fs = self.fs.borrow_mut();
200 let entries = match fs.journal_transactions() {
201 Ok(txns) => txns
202 .iter()
203 .map(|txn| JournalTxnEntry {
204 sequence: txn.sequence,
205 name: format!("txn_{}", txn.sequence),
206 })
207 .collect(),
208 Err(_) => Vec::new(),
209 };
210 *self.journal_cache.borrow_mut() = Some(entries);
211 }
212
213 fn ensure_metadata_cache(&self) {
215 if self.metadata_cache.borrow().is_some() {
216 return;
217 }
218 let fs = self.fs.borrow();
219
220 let superblock_json = match fs.fs_info() {
222 Ok(info) => serde_json::to_string_pretty(&info)
223 .unwrap_or_default()
224 .into_bytes(),
225 Err(_) => b"{}".to_vec(),
226 };
227 drop(fs);
228
229 let mut fs = self.fs.borrow_mut();
231 let timeline_jsonl = match fs.timeline() {
232 Ok(events) => {
233 let mut buf = Vec::new();
234 for event in &events {
235 let event_type = match event.event_type {
236 FsEventType::Created => "Created",
237 FsEventType::Modified => "Modified",
238 FsEventType::Accessed => "Accessed",
239 FsEventType::Changed => "Changed",
240 FsEventType::Deleted => "Deleted",
241 FsEventType::Mounted => "Mounted",
242 };
243 let line = serde_json::json!({
244 "timestamp_secs": event.timestamp.seconds,
245 "timestamp_nsecs": event.timestamp.nanoseconds,
246 "event_type": event_type,
247 "inode": event.inode,
248 "size": event.size,
249 "uid": event.uid,
250 "gid": event.gid,
251 });
252 let line_str = serde_json::to_string(&line).unwrap_or_default();
253 buf.extend_from_slice(line_str.as_bytes());
254 buf.push(b'\n');
255 }
256 buf
257 }
258 Err(_) => Vec::new(),
259 };
260
261 *self.metadata_cache.borrow_mut() = Some(MetadataCache {
262 superblock_json,
263 timeline_jsonl,
264 });
265 }
266
267 fn ensure_unallocated_cache(&self) {
269 if self.unallocated_cache.borrow().is_some() {
270 return;
271 }
272 let mut fs = self.fs.borrow_mut();
273 let entries = match fs.unallocated_blocks() {
274 Ok(ranges) => ranges
275 .iter()
276 .enumerate()
277 .map(|(i, r)| UnallocatedEntry {
278 range_id: i as u64,
279 name: format!("blocks_{}-{}.raw", r.start, r.start + r.length),
280 start: r.start,
281 length: r.length,
282 })
283 .collect(),
284 Err(_) => Vec::new(),
285 };
286 *self.unallocated_cache.borrow_mut() = Some(entries);
287 }
288
289 fn find_created_by_name(&self, parent_ino: u64, name: &[u8]) -> Option<(String, u64, bool)> {
291 let session = self.session.borrow();
292 let session = session.as_ref()?;
293 let name_str = std::str::from_utf8(name).ok()?;
294 for (id, entry) in &session.overlay.created {
295 if entry.parent_ino == parent_ino && entry.name == name_str {
296 let counter: u64 = id.strip_prefix("new_").and_then(|s| s.parse().ok())?;
297 return Some((id.clone(), counter, false));
298 }
299 }
300 for (id, entry) in &session.overlay.dirs {
301 if entry.parent_ino == parent_ino && entry.name == name_str {
302 let counter: u64 = id.strip_prefix("new_").and_then(|s| s.parse().ok())?;
303 return Some((id.clone(), counter, true));
304 }
305 }
306 None
307 }
308}
309
310fn ts_to_systime(t: &FsTimestamp) -> SystemTime {
312 if t.seconds >= 0 {
313 UNIX_EPOCH + Duration::new(t.seconds as u64, t.nanoseconds)
314 } else {
315 UNIX_EPOCH
316 }
317}
318
319fn fs_to_attr(fuse_ino: u64, meta: &FsMetadata) -> FileAttr {
321 let kind = match meta.file_type {
322 FsFileType::RegularFile | FsFileType::Unknown => FileType::RegularFile,
323 FsFileType::Directory => FileType::Directory,
324 FsFileType::Symlink => FileType::Symlink,
325 FsFileType::CharDevice => FileType::CharDevice,
326 FsFileType::BlockDevice => FileType::BlockDevice,
327 FsFileType::Fifo => FileType::NamedPipe,
328 FsFileType::Socket => FileType::Socket,
329 };
330
331 FileAttr {
332 ino: fuse_ino,
333 size: meta.size,
334 blocks: meta.size.div_ceil(512),
335 atime: ts_to_systime(&meta.atime),
336 mtime: ts_to_systime(&meta.mtime),
337 ctime: ts_to_systime(&meta.ctime),
338 crtime: ts_to_systime(&meta.crtime),
339 kind,
340 perm: meta.mode & 0o7777,
341 nlink: u32::from(meta.links_count),
342 uid: meta.uid,
343 gid: meta.gid,
344 rdev: 0,
345 blksize: 4096,
346 flags: 0,
347 }
348}
349
350fn virtual_dir_attr(ino: u64) -> FileAttr {
352 FileAttr {
353 ino,
354 size: 0,
355 blocks: 0,
356 atime: UNIX_EPOCH,
357 mtime: UNIX_EPOCH,
358 ctime: UNIX_EPOCH,
359 crtime: UNIX_EPOCH,
360 kind: FileType::Directory,
361 perm: 0o555,
362 nlink: 2,
363 uid: 0,
364 gid: 0,
365 rdev: 0,
366 blksize: 4096,
367 flags: 0,
368 }
369}
370
371fn virtual_file_attr(ino: u64, size: u64) -> FileAttr {
373 FileAttr {
374 ino,
375 size,
376 blocks: size.div_ceil(512),
377 atime: UNIX_EPOCH,
378 mtime: UNIX_EPOCH,
379 ctime: UNIX_EPOCH,
380 crtime: UNIX_EPOCH,
381 kind: FileType::RegularFile,
382 perm: 0o444,
383 nlink: 1,
384 uid: 0,
385 gid: 0,
386 rdev: 0,
387 blksize: 4096,
388 flags: 0,
389 }
390}
391
392fn fs_file_type_to_fuse(t: FsFileType) -> FileType {
394 match t {
395 FsFileType::RegularFile | FsFileType::Unknown => FileType::RegularFile,
396 FsFileType::Directory => FileType::Directory,
397 FsFileType::Symlink => FileType::Symlink,
398 FsFileType::CharDevice => FileType::CharDevice,
399 FsFileType::BlockDevice => FileType::BlockDevice,
400 FsFileType::Fifo => FileType::NamedPipe,
401 FsFileType::Socket => FileType::Socket,
402 }
403}
404
405fn root_children(
413 layout: crate::MountLayout,
414 fs: &mut dyn ForensicFs,
415 root_ino: u64,
416) -> crate::FsResult<Vec<(u64, Vec<u8>, FileType)>> {
417 match layout {
418 crate::MountLayout::DiskOverlay => Ok(VIRTUAL_DIRS
419 .iter()
420 .map(|&(ino, name)| (ino, name.as_bytes().to_vec(), FileType::Directory))
421 .collect()),
422 crate::MountLayout::Raw => {
423 let mut out = Vec::new();
424 for e in fs.read_dir(root_ino)? {
425 if e.name == b"." || e.name == b".." {
426 continue;
427 }
428 out.push((ro_ino(e.inode), e.name, fs_file_type_to_fuse(e.file_type)));
431 }
432 Ok(out)
433 }
434 }
435}
436
437impl Filesystem for ForensicFuseFs {
438 fn lookup(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEntry) {
439 let name_bytes = name.as_encoded_bytes();
440
441 if parent == FUSE_ROOT_INO && self.layout == crate::MountLayout::DiskOverlay {
444 for &(ino, dir_name) in VIRTUAL_DIRS {
445 if name_bytes == dir_name.as_bytes() {
446 let attr = if ino == FUSE_RW_INO && self.has_session() {
447 let mut a = virtual_dir_attr(ino);
448 a.perm = 0o755;
449 a
450 } else {
451 virtual_dir_attr(ino)
452 };
453 reply.entry(&TTL, &attr, 0);
454 return;
455 }
456 }
457 reply.error(libc::ENOENT);
458 return;
459 }
460
461 if parent == FUSE_DELETED_INO {
463 self.ensure_deleted_cache();
464 let cache = self.deleted_cache.borrow();
465 if let Some(entries) = cache.as_ref() {
466 for entry in entries {
467 if name_bytes == entry.name.as_bytes() {
468 let fuse_ino = deleted_ino(entry.fs_ino);
469 let attr = virtual_file_attr(fuse_ino, entry.size);
470 reply.entry(&TTL, &attr, 0);
471 return;
472 }
473 }
474 }
475 reply.error(libc::ENOENT);
476 return;
477 }
478
479 if parent == FUSE_JOURNAL_INO {
481 self.ensure_journal_cache();
482 let cache = self.journal_cache.borrow();
483 if let Some(entries) = cache.as_ref() {
484 for entry in entries {
485 if name_bytes == entry.name.as_bytes() {
486 let fuse_ino = journal_ino(entry.sequence);
487 let attr = virtual_dir_attr(fuse_ino);
488 reply.entry(&TTL, &attr, 0);
489 return;
490 }
491 }
492 }
493 reply.error(libc::ENOENT);
494 return;
495 }
496
497 if parent == FUSE_METADATA_INO {
499 self.ensure_metadata_cache();
500 let cache = self.metadata_cache.borrow();
501 if let Some(mc) = cache.as_ref() {
502 if name_bytes == b"superblock.json" {
503 let fuse_ino = metadata_ino(1);
504 let attr = virtual_file_attr(fuse_ino, mc.superblock_json.len() as u64);
505 reply.entry(&TTL, &attr, 0);
506 return;
507 }
508 if name_bytes == b"timeline.jsonl" {
509 let fuse_ino = metadata_ino(2);
510 let attr = virtual_file_attr(fuse_ino, mc.timeline_jsonl.len() as u64);
511 reply.entry(&TTL, &attr, 0);
512 return;
513 }
514 }
515 reply.error(libc::ENOENT);
516 return;
517 }
518
519 if parent == FUSE_UNALLOCATED_INO {
521 self.ensure_unallocated_cache();
522 let cache = self.unallocated_cache.borrow();
523 if let Some(entries) = cache.as_ref() {
524 for (i, entry) in entries.iter().enumerate() {
525 if name_bytes == entry.name.as_bytes() {
526 let fuse_ino = unallocated_ino(i as u64);
527 let block_size = self.fs.borrow().block_size();
528 let size = entry.length * block_size;
529 let attr = virtual_file_attr(fuse_ino, size);
530 reply.entry(&TTL, &attr, 0);
531 return;
532 }
533 }
534 }
535 reply.error(libc::ENOENT);
536 return;
537 }
538
539 if parent == FUSE_SESSION_INO {
541 if name_bytes == b"status.json" && self.has_session() {
542 let session = self.session.borrow();
543 let s = session.as_ref().unwrap();
544 let status = serde_json::json!({
545 "image_path": s.metadata.image_path,
546 "image_sha256": s.metadata.image_sha256,
547 "created": s.metadata.created,
548 });
549 let data = serde_json::to_string_pretty(&status)
550 .unwrap_or_default()
551 .into_bytes();
552 let fuse_ino = metadata_ino(100);
553 let attr = virtual_file_attr(fuse_ino, data.len() as u64);
554 reply.entry(&TTL, &attr, 0);
555 return;
556 }
557 reply.error(libc::ENOENT);
558 return;
559 }
560
561 if let Some(fs_parent) = self.rw_parent_to_fs(parent) {
563 if let Some((id, counter, is_dir)) = self.find_created_by_name(fs_parent, name_bytes) {
565 let session = self.session.borrow();
566 let session = session.as_ref().unwrap();
567 let entry = if is_dir {
568 session.overlay.dirs.get(&id)
569 } else {
570 session.overlay.created.get(&id)
571 };
572 if let Some(entry) = entry {
573 let fuse_ino = rw_ino(counter + 9_000_000);
574 let attr = Self::overlay_created_attr(fuse_ino, entry.size, is_dir);
575 reply.entry(&TTL, &attr, 0);
576 return;
577 }
578 }
579
580 {
582 let mut fs = self.fs.borrow_mut();
583 match fs.lookup(fs_parent, name_bytes) {
584 Ok(Some(child_ino)) => {
585 if self.is_whiteout(child_ino) {
587 reply.error(libc::ENOENT);
588 return;
589 }
590
591 let session = self.session.borrow();
593 let overlay_id = Self::modified_overlay_id(child_ino);
594 if let Some(s) = session.as_ref() {
595 if s.overlay.modified.contains_key(&child_ino) {
596 if let Ok(meta) = fs.metadata(child_ino) {
597 let fuse_ino = rw_ino(child_ino);
598 let mut attr = fs_to_attr(fuse_ino, &meta);
599 if let Ok(data) = s.read_overlay_file(&overlay_id) {
600 attr.size = data.len() as u64;
601 attr.blocks = attr.size.div_ceil(512);
602 }
603 reply.entry(&TTL, &attr, 0);
604 return;
605 }
606 reply.error(libc::EIO);
607 return;
608 }
609 }
610
611 match fs.metadata(child_ino) {
613 Ok(meta) => {
614 let fuse_ino = rw_ino(child_ino);
615 reply.entry(&TTL, &fs_to_attr(fuse_ino, &meta), 0);
616 }
617 Err(_) => reply.error(libc::EIO),
618 }
619 return;
620 }
621 Ok(None) => {
622 reply.error(libc::ENOENT);
623 return;
624 }
625 Err(_) => {
626 reply.error(libc::EIO);
627 return;
628 }
629 }
630 }
631 }
632
633 let fs_parent = match parent {
636 FUSE_RO_INO => self.root_ino,
637 FUSE_ROOT_INO if self.layout == crate::MountLayout::Raw => self.root_ino,
638 _ => {
639 if let InodeNamespace::Ro(ino) = decode_fuse_ino(parent) {
640 ino
641 } else {
642 reply.error(libc::ENOENT);
643 return;
644 }
645 }
646 };
647
648 let mut fs = self.fs.borrow_mut();
649 match fs.lookup(fs_parent, name_bytes) {
650 Ok(Some(child_ino)) => match fs.metadata(child_ino) {
651 Ok(meta) => {
652 let fuse_ino = ro_ino(child_ino);
653 reply.entry(&TTL, &fs_to_attr(fuse_ino, &meta), 0);
654 }
655 Err(_) => reply.error(libc::EIO),
656 },
657 Ok(None) => reply.error(libc::ENOENT),
658 Err(_) => reply.error(libc::EIO),
659 }
660 }
661
662 fn getattr(&mut self, _req: &Request, ino: u64, _fh: Option<u64>, reply: ReplyAttr) {
663 if ino == FUSE_ROOT_INO {
665 reply.attr(&TTL, &virtual_dir_attr(FUSE_ROOT_INO));
666 return;
667 }
668
669 if (FUSE_RO_INO..=FUSE_SESSION_INO).contains(&ino) {
671 let mut attr = virtual_dir_attr(ino);
672 if ino == FUSE_RW_INO && self.has_session() {
673 attr.perm = 0o755;
674 }
675 reply.attr(&TTL, &attr);
676 return;
677 }
678
679 match decode_fuse_ino(ino) {
680 InodeNamespace::Deleted(fs_ino) => {
681 self.ensure_deleted_cache();
682 let cache = self.deleted_cache.borrow();
683 if let Some(entries) = cache.as_ref() {
684 if let Some(entry) = entries.iter().find(|e| e.fs_ino == fs_ino) {
685 reply.attr(&TTL, &virtual_file_attr(ino, entry.size));
686 return;
687 }
688 }
689 reply.error(libc::ENOENT);
690 }
691 InodeNamespace::Metadata(id) => {
692 self.ensure_metadata_cache();
693 let cache = self.metadata_cache.borrow();
694 if let Some(mc) = cache.as_ref() {
695 match id {
696 1 => {
697 reply.attr(
698 &TTL,
699 &virtual_file_attr(ino, mc.superblock_json.len() as u64),
700 );
701 }
702 2 => {
703 reply.attr(
704 &TTL,
705 &virtual_file_attr(ino, mc.timeline_jsonl.len() as u64),
706 );
707 }
708 100 => {
709 if self.has_session() {
711 let session = self.session.borrow();
712 let s = session.as_ref().unwrap();
713 let status = serde_json::json!({
714 "image_path": s.metadata.image_path,
715 "image_sha256": s.metadata.image_sha256,
716 "created": s.metadata.created,
717 });
718 let data =
719 serde_json::to_string_pretty(&status).unwrap_or_default();
720 reply.attr(&TTL, &virtual_file_attr(ino, data.len() as u64));
721 } else {
722 reply.error(libc::ENOENT);
723 }
724 }
725 _ => reply.error(libc::ENOENT),
726 }
727 } else {
728 reply.error(libc::ENOENT);
729 }
730 }
731 InodeNamespace::Journal(seq) => {
732 self.ensure_journal_cache();
733 let cache = self.journal_cache.borrow();
734 if let Some(entries) = cache.as_ref() {
735 if entries.iter().any(|e| e.sequence == seq) {
736 reply.attr(&TTL, &virtual_dir_attr(ino));
737 } else {
738 reply.error(libc::ENOENT);
739 }
740 } else {
741 reply.error(libc::ENOENT);
742 }
743 }
744 InodeNamespace::Unallocated(range_id) => {
745 self.ensure_unallocated_cache();
746 let cache = self.unallocated_cache.borrow();
747 if let Some(entries) = cache.as_ref() {
748 if let Some(entry) = entries.get(range_id as usize) {
749 let block_size = self.fs.borrow().block_size();
750 let size = entry.length * block_size;
751 reply.attr(&TTL, &virtual_file_attr(ino, size));
752 } else {
753 reply.error(libc::ENOENT);
754 }
755 } else {
756 reply.error(libc::ENOENT);
757 }
758 }
759 InodeNamespace::Ro(fs_ino) => {
760 let mut fs = self.fs.borrow_mut();
761 match fs.metadata(fs_ino) {
762 Ok(meta) => reply.attr(&TTL, &fs_to_attr(ino, &meta)),
763 Err(_) => reply.error(libc::EIO),
764 }
765 }
766 InodeNamespace::Rw(rw_id) => {
767 if rw_id >= 9_000_000 {
769 let counter = rw_id - 9_000_000;
770 let created_id = Self::created_overlay_id(counter);
771 let session = self.session.borrow();
772 if let Some(s) = session.as_ref() {
773 if let Some(entry) = s.overlay.created.get(&created_id) {
774 let attr = Self::overlay_created_attr(ino, entry.size, false);
775 reply.attr(&TTL, &attr);
776 return;
777 }
778 if let Some(entry) = s.overlay.dirs.get(&created_id) {
779 let attr = Self::overlay_created_attr(ino, entry.size, true);
780 reply.attr(&TTL, &attr);
781 return;
782 }
783 }
784 reply.error(libc::ENOENT);
785 return;
786 }
787
788 let fs_ino = rw_id;
790 let mut fs = self.fs.borrow_mut();
791 match fs.metadata(fs_ino) {
792 Ok(meta) => {
793 let mut attr = fs_to_attr(ino, &meta);
794 let session = self.session.borrow();
796 if let Some(s) = session.as_ref() {
797 let overlay_id = Self::modified_overlay_id(fs_ino);
798 if s.overlay.modified.contains_key(&fs_ino) {
799 if let Ok(data) = s.read_overlay_file(&overlay_id) {
800 attr.size = data.len() as u64;
801 attr.blocks = attr.size.div_ceil(512);
802 }
803 }
804 }
805 reply.attr(&TTL, &attr);
806 }
807 Err(_) => reply.error(libc::EIO),
808 }
809 }
810 _ => reply.error(libc::ENOENT),
811 }
812 }
813
814 fn readdir(
815 &mut self,
816 _req: &Request,
817 ino: u64,
818 _fh: u64,
819 offset: i64,
820 mut reply: ReplyDirectory,
821 ) {
822 let offset = offset as usize;
823
824 if ino == FUSE_ROOT_INO {
827 let mut entries: Vec<(u64, FileType, String)> = vec![
828 (FUSE_ROOT_INO, FileType::Directory, ".".to_string()),
829 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
830 ];
831 {
832 let mut fs = self.fs.borrow_mut();
833 let Ok(children) = root_children(self.layout, &mut **fs, self.root_ino) else {
834 reply.error(libc::EIO);
835 return;
836 };
837 for (fino, name, kind) in children {
838 entries.push((fino, kind, String::from_utf8_lossy(&name).into_owned()));
839 }
840 }
841 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
842 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
843 break;
844 }
845 }
846 reply.ok();
847 return;
848 }
849
850 if let Some(fs_dir_ino) = match ino {
852 FUSE_RW_INO => Some(self.root_ino),
853 _ => match decode_fuse_ino(ino) {
854 InodeNamespace::Rw(rw_id) if rw_id < 9_000_000 => Some(rw_id),
855 _ => None,
856 },
857 } {
858 let mut fs = self.fs.borrow_mut();
859 match fs.read_dir(fs_dir_ino) {
860 Ok(entries) => {
861 let session = self.session.borrow();
862 let mut fuse_entries: Vec<(u64, FileType, String)> = Vec::new();
863
864 for e in &entries {
865 let name = e.name_str();
866 let child_ino = e.inode;
867
868 if let Some(s) = session.as_ref() {
870 if name != "." && name != ".." && s.overlay.deleted.contains(&child_ino)
871 {
872 continue;
873 }
874 }
875
876 let fuse_ino = if name == "." || name == ".." {
877 if fs_dir_ino == self.root_ino && name == "." {
878 FUSE_RW_INO
879 } else if fs_dir_ino == self.root_ino && name == ".." {
880 FUSE_ROOT_INO
881 } else {
882 rw_ino(child_ino)
883 }
884 } else {
885 rw_ino(child_ino)
886 };
887 let kind = fs_file_type_to_fuse(e.file_type);
888 fuse_entries.push((fuse_ino, kind, name));
889 }
890
891 if let Some(s) = session.as_ref() {
893 for (id, entry) in &s.overlay.created {
894 if entry.parent_ino == fs_dir_ino {
895 if let Some(counter) =
896 id.strip_prefix("new_").and_then(|s| s.parse::<u64>().ok())
897 {
898 let fuse_ino = rw_ino(counter + 9_000_000);
899 fuse_entries.push((
900 fuse_ino,
901 FileType::RegularFile,
902 entry.name.clone(),
903 ));
904 }
905 }
906 }
907 for (id, entry) in &s.overlay.dirs {
908 if entry.parent_ino == fs_dir_ino {
909 if let Some(counter) =
910 id.strip_prefix("new_").and_then(|s| s.parse::<u64>().ok())
911 {
912 let fuse_ino = rw_ino(counter + 9_000_000);
913 fuse_entries.push((
914 fuse_ino,
915 FileType::Directory,
916 entry.name.clone(),
917 ));
918 }
919 }
920 }
921 }
922
923 for (i, (entry_ino, kind, name)) in fuse_entries.iter().enumerate().skip(offset)
924 {
925 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
926 break;
927 }
928 }
929 reply.ok();
930 }
931 Err(_) => reply.error(libc::EIO),
932 }
933 return;
934 }
935
936 if ino == FUSE_DELETED_INO {
938 self.ensure_deleted_cache();
939 let cache = self.deleted_cache.borrow();
940 let mut entries: Vec<(u64, FileType, String)> = vec![
941 (FUSE_DELETED_INO, FileType::Directory, ".".to_string()),
942 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
943 ];
944 if let Some(cached) = cache.as_ref() {
945 for entry in cached {
946 entries.push((
947 deleted_ino(entry.fs_ino),
948 FileType::RegularFile,
949 entry.name.clone(),
950 ));
951 }
952 }
953 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
954 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
955 break;
956 }
957 }
958 reply.ok();
959 return;
960 }
961
962 if ino == FUSE_JOURNAL_INO {
964 self.ensure_journal_cache();
965 let cache = self.journal_cache.borrow();
966 let mut entries: Vec<(u64, FileType, String)> = vec![
967 (FUSE_JOURNAL_INO, FileType::Directory, ".".to_string()),
968 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
969 ];
970 if let Some(cached) = cache.as_ref() {
971 for entry in cached {
972 entries.push((
973 journal_ino(entry.sequence),
974 FileType::Directory,
975 entry.name.clone(),
976 ));
977 }
978 }
979 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
980 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
981 break;
982 }
983 }
984 reply.ok();
985 return;
986 }
987
988 if let InodeNamespace::Journal(_seq) = decode_fuse_ino(ino) {
990 if offset == 0 {
991 let _ = reply.add(ino, 1, FileType::Directory, ".");
992 let _ = reply.add(FUSE_JOURNAL_INO, 2, FileType::Directory, "..");
993 }
994 reply.ok();
995 return;
996 }
997
998 if ino == FUSE_METADATA_INO {
1000 self.ensure_metadata_cache();
1001 let cache = self.metadata_cache.borrow();
1002 let mut entries: Vec<(u64, FileType, String)> = vec![
1003 (FUSE_METADATA_INO, FileType::Directory, ".".to_string()),
1004 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1005 ];
1006 if cache.is_some() {
1007 entries.push((
1008 metadata_ino(1),
1009 FileType::RegularFile,
1010 "superblock.json".to_string(),
1011 ));
1012 entries.push((
1013 metadata_ino(2),
1014 FileType::RegularFile,
1015 "timeline.jsonl".to_string(),
1016 ));
1017 }
1018 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1019 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1020 break;
1021 }
1022 }
1023 reply.ok();
1024 return;
1025 }
1026
1027 if ino == FUSE_UNALLOCATED_INO {
1029 self.ensure_unallocated_cache();
1030 let cache = self.unallocated_cache.borrow();
1031 let mut entries: Vec<(u64, FileType, String)> = vec![
1032 (FUSE_UNALLOCATED_INO, FileType::Directory, ".".to_string()),
1033 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1034 ];
1035 if let Some(cached) = cache.as_ref() {
1036 for (i, entry) in cached.iter().enumerate() {
1037 entries.push((
1038 unallocated_ino(i as u64),
1039 FileType::RegularFile,
1040 entry.name.clone(),
1041 ));
1042 }
1043 }
1044 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1045 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1046 break;
1047 }
1048 }
1049 reply.ok();
1050 return;
1051 }
1052
1053 if ino == FUSE_SESSION_INO {
1055 let mut entries: Vec<(u64, FileType, String)> = vec![
1056 (FUSE_SESSION_INO, FileType::Directory, ".".to_string()),
1057 (FUSE_ROOT_INO, FileType::Directory, "..".to_string()),
1058 ];
1059 if self.has_session() {
1060 entries.push((
1061 metadata_ino(100),
1062 FileType::RegularFile,
1063 "status.json".to_string(),
1064 ));
1065 }
1066 for (i, (entry_ino, kind, name)) in entries.iter().enumerate().skip(offset) {
1067 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1068 break;
1069 }
1070 }
1071 reply.ok();
1072 return;
1073 }
1074
1075 let fs_dir_ino = match ino {
1077 FUSE_RO_INO => self.root_ino,
1078 _ => {
1079 if let InodeNamespace::Ro(fs_ino) = decode_fuse_ino(ino) {
1080 fs_ino
1081 } else {
1082 reply.error(libc::ENOENT);
1083 return;
1084 }
1085 }
1086 };
1087
1088 let mut fs = self.fs.borrow_mut();
1089 match fs.read_dir(fs_dir_ino) {
1090 Ok(entries) => {
1091 let root_ino = self.root_ino;
1092 let fuse_entries: Vec<(u64, FileType, String)> = entries
1093 .iter()
1094 .map(|e| {
1095 let name = e.name_str();
1096 let fuse_ino = if name == "." || name == ".." {
1097 if fs_dir_ino == root_ino && name == "." {
1098 FUSE_RO_INO
1099 } else if fs_dir_ino == root_ino && name == ".." {
1100 FUSE_ROOT_INO
1101 } else {
1102 ro_ino(e.inode)
1103 }
1104 } else {
1105 ro_ino(e.inode)
1106 };
1107 let kind = fs_file_type_to_fuse(e.file_type);
1108 (fuse_ino, kind, name)
1109 })
1110 .collect();
1111
1112 for (i, (entry_ino, kind, name)) in fuse_entries.iter().enumerate().skip(offset) {
1113 if reply.add(*entry_ino, (i + 1) as i64, *kind, name) {
1114 break;
1115 }
1116 }
1117 reply.ok();
1118 }
1119 Err(_) => reply.error(libc::EIO),
1120 }
1121 }
1122
1123 fn read(
1124 &mut self,
1125 _req: &Request,
1126 ino: u64,
1127 _fh: u64,
1128 offset: i64,
1129 size: u32,
1130 _flags: i32,
1131 _lock_owner: Option<u64>,
1132 reply: ReplyData,
1133 ) {
1134 match decode_fuse_ino(ino) {
1135 InodeNamespace::Deleted(fs_ino) => {
1136 self.ensure_deleted_cache();
1137 let cache = self.deleted_cache.borrow();
1138 if let Some(entries) = cache.as_ref() {
1139 if let Some(entry) = entries.iter().find(|e| e.fs_ino == fs_ino) {
1140 let off = offset as usize;
1141 if off >= entry.data.len() {
1142 reply.data(&[]);
1143 } else {
1144 let end = (off + size as usize).min(entry.data.len());
1145 reply.data(&entry.data[off..end]);
1146 }
1147 return;
1148 }
1149 }
1150 reply.error(libc::ENOENT);
1151 }
1152 InodeNamespace::Metadata(id) => {
1153 self.ensure_metadata_cache();
1154 let data = match id {
1155 1 => {
1156 let cache = self.metadata_cache.borrow();
1157 cache.as_ref().map(|mc| mc.superblock_json.clone())
1158 }
1159 2 => {
1160 let cache = self.metadata_cache.borrow();
1161 cache.as_ref().map(|mc| mc.timeline_jsonl.clone())
1162 }
1163 100 => {
1164 let session = self.session.borrow();
1166 session.as_ref().map(|s| {
1167 let status = serde_json::json!({
1168 "image_path": s.metadata.image_path,
1169 "image_sha256": s.metadata.image_sha256,
1170 "created": s.metadata.created,
1171 });
1172 serde_json::to_string_pretty(&status)
1173 .unwrap_or_default()
1174 .into_bytes()
1175 })
1176 }
1177 _ => None,
1178 };
1179 match data {
1180 Some(buf) => {
1181 let off = offset as usize;
1182 if off >= buf.len() {
1183 reply.data(&[]);
1184 } else {
1185 let end = (off + size as usize).min(buf.len());
1186 reply.data(&buf[off..end]);
1187 }
1188 }
1189 None => reply.error(libc::ENOENT),
1190 }
1191 }
1192 InodeNamespace::Unallocated(range_id) => {
1193 self.ensure_unallocated_cache();
1194 let range_info = {
1195 let cache = self.unallocated_cache.borrow();
1196 cache.as_ref().and_then(|entries| {
1197 entries.get(range_id as usize).map(|e| FsBlockRange {
1198 start: e.start,
1199 length: e.length,
1200 })
1201 })
1202 };
1203 match range_info {
1204 Some(range) => {
1205 let mut fs = self.fs.borrow_mut();
1206 match fs.read_unallocated(&range) {
1207 Ok(data) => {
1208 let off = offset as usize;
1209 if off >= data.len() {
1210 reply.data(&[]);
1211 } else {
1212 let end = (off + size as usize).min(data.len());
1213 reply.data(&data[off..end]);
1214 }
1215 }
1216 Err(_) => reply.error(libc::EIO),
1217 }
1218 }
1219 None => reply.error(libc::ENOENT),
1220 }
1221 }
1222 InodeNamespace::Ro(fs_ino) => {
1223 let mut fs = self.fs.borrow_mut();
1224 match fs.read_file_range(fs_ino, offset as u64, u64::from(size)) {
1225 Ok(data) => reply.data(&data),
1226 Err(_) => reply.error(libc::EIO),
1227 }
1228 }
1229 InodeNamespace::Rw(rw_id) => {
1230 if rw_id >= 9_000_000 {
1232 let counter = rw_id - 9_000_000;
1233 let created_id = Self::created_overlay_id(counter);
1234 let session = self.session.borrow();
1235 if let Some(s) = session.as_ref() {
1236 if s.overlay.created.contains_key(&created_id)
1237 || s.overlay.dirs.contains_key(&created_id)
1238 {
1239 if let Ok(data) = s.read_overlay_file(&created_id) {
1240 let off = offset as usize;
1241 let end = (off + size as usize).min(data.len());
1242 if off >= data.len() {
1243 reply.data(&[]);
1244 } else {
1245 reply.data(&data[off..end]);
1246 }
1247 return;
1248 }
1249 reply.error(libc::EIO);
1250 return;
1251 }
1252 }
1253 reply.error(libc::ENOENT);
1254 return;
1255 }
1256
1257 let fs_ino = rw_id;
1259 let session = self.session.borrow();
1261 if let Some(s) = session.as_ref() {
1262 let overlay_id = Self::modified_overlay_id(fs_ino);
1263 if s.overlay.modified.contains_key(&fs_ino) {
1264 if let Ok(data) = s.read_overlay_file(&overlay_id) {
1265 let off = offset as usize;
1266 let end = (off + size as usize).min(data.len());
1267 if off >= data.len() {
1268 reply.data(&[]);
1269 } else {
1270 reply.data(&data[off..end]);
1271 }
1272 return;
1273 }
1274 reply.error(libc::EIO);
1275 return;
1276 }
1277 }
1278 drop(session);
1279
1280 let mut fs = self.fs.borrow_mut();
1282 match fs.read_file_range(fs_ino, offset as u64, u64::from(size)) {
1283 Ok(data) => reply.data(&data),
1284 Err(_) => reply.error(libc::EIO),
1285 }
1286 }
1287 _ => {
1288 reply.error(libc::ENOENT);
1289 }
1290 }
1291 }
1292
1293 fn readlink(&mut self, _req: &Request, ino: u64, reply: ReplyData) {
1294 let fs_ino = match decode_fuse_ino(ino) {
1295 InodeNamespace::Ro(fs_ino) => fs_ino,
1296 InodeNamespace::Rw(rw_id) if rw_id < 9_000_000 => rw_id,
1297 _ => {
1298 reply.error(libc::ENOENT);
1299 return;
1300 }
1301 };
1302
1303 let mut fs = self.fs.borrow_mut();
1304 match fs.read_link(fs_ino) {
1305 Ok(target) => reply.data(&target),
1306 Err(_) => reply.error(libc::EIO),
1307 }
1308 }
1309
1310 fn write(
1311 &mut self,
1312 _req: &Request,
1313 ino: u64,
1314 _fh: u64,
1315 offset: i64,
1316 data: &[u8],
1317 _write_flags: u32,
1318 _flags: i32,
1319 _lock_owner: Option<u64>,
1320 reply: ReplyWrite,
1321 ) {
1322 if !self.has_session() {
1323 reply.error(libc::EROFS);
1324 return;
1325 }
1326
1327 match decode_fuse_ino(ino) {
1328 InodeNamespace::Rw(rw_id) => {
1329 if rw_id >= 9_000_000 {
1331 let counter = rw_id - 9_000_000;
1332 let created_id = Self::created_overlay_id(counter);
1333 let mut session = self.session.borrow_mut();
1334 let s = session.as_mut().unwrap();
1335
1336 let is_known = s.overlay.created.contains_key(&created_id)
1337 || s.overlay.dirs.contains_key(&created_id);
1338 if !is_known {
1339 reply.error(libc::ENOENT);
1340 return;
1341 }
1342
1343 let mut buf = s.read_overlay_file(&created_id).unwrap_or_default();
1344 let off = offset as usize;
1345 let end = off + data.len();
1346 if end > buf.len() {
1347 buf.resize(end, 0);
1348 }
1349 buf[off..end].copy_from_slice(data);
1350
1351 if s.write_overlay_file(&created_id, &buf).is_err() {
1352 reply.error(libc::EIO);
1353 return;
1354 }
1355
1356 if let Some(entry) = s.overlay.created.get_mut(&created_id) {
1357 entry.size = buf.len() as u64;
1358 }
1359 if let Some(entry) = s.overlay.dirs.get_mut(&created_id) {
1360 entry.size = buf.len() as u64;
1361 }
1362
1363 if s.save().is_err() {
1364 reply.error(libc::EIO);
1365 return;
1366 }
1367
1368 reply.written(data.len() as u32);
1369 return;
1370 }
1371
1372 let fs_ino = rw_id;
1374 let overlay_id = Self::modified_overlay_id(fs_ino);
1375
1376 let mut session = self.session.borrow_mut();
1377 let s = session.as_mut().unwrap();
1378
1379 if !s.overlay.modified.contains_key(&fs_ino) {
1380 let mut fs = self.fs.borrow_mut();
1381 let Ok(original) = fs.read_file(fs_ino) else {
1382 reply.error(libc::EIO);
1383 return;
1384 };
1385 if s.write_overlay_file(&overlay_id, &original).is_err() {
1386 reply.error(libc::EIO);
1387 return;
1388 }
1389 s.overlay.modified.insert(fs_ino, overlay_id.clone());
1390 }
1391
1392 let mut buf = s.read_overlay_file(&overlay_id).unwrap_or_default();
1393 let off = offset as usize;
1394 let end = off + data.len();
1395 if end > buf.len() {
1396 buf.resize(end, 0);
1397 }
1398 buf[off..end].copy_from_slice(data);
1399
1400 if s.write_overlay_file(&overlay_id, &buf).is_err() {
1401 reply.error(libc::EIO);
1402 return;
1403 }
1404
1405 if s.save().is_err() {
1406 reply.error(libc::EIO);
1407 return;
1408 }
1409
1410 reply.written(data.len() as u32);
1411 }
1412 _ => reply.error(libc::EROFS),
1413 }
1414 }
1415
1416 fn create(
1417 &mut self,
1418 _req: &Request,
1419 parent: u64,
1420 name: &OsStr,
1421 _mode: u32,
1422 _umask: u32,
1423 _flags: i32,
1424 reply: ReplyCreate,
1425 ) {
1426 if !self.has_session() {
1427 reply.error(libc::EROFS);
1428 return;
1429 }
1430
1431 let Some(fs_parent) = self.rw_parent_to_fs(parent) else {
1432 reply.error(libc::EROFS);
1433 return;
1434 };
1435
1436 let Some(name_s) = name.to_str() else {
1437 reply.error(libc::EINVAL);
1438 return;
1439 };
1440 let name_str = name_s.to_string();
1441
1442 let counter = self.alloc_overlay_ino();
1443 let created_id = Self::created_overlay_id(counter);
1444 let fuse_ino = rw_ino(counter + 9_000_000);
1445
1446 let mut session = self.session.borrow_mut();
1447 let s = session.as_mut().unwrap();
1448
1449 if s.write_overlay_file(&created_id, &[]).is_err() {
1450 reply.error(libc::EIO);
1451 return;
1452 }
1453
1454 s.overlay.created.insert(
1455 created_id,
1456 crate::session::OverlayEntry {
1457 parent_ino: fs_parent,
1458 name: name_str,
1459 size: 0,
1460 },
1461 );
1462
1463 if s.save().is_err() {
1464 reply.error(libc::EIO);
1465 return;
1466 }
1467
1468 let attr = Self::overlay_created_attr(fuse_ino, 0, false);
1469 reply.created(&TTL, &attr, 0, 0, 0);
1470 }
1471
1472 fn mkdir(
1473 &mut self,
1474 _req: &Request,
1475 parent: u64,
1476 name: &OsStr,
1477 _mode: u32,
1478 _umask: u32,
1479 reply: ReplyEntry,
1480 ) {
1481 if !self.has_session() {
1482 reply.error(libc::EROFS);
1483 return;
1484 }
1485
1486 let Some(fs_parent) = self.rw_parent_to_fs(parent) else {
1487 reply.error(libc::EROFS);
1488 return;
1489 };
1490
1491 let Some(name_s) = name.to_str() else {
1492 reply.error(libc::EINVAL);
1493 return;
1494 };
1495 let name_str = name_s.to_string();
1496
1497 let counter = self.alloc_overlay_ino();
1498 let created_id = Self::created_overlay_id(counter);
1499 let fuse_ino = rw_ino(counter + 9_000_000);
1500
1501 let mut session = self.session.borrow_mut();
1502 let s = session.as_mut().unwrap();
1503
1504 s.overlay.dirs.insert(
1505 created_id,
1506 crate::session::OverlayEntry {
1507 parent_ino: fs_parent,
1508 name: name_str,
1509 size: 0,
1510 },
1511 );
1512
1513 if s.save().is_err() {
1514 reply.error(libc::EIO);
1515 return;
1516 }
1517
1518 let attr = Self::overlay_created_attr(fuse_ino, 0, true);
1519 reply.entry(&TTL, &attr, 0);
1520 }
1521
1522 fn unlink(&mut self, _req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) {
1523 if !self.has_session() {
1524 reply.error(libc::EROFS);
1525 return;
1526 }
1527
1528 let Some(fs_parent) = self.rw_parent_to_fs(parent) else {
1529 reply.error(libc::EROFS);
1530 return;
1531 };
1532
1533 let name_bytes = name.as_encoded_bytes();
1534
1535 if let Some((id, _counter, _is_dir)) = self.find_created_by_name(fs_parent, name_bytes) {
1537 let mut session = self.session.borrow_mut();
1538 let s = session.as_mut().unwrap();
1539 s.overlay.created.remove(&id);
1540 s.overlay.dirs.remove(&id);
1541 let _ = std::fs::remove_file(s.overlay_file_path(&id));
1542 if s.save().is_err() {
1543 reply.error(libc::EIO);
1544 return;
1545 }
1546 reply.ok();
1547 return;
1548 }
1549
1550 let mut fs = self.fs.borrow_mut();
1552 match fs.lookup(fs_parent, name_bytes) {
1553 Ok(Some(child_ino)) => {
1554 let mut session = self.session.borrow_mut();
1555 let s = session.as_mut().unwrap();
1556 if !s.overlay.deleted.contains(&child_ino) {
1557 s.overlay.deleted.push(child_ino);
1558 }
1559 if s.save().is_err() {
1560 reply.error(libc::EIO);
1561 return;
1562 }
1563 reply.ok();
1564 }
1565 Ok(None) => reply.error(libc::ENOENT),
1566 Err(_) => reply.error(libc::EIO),
1567 }
1568 }
1569
1570 fn rmdir(&mut self, req: &Request, parent: u64, name: &OsStr, reply: ReplyEmpty) {
1571 self.unlink(req, parent, name, reply);
1572 }
1573
1574 #[allow(clippy::too_many_arguments)]
1575 fn setattr(
1576 &mut self,
1577 _req: &Request,
1578 ino: u64,
1579 _mode: Option<u32>,
1580 _uid: Option<u32>,
1581 _gid: Option<u32>,
1582 size: Option<u64>,
1583 _atime: Option<TimeOrNow>,
1584 _mtime: Option<TimeOrNow>,
1585 _ctime: Option<SystemTime>,
1586 _fh: Option<u64>,
1587 _crtime: Option<SystemTime>,
1588 _chgtime: Option<SystemTime>,
1589 _bkuptime: Option<SystemTime>,
1590 _flags: Option<u32>,
1591 reply: ReplyAttr,
1592 ) {
1593 match decode_fuse_ino(ino) {
1594 InodeNamespace::Rw(rw_id) => {
1595 if !self.has_session() {
1596 reply.error(libc::EROFS);
1597 return;
1598 }
1599
1600 if let Some(new_size) = size {
1602 if rw_id >= 9_000_000 {
1604 let counter = rw_id - 9_000_000;
1605 let created_id = Self::created_overlay_id(counter);
1606 let mut session = self.session.borrow_mut();
1607 let s = session.as_mut().unwrap();
1608
1609 let mut buf = s.read_overlay_file(&created_id).unwrap_or_default();
1610 buf.resize(new_size as usize, 0);
1611 if s.write_overlay_file(&created_id, &buf).is_err() {
1612 reply.error(libc::EIO);
1613 return;
1614 }
1615
1616 if let Some(entry) = s.overlay.created.get_mut(&created_id) {
1617 entry.size = new_size;
1618 }
1619 if let Some(entry) = s.overlay.dirs.get_mut(&created_id) {
1620 entry.size = new_size;
1621 }
1622 if s.save().is_err() {
1623 reply.error(libc::EIO);
1624 return;
1625 }
1626
1627 let attr = Self::overlay_created_attr(ino, new_size, false);
1628 reply.attr(&TTL, &attr);
1629 return;
1630 }
1631
1632 let fs_ino = rw_id;
1634 let overlay_id = Self::modified_overlay_id(fs_ino);
1635
1636 let mut session = self.session.borrow_mut();
1637 let s = session.as_mut().unwrap();
1638
1639 if !s.overlay.modified.contains_key(&fs_ino) {
1640 let mut fs = self.fs.borrow_mut();
1641 let Ok(original) = fs.read_file(fs_ino) else {
1642 reply.error(libc::EIO);
1643 return;
1644 };
1645 if s.write_overlay_file(&overlay_id, &original).is_err() {
1646 reply.error(libc::EIO);
1647 return;
1648 }
1649 s.overlay.modified.insert(fs_ino, overlay_id.clone());
1650 }
1651
1652 let mut buf = s.read_overlay_file(&overlay_id).unwrap_or_default();
1653 buf.resize(new_size as usize, 0);
1654 if s.write_overlay_file(&overlay_id, &buf).is_err() {
1655 reply.error(libc::EIO);
1656 return;
1657 }
1658 if s.save().is_err() {
1659 reply.error(libc::EIO);
1660 return;
1661 }
1662
1663 let mut fs = self.fs.borrow_mut();
1665 match fs.metadata(fs_ino) {
1666 Ok(meta) => {
1667 let mut attr = fs_to_attr(ino, &meta);
1668 attr.size = new_size;
1669 attr.blocks = new_size.div_ceil(512);
1670 reply.attr(&TTL, &attr);
1671 }
1672 Err(_) => reply.error(libc::EIO),
1673 }
1674 return;
1675 }
1676
1677 if rw_id >= 9_000_000 {
1679 let counter = rw_id - 9_000_000;
1680 let created_id = Self::created_overlay_id(counter);
1681 let session = self.session.borrow();
1682 if let Some(s) = session.as_ref() {
1683 if let Some(entry) = s.overlay.created.get(&created_id) {
1684 let attr = Self::overlay_created_attr(ino, entry.size, false);
1685 reply.attr(&TTL, &attr);
1686 return;
1687 }
1688 if let Some(entry) = s.overlay.dirs.get(&created_id) {
1689 let attr = Self::overlay_created_attr(ino, entry.size, true);
1690 reply.attr(&TTL, &attr);
1691 return;
1692 }
1693 }
1694 reply.error(libc::ENOENT);
1695 } else {
1696 let fs_ino = rw_id;
1697 let mut fs = self.fs.borrow_mut();
1698 match fs.metadata(fs_ino) {
1699 Ok(meta) => {
1700 let mut attr = fs_to_attr(ino, &meta);
1701 let session = self.session.borrow();
1702 if let Some(s) = session.as_ref() {
1703 let overlay_id = Self::modified_overlay_id(fs_ino);
1704 if s.overlay.modified.contains_key(&fs_ino) {
1705 if let Ok(data) = s.read_overlay_file(&overlay_id) {
1706 attr.size = data.len() as u64;
1707 attr.blocks = attr.size.div_ceil(512);
1708 }
1709 }
1710 }
1711 reply.attr(&TTL, &attr);
1712 }
1713 Err(_) => reply.error(libc::EIO),
1714 }
1715 }
1716 }
1717 _ => reply.error(libc::EROFS),
1719 }
1720 }
1721}
1722
1723#[cfg(test)]
1724mod tests {
1725 use super::*;
1726 use crate::{FsDeletedInode, FsDirEntry, FsError, FsRecoveryResult, FsResult, FsTimelineEvent};
1727 use fuser::FileType;
1728 use std::time::{Duration, UNIX_EPOCH};
1729
1730 #[test]
1735 fn virtual_dir_attr_is_directory() {
1736 let attr = virtual_dir_attr(1);
1737 assert_eq!(attr.ino, 1);
1738 assert_eq!(attr.kind, FileType::Directory);
1739 assert_eq!(attr.perm, 0o555);
1740 assert_eq!(attr.nlink, 2);
1741 assert_eq!(attr.size, 0);
1742 assert_eq!(attr.blocks, 0);
1743 assert_eq!(attr.uid, 0);
1744 assert_eq!(attr.gid, 0);
1745 assert_eq!(attr.blksize, 4096);
1746 assert_eq!(attr.atime, UNIX_EPOCH);
1747 assert_eq!(attr.mtime, UNIX_EPOCH);
1748 assert_eq!(attr.ctime, UNIX_EPOCH);
1749 assert_eq!(attr.crtime, UNIX_EPOCH);
1750 }
1751
1752 #[test]
1753 fn virtual_dir_attr_preserves_ino() {
1754 for ino in [1, 42, FUSE_ROOT_INO, FUSE_DELETED_INO, 999_999] {
1755 assert_eq!(virtual_dir_attr(ino).ino, ino);
1756 }
1757 }
1758
1759 #[test]
1764 fn virtual_file_attr_regular() {
1765 let attr = virtual_file_attr(100, 4096);
1766 assert_eq!(attr.ino, 100);
1767 assert_eq!(attr.kind, FileType::RegularFile);
1768 assert_eq!(attr.perm, 0o444);
1769 assert_eq!(attr.nlink, 1);
1770 assert_eq!(attr.size, 4096);
1771 assert_eq!(attr.blocks, 8); }
1773
1774 #[test]
1775 fn virtual_file_attr_zero_size() {
1776 let attr = virtual_file_attr(1, 0);
1777 assert_eq!(attr.size, 0);
1778 assert_eq!(attr.blocks, 0);
1779 }
1780
1781 #[test]
1782 fn virtual_file_attr_non_512_aligned() {
1783 let attr = virtual_file_attr(1, 1000);
1785 assert_eq!(attr.blocks, 2);
1786 }
1787
1788 #[test]
1793 fn timestamp_conversion_positive() {
1794 let ts = FsTimestamp {
1795 seconds: 1_700_000_000,
1796 nanoseconds: 500_000_000,
1797 };
1798 let st = ts_to_systime(&ts);
1799 let dur = st.duration_since(UNIX_EPOCH).unwrap();
1800 assert_eq!(dur.as_secs(), 1_700_000_000);
1801 assert_eq!(dur.subsec_nanos(), 500_000_000);
1802 }
1803
1804 #[test]
1805 fn timestamp_zero() {
1806 let ts = FsTimestamp {
1807 seconds: 0,
1808 nanoseconds: 0,
1809 };
1810 let st = ts_to_systime(&ts);
1811 assert_eq!(st, UNIX_EPOCH);
1812 }
1813
1814 #[test]
1815 fn timestamp_negative_clamps_to_epoch() {
1816 let ts = FsTimestamp {
1817 seconds: -1,
1818 nanoseconds: 0,
1819 };
1820 let st = ts_to_systime(&ts);
1821 assert_eq!(st, UNIX_EPOCH);
1822 }
1823
1824 #[test]
1825 fn timestamp_negative_large_clamps_to_epoch() {
1826 let ts = FsTimestamp {
1827 seconds: -1_000_000,
1828 nanoseconds: 999_999_999,
1829 };
1830 let st = ts_to_systime(&ts);
1831 assert_eq!(st, UNIX_EPOCH);
1832 }
1833
1834 #[test]
1835 fn timestamp_epoch_plus_one_second() {
1836 let ts = FsTimestamp {
1837 seconds: 1,
1838 nanoseconds: 0,
1839 };
1840 let st = ts_to_systime(&ts);
1841 assert_eq!(st, UNIX_EPOCH + Duration::from_secs(1));
1842 }
1843
1844 #[test]
1849 fn file_type_mapping_regular() {
1850 assert_eq!(
1851 fs_file_type_to_fuse(FsFileType::RegularFile),
1852 FileType::RegularFile
1853 );
1854 }
1855
1856 #[test]
1857 fn file_type_mapping_directory() {
1858 assert_eq!(
1859 fs_file_type_to_fuse(FsFileType::Directory),
1860 FileType::Directory
1861 );
1862 }
1863
1864 #[test]
1865 fn file_type_mapping_symlink() {
1866 assert_eq!(fs_file_type_to_fuse(FsFileType::Symlink), FileType::Symlink);
1867 }
1868
1869 #[test]
1870 fn file_type_mapping_chardev() {
1871 assert_eq!(
1872 fs_file_type_to_fuse(FsFileType::CharDevice),
1873 FileType::CharDevice
1874 );
1875 }
1876
1877 #[test]
1878 fn file_type_mapping_blockdev() {
1879 assert_eq!(
1880 fs_file_type_to_fuse(FsFileType::BlockDevice),
1881 FileType::BlockDevice
1882 );
1883 }
1884
1885 #[test]
1886 fn file_type_mapping_fifo() {
1887 assert_eq!(fs_file_type_to_fuse(FsFileType::Fifo), FileType::NamedPipe);
1888 }
1889
1890 #[test]
1891 fn file_type_mapping_socket() {
1892 assert_eq!(fs_file_type_to_fuse(FsFileType::Socket), FileType::Socket);
1893 }
1894
1895 #[test]
1896 fn file_type_mapping_unknown() {
1897 assert_eq!(
1898 fs_file_type_to_fuse(FsFileType::Unknown),
1899 FileType::RegularFile
1900 );
1901 }
1902
1903 #[test]
1908 fn fs_to_attr_regular_file() {
1909 let meta = FsMetadata {
1910 ino: 42,
1911 file_type: FsFileType::RegularFile,
1912 mode: 0o100_644,
1913 uid: 1000,
1914 gid: 1000,
1915 size: 100,
1916 links_count: 1,
1917 atime: FsTimestamp {
1918 seconds: 1_700_000_000,
1919 nanoseconds: 0,
1920 },
1921 mtime: FsTimestamp {
1922 seconds: 1_700_000_000,
1923 nanoseconds: 0,
1924 },
1925 ctime: FsTimestamp {
1926 seconds: 1_700_000_000,
1927 nanoseconds: 0,
1928 },
1929 crtime: FsTimestamp {
1930 seconds: 1_700_000_000,
1931 nanoseconds: 0,
1932 },
1933 allocated: true,
1934 };
1935 let attr = fs_to_attr(1012, &meta);
1936 assert_eq!(attr.ino, 1012);
1937 assert_eq!(attr.size, 100);
1938 assert_eq!(attr.kind, FileType::RegularFile);
1939 assert_eq!(attr.nlink, 1);
1940 assert_eq!(attr.perm, 0o644);
1941 assert_eq!(attr.uid, 1000);
1942 assert_eq!(attr.gid, 1000);
1943 }
1944
1945 #[test]
1946 fn fs_to_attr_directory() {
1947 let meta = FsMetadata {
1948 ino: 2,
1949 file_type: FsFileType::Directory,
1950 mode: 0o40755,
1951 uid: 0,
1952 gid: 0,
1953 size: 4096,
1954 links_count: 3,
1955 atime: FsTimestamp::default(),
1956 mtime: FsTimestamp::default(),
1957 ctime: FsTimestamp::default(),
1958 crtime: FsTimestamp::default(),
1959 allocated: true,
1960 };
1961 let attr = fs_to_attr(2000, &meta);
1962 assert_eq!(attr.kind, FileType::Directory);
1963 assert_eq!(attr.nlink, 3);
1964 assert_eq!(attr.perm, 0o755);
1965 }
1966
1967 #[test]
1968 fn fs_to_attr_symlink() {
1969 let meta = FsMetadata {
1970 ino: 10,
1971 file_type: FsFileType::Symlink,
1972 mode: 0o120_777,
1973 uid: 0,
1974 gid: 0,
1975 size: 11,
1976 links_count: 1,
1977 atime: FsTimestamp::default(),
1978 mtime: FsTimestamp::default(),
1979 ctime: FsTimestamp::default(),
1980 crtime: FsTimestamp::default(),
1981 allocated: true,
1982 };
1983 let attr = fs_to_attr(3000, &meta);
1984 assert_eq!(attr.kind, FileType::Symlink);
1985 assert_eq!(attr.perm, 0o777);
1986 }
1987
1988 #[test]
1989 fn fs_to_attr_blocks_calculation() {
1990 let meta = FsMetadata {
1991 ino: 42,
1992 file_type: FsFileType::RegularFile,
1993 mode: 0o100_644,
1994 uid: 0,
1995 gid: 0,
1996 size: 1000,
1997 links_count: 1,
1998 atime: FsTimestamp::default(),
1999 mtime: FsTimestamp::default(),
2000 ctime: FsTimestamp::default(),
2001 crtime: FsTimestamp::default(),
2002 allocated: true,
2003 };
2004 let attr = fs_to_attr(42, &meta);
2005 assert_eq!(attr.size, 1000);
2006 assert_eq!(attr.blocks, 2);
2007 }
2008
2009 #[test]
2010 fn fs_to_attr_blksize_always_4096() {
2011 let meta = FsMetadata {
2012 ino: 1,
2013 file_type: FsFileType::RegularFile,
2014 mode: 0o100_644,
2015 uid: 0,
2016 gid: 0,
2017 size: 0,
2018 links_count: 1,
2019 atime: FsTimestamp::default(),
2020 mtime: FsTimestamp::default(),
2021 ctime: FsTimestamp::default(),
2022 crtime: FsTimestamp::default(),
2023 allocated: true,
2024 };
2025 let attr = fs_to_attr(1, &meta);
2026 assert_eq!(attr.blksize, 4096);
2027 }
2028
2029 #[test]
2034 fn overlay_created_attr_regular_file() {
2035 let attr = ForensicFuseFs::overlay_created_attr(999, 512, false);
2036 assert_eq!(attr.ino, 999);
2037 assert_eq!(attr.size, 512);
2038 assert_eq!(attr.kind, FileType::RegularFile);
2039 assert_eq!(attr.perm, 0o644);
2040 assert_eq!(attr.nlink, 1);
2041 assert_eq!(attr.blocks, 1);
2042 }
2043
2044 #[test]
2045 fn overlay_created_attr_directory() {
2046 let attr = ForensicFuseFs::overlay_created_attr(888, 0, true);
2047 assert_eq!(attr.kind, FileType::Directory);
2048 assert_eq!(attr.perm, 0o755);
2049 }
2050
2051 #[test]
2056 fn modified_overlay_id_format() {
2057 assert_eq!(ForensicFuseFs::modified_overlay_id(42), "ino_42");
2058 assert_eq!(ForensicFuseFs::modified_overlay_id(0), "ino_0");
2059 assert_eq!(
2060 ForensicFuseFs::modified_overlay_id(9_999_999),
2061 "ino_9999999"
2062 );
2063 }
2064
2065 #[test]
2066 fn created_overlay_id_format() {
2067 assert_eq!(ForensicFuseFs::created_overlay_id(1), "new_1");
2068 assert_eq!(ForensicFuseFs::created_overlay_id(0), "new_0");
2069 }
2070
2071 fn root_child_names(layout: crate::MountLayout) -> Vec<String> {
2076 let mut fs = MockForensicFs;
2077 let root = fs.root_ino();
2078 root_children(layout, &mut fs, root)
2079 .unwrap()
2080 .iter()
2081 .map(|(_, n, _)| String::from_utf8_lossy(n).to_string())
2082 .collect()
2083 }
2084
2085 #[test]
2086 fn root_children_raw_lists_fs_tree_without_overlay() {
2087 let names = root_child_names(crate::MountLayout::Raw);
2088 assert!(names.contains(&"hello.txt".to_string()), "got {names:?}");
2089 assert!(names.contains(&"subdir".to_string()), "got {names:?}");
2090 assert!(
2091 !names
2092 .iter()
2093 .any(|n| n == "rw" || n == "deleted" || n == "ro"),
2094 "Raw root must have no overlay dirs: {names:?}"
2095 );
2096 }
2097
2098 #[test]
2099 fn root_children_diskoverlay_lists_virtual_dirs() {
2100 let names = root_child_names(crate::MountLayout::DiskOverlay);
2101 for d in [
2102 "ro",
2103 "rw",
2104 "deleted",
2105 "journal",
2106 "metadata",
2107 "unallocated",
2108 "session",
2109 ] {
2110 assert!(names.contains(&d.to_string()), "missing {d}: {names:?}");
2111 }
2112 }
2113
2114 #[test]
2119 fn virtual_dirs_has_expected_entries() {
2120 assert_eq!(VIRTUAL_DIRS.len(), 7);
2121 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "ro"));
2122 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "rw"));
2123 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "deleted"));
2124 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "journal"));
2125 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "metadata"));
2126 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "unallocated"));
2127 assert!(VIRTUAL_DIRS.iter().any(|(_, name)| *name == "session"));
2128 }
2129
2130 #[test]
2131 fn virtual_dirs_ino_matches_constants() {
2132 for &(ino, name) in VIRTUAL_DIRS {
2133 match name {
2134 "ro" => assert_eq!(ino, FUSE_RO_INO),
2135 "rw" => assert_eq!(ino, FUSE_RW_INO),
2136 "deleted" => assert_eq!(ino, FUSE_DELETED_INO),
2137 "journal" => assert_eq!(ino, FUSE_JOURNAL_INO),
2138 "metadata" => assert_eq!(ino, FUSE_METADATA_INO),
2139 "unallocated" => assert_eq!(ino, FUSE_UNALLOCATED_INO),
2140 "session" => assert_eq!(ino, FUSE_SESSION_INO),
2141 _ => panic!("unexpected virtual dir: {name}"),
2142 }
2143 }
2144 }
2145
2146 struct MockForensicFs;
2151
2152 impl crate::ForensicFs for MockForensicFs {
2153 fn root_ino(&self) -> u64 {
2154 2
2155 }
2156
2157 fn read_dir(&mut self, ino: u64) -> FsResult<Vec<FsDirEntry>> {
2158 match ino {
2159 2 => Ok(vec![
2160 FsDirEntry {
2161 inode: 2,
2162 name: b".".to_vec(),
2163 file_type: FsFileType::Directory,
2164 },
2165 FsDirEntry {
2166 inode: 2,
2167 name: b"..".to_vec(),
2168 file_type: FsFileType::Directory,
2169 },
2170 FsDirEntry {
2171 inode: 10,
2172 name: b"hello.txt".to_vec(),
2173 file_type: FsFileType::RegularFile,
2174 },
2175 FsDirEntry {
2176 inode: 11,
2177 name: b"subdir".to_vec(),
2178 file_type: FsFileType::Directory,
2179 },
2180 ]),
2181 11 => Ok(vec![
2182 FsDirEntry {
2183 inode: 11,
2184 name: b".".to_vec(),
2185 file_type: FsFileType::Directory,
2186 },
2187 FsDirEntry {
2188 inode: 2,
2189 name: b"..".to_vec(),
2190 file_type: FsFileType::Directory,
2191 },
2192 FsDirEntry {
2193 inode: 12,
2194 name: b"nested.txt".to_vec(),
2195 file_type: FsFileType::RegularFile,
2196 },
2197 ]),
2198 _ => Err(FsError::NotFound(format!("inode {ino}"))),
2199 }
2200 }
2201
2202 fn lookup(&mut self, parent_ino: u64, name: &[u8]) -> FsResult<Option<u64>> {
2203 let entries = self.read_dir(parent_ino)?;
2204 Ok(entries.iter().find(|e| e.name == name).map(|e| e.inode))
2205 }
2206
2207 fn metadata(&mut self, ino: u64) -> FsResult<FsMetadata> {
2208 let (file_type, size) = match ino {
2209 2 | 11 => (FsFileType::Directory, 4096),
2210 10 => (FsFileType::RegularFile, 12),
2211 12 => (FsFileType::RegularFile, 11),
2212 _ => return Err(FsError::NotFound(format!("inode {ino}"))),
2213 };
2214 Ok(FsMetadata {
2215 ino,
2216 file_type,
2217 mode: if file_type == FsFileType::Directory {
2218 0o40755
2219 } else {
2220 0o100_644
2221 },
2222 uid: 1000,
2223 gid: 1000,
2224 size: size as u64,
2225 links_count: if file_type == FsFileType::Directory {
2226 2
2227 } else {
2228 1
2229 },
2230 atime: FsTimestamp {
2231 seconds: 1_700_000_000,
2232 nanoseconds: 0,
2233 },
2234 mtime: FsTimestamp {
2235 seconds: 1_700_000_000,
2236 nanoseconds: 0,
2237 },
2238 ctime: FsTimestamp {
2239 seconds: 1_700_000_000,
2240 nanoseconds: 0,
2241 },
2242 crtime: FsTimestamp {
2243 seconds: 1_699_000_000,
2244 nanoseconds: 0,
2245 },
2246 allocated: true,
2247 })
2248 }
2249
2250 fn read_file(&mut self, ino: u64) -> FsResult<Vec<u8>> {
2251 match ino {
2252 10 => Ok(b"Hello, mock!".to_vec()),
2253 12 => Ok(b"Nested file".to_vec()),
2254 _ => Err(FsError::NotFound(format!("inode {ino}"))),
2255 }
2256 }
2257
2258 fn read_file_range(&mut self, ino: u64, offset: u64, len: u64) -> FsResult<Vec<u8>> {
2259 let data = self.read_file(ino)?;
2260 let start = (offset as usize).min(data.len());
2261 let end = (start + len as usize).min(data.len());
2262 Ok(data[start..end].to_vec())
2263 }
2264
2265 fn read_link(&mut self, _ino: u64) -> FsResult<Vec<u8>> {
2266 Err(FsError::NotFound("no symlinks in mock".to_string()))
2267 }
2268
2269 fn deleted_inodes(&mut self) -> FsResult<Vec<FsDeletedInode>> {
2270 Ok(vec![FsDeletedInode {
2271 ino: 99,
2272 file_type: FsFileType::RegularFile,
2273 size: 100,
2274 dtime: 1_700_001_000,
2275 recoverability: 0.75,
2276 }])
2277 }
2278
2279 fn recover_file(&mut self, ino: u64) -> FsResult<FsRecoveryResult> {
2280 match ino {
2281 99 => Ok(FsRecoveryResult {
2282 ino: 99,
2283 data: vec![0xDE; 100],
2284 expected_size: 100,
2285 recovered_bytes: 100,
2286 recovery_percentage: 1.0,
2287 }),
2288 _ => Err(FsError::NotFound(format!("inode {ino}"))),
2289 }
2290 }
2291
2292 fn timeline(&mut self) -> FsResult<Vec<FsTimelineEvent>> {
2293 Ok(vec![FsTimelineEvent {
2294 timestamp: FsTimestamp {
2295 seconds: 1_700_000_000,
2296 nanoseconds: 0,
2297 },
2298 event_type: FsEventType::Created,
2299 inode: 10,
2300 size: 12,
2301 uid: 1000,
2302 gid: 1000,
2303 }])
2304 }
2305
2306 fn fs_info(&self) -> FsResult<serde_json::Value> {
2307 Ok(serde_json::json!({ "filesystem": "mock", "block_size": 4096 }))
2308 }
2309
2310 fn block_size(&self) -> u64 {
2311 4096
2312 }
2313 }
2314
2315 fn make_mock_fuse() -> ForensicFuseFs {
2316 ForensicFuseFs::new(
2317 Box::new(MockForensicFs),
2318 None,
2319 crate::MountLayout::DiskOverlay,
2320 )
2321 }
2322
2323 #[test]
2324 fn mock_ensure_deleted_cache() {
2325 let fuse = make_mock_fuse();
2326 assert!(fuse.deleted_cache.borrow().is_none());
2327 fuse.ensure_deleted_cache();
2328 let cache = fuse.deleted_cache.borrow();
2329 let entries = cache.as_ref().expect("cache should be populated");
2330 assert_eq!(entries.len(), 1);
2331 assert_eq!(entries[0].fs_ino, 99);
2332 assert_eq!(entries[0].name, "99_unknown");
2333 assert_eq!(entries[0].data.len(), 100);
2334 }
2335
2336 #[test]
2337 fn mock_ensure_metadata_cache() {
2338 let fuse = make_mock_fuse();
2339 assert!(fuse.metadata_cache.borrow().is_none());
2340 fuse.ensure_metadata_cache();
2341 let cache = fuse.metadata_cache.borrow();
2342 let mc = cache.as_ref().expect("cache should be populated");
2343 let sb_str = String::from_utf8_lossy(&mc.superblock_json);
2344 assert!(
2345 sb_str.contains("mock"),
2346 "superblock_json should contain 'mock': {sb_str}"
2347 );
2348 assert!(
2349 !mc.timeline_jsonl.is_empty(),
2350 "timeline_jsonl should not be empty"
2351 );
2352 }
2353
2354 #[test]
2355 fn mock_root_ino_stored() {
2356 let fuse = make_mock_fuse();
2357 assert_eq!(fuse.root_ino, 2);
2358 }
2359
2360 #[test]
2361 fn mock_has_session_false() {
2362 let fuse = make_mock_fuse();
2363 assert!(!fuse.has_session());
2364 }
2365
2366 #[test]
2367 fn mock_read_file_through_fs() {
2368 let fuse = make_mock_fuse();
2369 let mut fs = fuse.fs.borrow_mut();
2370 let data = fs.read_file(10).expect("read_file(10) should succeed");
2371 assert_eq!(data, b"Hello, mock!");
2372 }
2373
2374 #[test]
2375 fn mock_read_file_range_through_fs() {
2376 let fuse = make_mock_fuse();
2377 let mut fs = fuse.fs.borrow_mut();
2378 let data = fs
2379 .read_file_range(10, 0, 5)
2380 .expect("read_file_range should succeed");
2381 assert_eq!(data, b"Hello");
2382 }
2383
2384 #[test]
2385 fn mock_lookup_through_fs() {
2386 let fuse = make_mock_fuse();
2387 let mut fs = fuse.fs.borrow_mut();
2388 let result = fs.lookup(2, b"hello.txt").expect("lookup should succeed");
2389 assert_eq!(result, Some(10));
2390 }
2391
2392 #[test]
2393 fn mock_metadata_through_fs() {
2394 let fuse = make_mock_fuse();
2395 let mut fs = fuse.fs.borrow_mut();
2396 let meta = fs.metadata(10).expect("metadata(10) should succeed");
2397 assert_eq!(meta.file_type, FsFileType::RegularFile);
2398 assert_eq!(meta.size, 12);
2399 assert_eq!(meta.ino, 10);
2400 }
2401
2402 #[test]
2403 fn mock_fs_to_attr() {
2404 let fuse = make_mock_fuse();
2405 let meta = {
2406 let mut fs = fuse.fs.borrow_mut();
2407 fs.metadata(10).expect("metadata(10) should succeed")
2408 };
2409 let attr = fs_to_attr(ro_ino(10), &meta);
2410 assert_eq!(attr.ino, ro_ino(10));
2411 assert_eq!(attr.kind, FileType::RegularFile);
2412 assert_eq!(attr.size, 12);
2413 assert_eq!(attr.perm, 0o644);
2414 assert_eq!(attr.uid, 1000);
2415 assert_eq!(attr.gid, 1000);
2416 assert_eq!(attr.nlink, 1);
2417 assert_eq!(attr.blksize, 4096);
2418 let expected_atime = UNIX_EPOCH + Duration::from_secs(1_700_000_000);
2419 assert_eq!(attr.atime, expected_atime);
2420 let expected_crtime = UNIX_EPOCH + Duration::from_secs(1_699_000_000);
2421 assert_eq!(attr.crtime, expected_crtime);
2422 }
2423
2424 #[test]
2425 fn mock_timeline_through_fs() {
2426 let fuse = make_mock_fuse();
2427 let mut fs = fuse.fs.borrow_mut();
2428 let events = fs.timeline().expect("timeline should succeed");
2429 assert_eq!(events.len(), 1);
2430 assert_eq!(events[0].event_type, FsEventType::Created);
2431 assert_eq!(events[0].inode, 10);
2432 assert_eq!(events[0].size, 12);
2433 }
2434
2435 #[test]
2436 fn mock_ensure_journal_cache_empty() {
2437 let fuse = make_mock_fuse();
2438 assert!(fuse.journal_cache.borrow().is_none());
2439 fuse.ensure_journal_cache();
2440 let cache = fuse.journal_cache.borrow();
2441 let entries = cache.as_ref().expect("cache should be populated");
2442 assert!(
2443 entries.is_empty(),
2444 "mock has no journal_transactions override, should be empty"
2445 );
2446 }
2447}