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