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