Skip to main content

ext4fs/forensic/
history.rs

1#![forbid(unsafe_code)]
2
3use crate::error::Result;
4use crate::forensic::journal::Journal;
5use crate::inode::InodeReader;
6use crate::ondisk::Inode;
7use std::io::{Read, Seek};
8
9/// A previous version of an inode recovered from the journal.
10#[derive(Debug, Clone)]
11pub struct InodeVersion {
12    /// Journal transaction sequence number.
13    pub sequence: u64,
14    /// Commit timestamp (seconds since epoch).
15    pub commit_time: u64,
16    /// The inode state at this transaction.
17    pub inode: Inode,
18}
19
20/// Reconstruct the history of an inode from journal transactions.
21pub fn inode_history<R: Read + Seek>(
22    reader: &mut InodeReader<R>,
23    journal: &Journal,
24    ino: u64,
25) -> Result<Vec<InodeVersion>> {
26    let (ipg, inode_size_u64, inode_size_u16, block_size, journal_ino) = {
27        let sb = reader.block_reader().superblock();
28        (
29            u64::from(sb.inodes_per_group),
30            u64::from(sb.inode_size),
31            sb.inode_size,
32            u64::from(sb.block_size),
33            u64::from(sb.journal_inum),
34        )
35    };
36
37    let group = ((ino - 1) / ipg) as u32;
38    let index = (ino - 1) % ipg;
39
40    let inode_table = match reader.block_reader().inode_table_block(group) {
41        Ok(t) => t,
42        Err(_) => return Ok(Vec::new()),
43    };
44
45    let inode_offset_in_table = index * inode_size_u64;
46    let target_block = inode_table + inode_offset_in_table / block_size;
47    let offset_in_block = (inode_offset_in_table % block_size) as usize;
48
49    let journal_data = reader.read_inode_data(journal_ino)?;
50    let j_block_size = journal.block_size as usize;
51
52    let mut versions = Vec::new();
53    for txn in &journal.transactions {
54        for mapping in &txn.mappings {
55            if mapping.filesystem_block == target_block {
56                let j_offset = mapping.journal_block as usize * j_block_size;
57                if j_offset + j_block_size <= journal_data.len() {
58                    let block_data = &journal_data[j_offset..j_offset + j_block_size];
59                    let end = offset_in_block + inode_size_u64 as usize;
60                    if end <= block_data.len() {
61                        if let Ok(inode) =
62                            Inode::parse(&block_data[offset_in_block..end], inode_size_u16)
63                        {
64                            if inode.mode != 0 {
65                                versions.push(InodeVersion {
66                                    sequence: u64::from(txn.sequence),
67                                    commit_time: txn.commit_seconds as u64,
68                                    inode,
69                                });
70                            }
71                        }
72                    }
73                }
74            }
75        }
76    }
77
78    versions.sort_by_key(|v| v.sequence);
79    Ok(versions)
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::block::BlockReader;
86    use crate::forensic::journal::parse_journal;
87    use std::io::Cursor;
88
89    fn open_forensic() -> Option<InodeReader<Cursor<Vec<u8>>>> {
90        let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
91        let data = std::fs::read(path).ok()?;
92        let br = BlockReader::open(Cursor::new(data)).ok()?;
93        Some(InodeReader::new(br))
94    }
95
96    #[test]
97    fn inode_history_for_hello_txt() {
98        let mut reader = if let Some(r) = open_forensic() {
99            r
100        } else {
101            eprintln!("skip");
102            return;
103        };
104        let journal = parse_journal(&mut reader).unwrap();
105        let versions = inode_history(&mut reader, &journal, 12).unwrap();
106        // hello.txt was created and synced — should have at least one version
107        eprintln!("hello.txt versions: {}", versions.len());
108        for v in &versions {
109            assert!(v.sequence > 0);
110            assert!(v.commit_time > 0);
111            assert!(v.inode.mode != 0);
112        }
113    }
114
115    #[test]
116    fn inode_history_for_deleted_file() {
117        let mut reader = if let Some(r) = open_forensic() {
118            r
119        } else {
120            eprintln!("skip");
121            return;
122        };
123        let journal = parse_journal(&mut reader).unwrap();
124        // Inode 21 was created then deleted
125        let versions = inode_history(&mut reader, &journal, 21).unwrap();
126        eprintln!("deleted file (ino 21) versions: {}", versions.len());
127        // Should not error even if no versions found
128    }
129
130    #[test]
131    fn inode_history_nonexistent() {
132        let mut reader = if let Some(r) = open_forensic() {
133            r
134        } else {
135            eprintln!("skip");
136            return;
137        };
138        let journal = parse_journal(&mut reader).unwrap();
139        let versions = inode_history(&mut reader, &journal, 999_999).unwrap();
140        assert!(versions.is_empty());
141    }
142
143    #[test]
144    fn inode_history_sorted_by_sequence() {
145        let mut reader = if let Some(r) = open_forensic() {
146            r
147        } else {
148            eprintln!("skip");
149            return;
150        };
151        let journal = parse_journal(&mut reader).unwrap();
152        let versions = inode_history(&mut reader, &journal, 12).unwrap();
153        for w in versions.windows(2) {
154            assert!(w[0].sequence <= w[1].sequence);
155        }
156    }
157}