Skip to main content

forensic_mount/
inode_map.rs

1#![forbid(unsafe_code)]
2
3/// FUSE inode number constants for virtual directories.
4pub const FUSE_ROOT_INO: u64 = 1;
5pub const FUSE_RO_INO: u64 = 2;
6pub const FUSE_RW_INO: u64 = 3;
7pub const FUSE_DELETED_INO: u64 = 4;
8pub const FUSE_JOURNAL_INO: u64 = 5;
9pub const FUSE_METADATA_INO: u64 = 6;
10pub const FUSE_UNALLOCATED_INO: u64 = 7;
11pub const FUSE_SESSION_INO: u64 = 8;
12pub const FUSE_EVIDENCE_INO: u64 = 9;
13
14/// Offset added to real ext4 inodes when exposing them under ro/.
15const RO_INODE_OFFSET: u64 = 1_000;
16/// Offset for rw/ overlay inodes.
17const RW_INODE_OFFSET: u64 = 10_000_000;
18/// Offset for deleted/ virtual file inodes.
19const DELETED_INODE_OFFSET: u64 = 20_000_000;
20/// Offset for metadata/ virtual file inodes.
21const METADATA_INODE_OFFSET: u64 = 30_000_000;
22/// Offset for journal/ virtual file inodes.
23const JOURNAL_INODE_OFFSET: u64 = 40_000_000;
24/// Offset for evidence/ filtered view inodes.
25const EVIDENCE_INODE_OFFSET: u64 = 50_000_000;
26/// Offset for unallocated/ virtual file inodes.
27const UNALLOCATED_INODE_OFFSET: u64 = 60_000_000;
28
29/// Which virtual namespace a FUSE inode belongs to.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum InodeNamespace {
32    /// Virtual root or top-level directory
33    Virtual(u64),
34    /// Real ext4 inode under ro/
35    Ro(u64),
36    /// Overlay inode under rw/
37    Rw(u64),
38    /// Deleted file virtual inode
39    Deleted(u64),
40    /// Metadata virtual file
41    Metadata(u64),
42    /// Journal virtual file
43    Journal(u64),
44    /// Evidence filtered view inode
45    Evidence(u64),
46    /// Unallocated block range
47    Unallocated(u64),
48}
49
50/// Convert a FUSE inode number to its namespace and real inode.
51pub fn decode_fuse_ino(ino: u64) -> InodeNamespace {
52    if ino <= FUSE_EVIDENCE_INO {
53        InodeNamespace::Virtual(ino)
54    } else if ino >= UNALLOCATED_INODE_OFFSET {
55        InodeNamespace::Unallocated(ino - UNALLOCATED_INODE_OFFSET)
56    } else if ino >= EVIDENCE_INODE_OFFSET {
57        InodeNamespace::Evidence(ino - EVIDENCE_INODE_OFFSET)
58    } else if ino >= JOURNAL_INODE_OFFSET {
59        InodeNamespace::Journal(ino - JOURNAL_INODE_OFFSET)
60    } else if ino >= METADATA_INODE_OFFSET {
61        InodeNamespace::Metadata(ino - METADATA_INODE_OFFSET)
62    } else if ino >= DELETED_INODE_OFFSET {
63        InodeNamespace::Deleted(ino - DELETED_INODE_OFFSET)
64    } else if ino >= RW_INODE_OFFSET {
65        InodeNamespace::Rw(ino - RW_INODE_OFFSET)
66    } else {
67        InodeNamespace::Ro(ino - RO_INODE_OFFSET)
68    }
69}
70
71/// Encode a real ext4 inode for the ro/ namespace.
72pub fn ro_ino(ext4_ino: u64) -> u64 {
73    ext4_ino.saturating_add(RO_INODE_OFFSET)
74}
75
76/// Encode an overlay inode for the rw/ namespace.
77pub fn rw_ino(ext4_ino: u64) -> u64 {
78    ext4_ino.saturating_add(RW_INODE_OFFSET)
79}
80
81/// Encode a deleted inode for the deleted/ namespace.
82pub fn deleted_ino(ext4_ino: u64) -> u64 {
83    ext4_ino.saturating_add(DELETED_INODE_OFFSET)
84}
85
86/// Encode a metadata virtual inode.
87pub fn metadata_ino(id: u64) -> u64 {
88    id.saturating_add(METADATA_INODE_OFFSET)
89}
90
91/// Encode a journal virtual inode.
92pub fn journal_ino(seq: u64) -> u64 {
93    seq.saturating_add(JOURNAL_INODE_OFFSET)
94}
95
96/// Encode an evidence filtered view inode.
97pub fn evidence_ino(ext4_ino: u64) -> u64 {
98    ext4_ino.saturating_add(EVIDENCE_INODE_OFFSET)
99}
100
101/// Encode an unallocated range virtual inode.
102pub fn unallocated_ino(id: u64) -> u64 {
103    id.saturating_add(UNALLOCATED_INODE_OFFSET)
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn roundtrip_ro_inode() {
112        let ext4_ino = 42;
113        let fuse = ro_ino(ext4_ino);
114        assert_eq!(decode_fuse_ino(fuse), InodeNamespace::Ro(ext4_ino));
115    }
116
117    #[test]
118    fn roundtrip_rw_inode() {
119        let ext4_ino = 42;
120        let fuse = rw_ino(ext4_ino);
121        assert_eq!(decode_fuse_ino(fuse), InodeNamespace::Rw(ext4_ino));
122    }
123
124    #[test]
125    fn roundtrip_deleted_inode() {
126        let ext4_ino = 21;
127        let fuse = deleted_ino(ext4_ino);
128        assert_eq!(decode_fuse_ino(fuse), InodeNamespace::Deleted(ext4_ino));
129    }
130
131    #[test]
132    fn virtual_root() {
133        assert_eq!(decode_fuse_ino(FUSE_ROOT_INO), InodeNamespace::Virtual(1));
134    }
135
136    #[test]
137    fn virtual_dirs() {
138        assert_eq!(decode_fuse_ino(FUSE_RO_INO), InodeNamespace::Virtual(2));
139        assert_eq!(decode_fuse_ino(FUSE_RW_INO), InodeNamespace::Virtual(3));
140        assert_eq!(
141            decode_fuse_ino(FUSE_DELETED_INO),
142            InodeNamespace::Virtual(4)
143        );
144    }
145
146    #[test]
147    fn roundtrip_metadata_inode() {
148        let id = 1;
149        let fuse = metadata_ino(id);
150        assert_eq!(decode_fuse_ino(fuse), InodeNamespace::Metadata(id));
151    }
152
153    #[test]
154    fn roundtrip_journal_inode() {
155        let seq = 42;
156        let fuse = journal_ino(seq);
157        assert_eq!(decode_fuse_ino(fuse), InodeNamespace::Journal(seq));
158    }
159
160    #[test]
161    fn roundtrip_unallocated_inode() {
162        let id = 7;
163        let fuse = unallocated_ino(id);
164        assert_eq!(decode_fuse_ino(fuse), InodeNamespace::Unallocated(id));
165    }
166
167    #[test]
168    fn namespaces_do_not_overlap() {
169        // Verify that max realistic ext4 inode in ro/ doesn't collide with rw/
170        let max_ro = ro_ino(9_000_000);
171        let min_rw = rw_ino(0);
172        assert!(max_ro < min_rw);
173    }
174}