Skip to main content

ext4fs/forensic/
slack.rs

1#![forbid(unsafe_code)]
2
3use crate::error::Result;
4use crate::inode::InodeReader;
5use crate::ondisk::FileType;
6use std::io::{Read, Seek};
7
8/// Slack space data from the end of a file's last allocated block.
9#[derive(Debug, Clone)]
10pub struct SlackSpace {
11    pub ino: u64,
12    pub file_size: u64,
13    /// Physical block number containing the slack.
14    pub block: u64,
15    /// Byte offset within the block where slack begins.
16    pub slack_offset: usize,
17    /// The slack bytes (from slack_offset to end of block).
18    pub data: Vec<u8>,
19}
20
21/// Read slack space for a single file inode.
22pub fn read_slack_space<R: Read + Seek>(
23    reader: &mut InodeReader<R>,
24    ino: u64,
25) -> Result<Option<SlackSpace>> {
26    let inode = reader.read_inode(ino)?;
27
28    // Only regular files have meaningful slack
29    if inode.file_type() != FileType::RegularFile {
30        return Ok(None);
31    }
32    if inode.size == 0 {
33        return Ok(None);
34    }
35
36    let block_size = u64::from(reader.block_reader().superblock().block_size);
37
38    // If file size is block-aligned, no slack
39    if inode.size % block_size == 0 {
40        return Ok(None);
41    }
42
43    // Get block mappings
44    let mappings = match reader.inode_block_map(ino) {
45        Ok(m) => m,
46        Err(_) => return Ok(None),
47    };
48
49    if mappings.is_empty() {
50        return Ok(None);
51    }
52
53    // Find the last mapped block
54    let last_mapping = &mappings[mappings.len() - 1];
55    let last_phys_block = last_mapping.physical_block + last_mapping.length - 1;
56
57    // Read the full last block
58    let block_data = reader.block_reader_mut().read_block(last_phys_block)?;
59
60    let slack_offset = (inode.size % block_size) as usize;
61    let slack_data = block_data[slack_offset..].to_vec();
62
63    Ok(Some(SlackSpace {
64        ino,
65        file_size: inode.size,
66        block: last_phys_block,
67        slack_offset,
68        data: slack_data,
69    }))
70}
71
72/// Scan all allocated regular file inodes for slack space.
73pub fn scan_all_slack<R: Read + Seek>(reader: &mut InodeReader<R>) -> Result<Vec<SlackSpace>> {
74    let all_inodes = reader.iter_all_inodes()?;
75    let mut results = Vec::new();
76
77    for (ino, inode) in &all_inodes {
78        if inode.file_type() != FileType::RegularFile || inode.size == 0 {
79            continue;
80        }
81        if let Ok(Some(slack)) = read_slack_space(reader, *ino) {
82            results.push(slack);
83        }
84    }
85
86    Ok(results)
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::block::BlockReader;
93    use std::io::Cursor;
94
95    fn open_minimal() -> Option<InodeReader<Cursor<Vec<u8>>>> {
96        let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/minimal.img");
97        let data = std::fs::read(path).ok()?;
98        let br = BlockReader::open(Cursor::new(data)).ok()?;
99        Some(InodeReader::new(br))
100    }
101
102    fn open_forensic() -> Option<InodeReader<Cursor<Vec<u8>>>> {
103        let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
104        let data = std::fs::read(path).ok()?;
105        let br = BlockReader::open(Cursor::new(data)).ok()?;
106        Some(InodeReader::new(br))
107    }
108
109    #[test]
110    fn slack_space_for_small_file() {
111        let mut reader = if let Some(r) = open_minimal() {
112            r
113        } else {
114            eprintln!("skip");
115            return;
116        };
117        // Find hello.txt's inode by looking for a 12-byte regular file
118        let all = reader.iter_all_inodes().unwrap();
119        let hello_ino = all
120            .iter()
121            .find(|(_, inode)| {
122                inode.file_type() == crate::ondisk::FileType::RegularFile && inode.size == 12
123            })
124            .map(|(ino, _)| *ino)
125            .expect("minimal.img should have a 12-byte regular file (hello.txt)");
126
127        let slack = read_slack_space(&mut reader, hello_ino).unwrap();
128        assert!(slack.is_some(), "small file should have slack");
129        let s = slack.unwrap();
130        assert_eq!(s.ino, hello_ino);
131        assert_eq!(s.file_size, 12);
132        let block_size = reader.block_reader().superblock().block_size as usize;
133        assert_eq!(s.data.len(), block_size - 12);
134        assert_eq!(s.slack_offset, 12);
135    }
136
137    #[test]
138    fn no_slack_for_zero_size_file() {
139        let mut reader = if let Some(r) = open_minimal() {
140            r
141        } else {
142            eprintln!("skip");
143            return;
144        };
145        // Inode 0 is invalid, should handle gracefully
146        let result = read_slack_space(&mut reader, 0);
147        assert!(result.is_err() || result.unwrap().is_none());
148    }
149
150    #[test]
151    fn scan_all_slack_finds_entries() {
152        let mut reader = if let Some(r) = open_forensic() {
153            r
154        } else {
155            eprintln!("skip");
156            return;
157        };
158        let slacks = scan_all_slack(&mut reader).unwrap();
159        assert!(
160            !slacks.is_empty(),
161            "forensic.img should have files with slack"
162        );
163        for s in &slacks {
164            assert!(!s.data.is_empty());
165            assert!(s.slack_offset > 0);
166            let block_size = reader.block_reader().superblock().block_size as usize;
167            assert_eq!(s.data.len() + s.slack_offset, block_size);
168        }
169    }
170}