ext4_fs/
extfs.rs

1use crate::{
2    error::Ext4Error,
3    reader::FileReader,
4    structs::{
5        Descriptor, Directory, Ext4Hash, Extents, FileInfo, HashValue, Inode, InodeType, Stat,
6    },
7    superblock::block::{IncompatFlags, SuperBlock},
8};
9use log::error;
10use md5::{Digest, Md5};
11use sha1::Sha1;
12use sha2::Sha256;
13use std::{
14    collections::HashMap,
15    io::{BufReader, Read, copy},
16};
17
18/// Create a reader that can parse the ext4 filesystem
19pub struct Ext4Reader<T: std::io::Seek + std::io::Read> {
20    pub fs: BufReader<T>,
21    /// Default is probably 4096
22    pub blocksize: u16,
23    /// Will be 0. Unless you are reading a disk image file like QCOW or VMDK
24    pub offset_start: u64,
25    pub(crate) descriptors: Option<Vec<Descriptor>>,
26    pub(crate) incompat_flags: Vec<IncompatFlags>,
27    pub(crate) blocks_per_group: u32,
28    pub(crate) fs_size: u64,
29    pub(crate) number_blocks: u32,
30    pub(crate) inode_size: u16,
31    pub(crate) inodes_per_group: u32,
32    pub(crate) cache_names: HashMap<u64, String>,
33}
34
35pub trait Ext4ReaderAction<'ext4, 'reader, T: std::io::Seek + std::io::Read> {
36    /// Return file info about the root directory. Can be used to start a file listing
37    fn root(&mut self) -> Result<FileInfo, Ext4Error>;
38    /// Read a directory based on provided inode value
39    fn read_dir(&mut self, inode: u32) -> Result<FileInfo, Ext4Error>;
40    /// Return the `SuperBlock` information for the ext4 filesystem
41    fn superblock(&mut self) -> Result<SuperBlock, Ext4Error>;
42    /// Return descriptors for the ext4 filesystem
43    fn descriptors(&mut self) -> Result<Vec<Descriptor>, Ext4Error>;
44    /// Return extents for a provide inode
45    fn extents(&mut self, inode: u32) -> Result<Option<Extents>, Ext4Error>;
46    /// Stat a file
47    fn stat(&mut self, inode: u32) -> Result<Stat, Ext4Error>;
48    /// Hash a file. MD5, SHA1, SHA256 are supported
49    fn hash(&mut self, inode: u32, hash: &Ext4Hash) -> Result<HashValue, Ext4Error>;
50    /// Create a reader to stream a file from the ext4 filesystem.
51    fn reader(&'reader mut self, inode: u32) -> Result<FileReader<'reader, T>, Ext4Error>;
52    /// Read the contents of a file into memory. **WARNING** this will read the entire file regardless of size into memory!
53    fn read(&mut self, inode: u32) -> Result<Vec<u8>, Ext4Error>;
54    /// Return verbose inode information for the provided inode
55    fn inode_verbose(&mut self, inode: u32) -> Result<Inode, Ext4Error>;
56}
57
58impl<T: std::io::Seek + std::io::Read> Ext4Reader<T> {
59    /// Initialize an ext4 filesystem reader. This reader will automatically set the correct blocksize if you do not know it
60    pub fn new(
61        fs: BufReader<T>,
62        blocksize: u16,
63        offset_start: u64,
64    ) -> Result<Ext4Reader<T>, Ext4Error> {
65        let mut reader = Ext4Reader {
66            fs,
67            blocksize,
68            offset_start,
69            descriptors: None,
70            incompat_flags: Vec::new(),
71            blocks_per_group: 0,
72            fs_size: 0,
73            number_blocks: 0,
74            inode_size: 0,
75            inodes_per_group: 0,
76            cache_names: HashMap::new(),
77        };
78
79        let block = SuperBlock::read_superblock(&mut reader.fs, reader.offset_start)?;
80        let size = 1024;
81        let base: u16 = 2;
82        reader.blocksize = size * base.pow(block.block_size);
83        reader.incompat_flags = block.incompatible_features_flags.clone();
84        reader.blocks_per_group = block.number_blocks_per_block_group;
85        reader.fs_size = block.number_blocks as u64 * blocksize as u64;
86        reader.number_blocks = block.number_blocks;
87        reader.inode_size = block.inode_size;
88        reader.inodes_per_group = block.number_inodes_per_block_group;
89        reader.descriptors = Some(Descriptor::read_descriptor(&mut reader)?);
90        Ok(reader)
91    }
92}
93
94impl<'ext4, 'reader, T: std::io::Seek + std::io::Read> Ext4ReaderAction<'ext4, 'reader, T>
95    for Ext4Reader<T>
96{
97    fn root(&mut self) -> Result<FileInfo, Ext4Error> {
98        let root_inode = 2;
99        self.read_dir(root_inode)
100    }
101
102    fn read_dir(&mut self, inode: u32) -> Result<FileInfo, Ext4Error> {
103        let inode_value = Inode::read_inode_table(self, inode)?;
104
105        if let Some(extent) = &inode_value.extents {
106            let dirs = Directory::read_directory_data(self, extent)?;
107            let mut info = FileInfo::new(inode_value, dirs, inode as u64);
108            if let Some(name) = self.cache_names.get(&info.inode) {
109                info.name = name.clone();
110            }
111            update_cache(&mut self.cache_names, &info);
112            return Ok(info);
113        }
114        error!("[ext4-fs] No extent data found. Cannot read directory");
115        Err(Ext4Error::Directory)
116    }
117
118    fn superblock(&mut self) -> Result<SuperBlock, Ext4Error> {
119        SuperBlock::read_superblock(&mut self.fs, self.offset_start)
120    }
121
122    fn stat(&mut self, inode: u32) -> Result<Stat, Ext4Error> {
123        let inode_value = Inode::read_inode_table(self, inode)?;
124        Ok(Stat::new(inode_value, inode as u64))
125    }
126
127    fn hash(&mut self, inode: u32, hashes: &Ext4Hash) -> Result<HashValue, Ext4Error> {
128        if !hashes.md5 && !hashes.sha1 && !hashes.sha256 {
129            return Ok(HashValue {
130                md5: String::new(),
131                sha1: String::new(),
132                sha256: String::new(),
133            });
134        }
135        let inode_value = Inode::read_inode_table(self, inode)?;
136        if inode_value.inode_type != InodeType::File {
137            return Err(Ext4Error::NotAFile);
138        }
139        let mut md5 = Md5::new();
140        let mut sha1 = Sha1::new();
141        let mut sha256 = Sha256::new();
142
143        let mut file_reader = self.reader(inode)?;
144        // Keep track of how many bytes we read
145        let mut bytes_read = 0;
146        // Keep track of our cumulative buffer size when reading in chunks
147        let mut buf_size = 0;
148        // Read file in small chunks
149        let mut temp_buf_size = 65536;
150        loop {
151            let mut temp_buf = vec![0u8; temp_buf_size];
152            let bytes = match file_reader.read(&mut temp_buf) {
153                Ok(result) => result,
154                Err(err) => {
155                    error!("[ext4-fs] Failed to read bytes for inode {inode}: {err:?}");
156                    return Err(Ext4Error::FailedToRead);
157                }
158            };
159
160            // If our reader returns 0 bytes. Then something went wrong
161            if bytes == 0 {
162                break;
163            }
164
165            bytes_read += bytes;
166            if bytes_read > inode_value.size as usize {
167                temp_buf_size = bytes_read - inode_value.size as usize;
168            }
169
170            // Make sure our temp buff does not have any extra zeros from the initialization
171            if bytes < temp_buf_size {
172                temp_buf = temp_buf[0..bytes].to_vec();
173            } else if bytes > inode_value.size as usize {
174                // Also check for opposite
175                // Small files maybe allocated more block bytes than needed
176                // Ex: A file less than 4k in size
177                temp_buf = temp_buf[0..inode_value.size as usize].to_vec();
178            }
179
180            // We may have read too many bytes at the end of the file
181            // If we have, adjust our buffer a little
182            if bytes_read > inode_value.size as usize && inode_value.size as usize > buf_size {
183                temp_buf = temp_buf[0..(inode_value.size as usize - buf_size)].to_vec();
184            }
185            buf_size += temp_buf.len();
186
187            if hashes.md5 {
188                let _ = copy(&mut temp_buf.as_slice(), &mut md5);
189            }
190            if hashes.sha1 {
191                let _ = copy(&mut temp_buf.as_slice(), &mut sha1);
192            }
193            if hashes.sha256 {
194                let _ = copy(&mut temp_buf.as_slice(), &mut sha256);
195            }
196
197            // Once we have read enough bytes, we are done
198            if bytes_read >= inode_value.size as usize {
199                break;
200            }
201        }
202
203        let mut hash_value = HashValue {
204            md5: String::new(),
205            sha1: String::new(),
206            sha256: String::new(),
207        };
208
209        if hashes.md5 {
210            let hash = md5.finalize();
211            hash_value.md5 = format!("{hash:x}");
212        }
213        if hashes.sha1 {
214            let hash = sha1.finalize();
215            hash_value.sha1 = format!("{hash:x}");
216        }
217        if hashes.sha256 {
218            let hash = sha256.finalize();
219            hash_value.sha256 = format!("{hash:x}");
220        }
221
222        Ok(hash_value)
223    }
224
225    fn read(&mut self, inode: u32) -> Result<Vec<u8>, Ext4Error> {
226        let inode_value = Inode::read_inode_table(self, inode)?;
227        if inode_value.inode_type != InodeType::File {
228            return Err(Ext4Error::NotAFile);
229        }
230        let mut file_reader = self.reader(inode)?;
231        let mut buf = vec![0; inode_value.size as usize];
232        if let Err(err) = file_reader.read(&mut buf) {
233            error!("[ext4-fs] Could not read file: {err:?}");
234            return Err(Ext4Error::ReadFile);
235        }
236
237        Ok(buf)
238    }
239
240    fn reader(&'reader mut self, inode: u32) -> Result<FileReader<'reader, T>, Ext4Error> {
241        let inode_value = Inode::read_inode_table(self, inode)?;
242        if inode_value.inode_type != InodeType::File {
243            return Err(Ext4Error::NotAFile);
244        }
245        if let Some(extent) = inode_value.extents {
246            return Ok(Ext4Reader::file_reader(self, &extent, inode_value.size));
247        }
248        error!("[ext4-fs] No extent data found. Cannot read directory");
249        Err(Ext4Error::Directory)
250    }
251
252    fn descriptors(&mut self) -> Result<Vec<Descriptor>, Ext4Error> {
253        Descriptor::read_descriptor(self)
254    }
255
256    fn extents(&mut self, inode: u32) -> Result<Option<Extents>, Ext4Error> {
257        let inode_value = Inode::read_inode_table(self, inode)?;
258        Ok(inode_value.extents)
259    }
260
261    fn inode_verbose(&mut self, inode: u32) -> Result<Inode, Ext4Error> {
262        Inode::read_inode_table(self, inode)
263    }
264}
265
266fn update_cache(cache: &mut HashMap<u64, String>, info: &FileInfo) {
267    for entry in &info.children {
268        if entry.inode as u64 == info.inode {
269            continue;
270        }
271        cache.insert(entry.inode as u64, entry.name.clone());
272    }
273}
274
275#[cfg(test)]
276mod tests {
277    use crate::{
278        extfs::{Ext4Reader, Ext4ReaderAction},
279        structs::{Ext4Hash, FileInfo, FileType},
280    };
281    use std::{collections::HashMap, fs::File, io::BufReader, path::PathBuf};
282
283    fn walk_dir<T: std::io::Seek + std::io::Read>(
284        info: &FileInfo,
285        reader: &mut Ext4Reader<T>,
286        cache: &mut HashMap<u64, String>,
287    ) {
288        for entry in &info.children {
289            if entry.file_type == FileType::Directory
290                && entry.name != "."
291                && entry.name != ".."
292                && entry.inode != 2
293            {
294                let info = reader.read_dir(entry.inode).unwrap();
295                cache_paths(cache, &info);
296                walk_dir(&info, reader, cache);
297                continue;
298            }
299            if entry.file_type == FileType::Directory {
300                continue;
301            }
302        }
303    }
304
305    fn cache_paths(cache: &mut HashMap<u64, String>, info: &FileInfo) {
306        for entry in &info.children {
307            if entry.file_type != FileType::Directory || entry.name == "." || entry.name == ".." {
308                continue;
309            }
310            if cache.contains_key(&(entry.inode as u64))
311                && entry.inode != 2
312                && entry.name != "."
313                && entry.name != ".."
314            {
315                continue;
316            }
317
318            let path = cache.get(&(info.inode as u64)).unwrap();
319
320            cache.insert(
321                entry.inode as u64,
322                format!("{}/{}", path, entry.name.clone()),
323            );
324        }
325    }
326
327    #[test]
328    fn test_read_ext4_root() {
329        let mut test_location = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
330        test_location.push("tests/images/test.img");
331        let reader = File::open(test_location.to_str().unwrap()).unwrap();
332        let buf = BufReader::new(reader);
333        let mut ext4_reader = Ext4Reader::new(buf, 4096, 0).unwrap();
334        let dir = ext4_reader.root().unwrap();
335
336        assert_eq!(dir.created, 1759689014000000000);
337        assert_eq!(dir.changed, 1759713496631583423);
338        assert_eq!(dir.children.len(), 6);
339    }
340
341    #[test]
342    fn test_read_ext4_dir() {
343        let mut test_location = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
344        test_location.push("tests/images/test.img");
345        let reader = File::open(test_location.to_str().unwrap()).unwrap();
346        let buf = BufReader::new(reader);
347        let mut ext4_reader = Ext4Reader::new(buf, 4096, 0).unwrap();
348        ext4_reader.root().unwrap();
349        let dir = ext4_reader.read_dir(7634).unwrap();
350
351        assert_eq!(dir.created, 1759689167899447083);
352        assert_eq!(dir.changed, 1759689170863467296);
353        assert_eq!(dir.children.len(), 10);
354        assert_eq!(dir.parent_inode, 2);
355    }
356
357    #[test]
358    fn test_read_ext4_index_dir() {
359        let mut test_location = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
360        test_location.push("tests/images/test.img");
361        let reader = File::open(test_location.to_str().unwrap()).unwrap();
362        let buf = BufReader::new(reader);
363        let mut ext4_reader = Ext4Reader::new(buf, 4096, 0).unwrap();
364        ext4_reader.root().unwrap();
365        let dir = ext4_reader.read_dir(7633).unwrap();
366
367        assert_eq!(dir.created, 1759689153355347892);
368        assert_eq!(dir.changed, 1759689156340368251);
369        assert_eq!(dir.children.len(), 165);
370        assert_eq!(dir.parent_inode, 2);
371    }
372
373    #[test]
374    fn test_walk_dir() {
375        let mut test_location = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
376        test_location.push("tests/images/test.img");
377        let reader = File::open(test_location.to_str().unwrap()).unwrap();
378        let buf = BufReader::new(reader);
379        let mut ext4_reader = Ext4Reader::new(buf, 4096, 0).unwrap();
380        let root = ext4_reader.root().unwrap();
381        let mut cache = HashMap::new();
382        cache.insert(2, String::from(""));
383        cache_paths(&mut cache, &root);
384        walk_dir(&root, &mut ext4_reader, &mut cache);
385        assert_eq!(cache.len(), 10);
386    }
387
388    #[test]
389    fn test_stat() {
390        let mut test_location = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
391        test_location.push("tests/images/test.img");
392        let reader = File::open(test_location.to_str().unwrap()).unwrap();
393        let buf = BufReader::new(reader);
394        let mut ext4_reader = Ext4Reader::new(buf, 4096, 0).unwrap();
395        let root = ext4_reader.root().unwrap();
396        let mut cache = HashMap::new();
397        cache.insert(2, String::from(""));
398        cache_paths(&mut cache, &root);
399        walk_dir(&root, &mut ext4_reader, &mut cache);
400
401        let info = ext4_reader.stat(16).unwrap();
402        assert_eq!(info.created, 1759689156064366369);
403        assert_eq!(info.changed, 1759689156065366375);
404        assert_eq!(info.accessed, 1759689156064366369);
405        assert_eq!(info.modified, 1676375355000000000);
406        assert_eq!(
407            info.extended_attributes.get("security.selinux").unwrap(),
408            "unconfined_u:object_r:unlabeled_t:s0"
409        );
410    }
411
412    #[test]
413    fn test_hash_large_file() {
414        let mut test_location = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
415        test_location.push("tests/images/test.img");
416        let reader = File::open(test_location.to_str().unwrap()).unwrap();
417        let buf = BufReader::new(reader);
418        let mut ext4_reader = Ext4Reader::new(buf, 4096, 0).unwrap();
419        let hashes = Ext4Hash {
420            md5: true,
421            sha1: true,
422            sha256: true,
423        };
424        let info = ext4_reader.hash(676, &hashes).unwrap();
425        assert_eq!(info.md5, "df8e85bd10b33ac804b7c46073768dc9");
426        assert_eq!(info.sha1, "beb51c72d95518720c76e69fd2ad5f7a57e01d6b");
427        assert_eq!(
428            info.sha256,
429            "703df175cdcbbe0163f4ed7c83819070630b8bffdf65dc5739caef062a9c7a73"
430        );
431    }
432
433    #[test]
434    fn test_read_large_file() {
435        let mut test_location = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
436        test_location.push("tests/images/test.img");
437        let reader = File::open(test_location.to_str().unwrap()).unwrap();
438        let buf = BufReader::new(reader);
439        let mut ext4_reader = Ext4Reader::new(buf, 4096, 0).unwrap();
440        let info = ext4_reader.read(676).unwrap();
441        assert_eq!(info.len(), 274310864);
442    }
443
444    #[test]
445    fn test_descriptors() {
446        let mut test_location = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
447        test_location.push("tests/images/test.img");
448        let reader = File::open(test_location.to_str().unwrap()).unwrap();
449        let buf = BufReader::new(reader);
450        let mut ext4_reader = Ext4Reader::new(buf, 4096, 0).unwrap();
451        let info = ext4_reader.descriptors().unwrap();
452        assert_eq!(info.len(), 7);
453    }
454
455    #[test]
456    fn test_extents() {
457        let mut test_location = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
458        test_location.push("tests/images/test.img");
459        let reader = File::open(test_location.to_str().unwrap()).unwrap();
460        let buf = BufReader::new(reader);
461        let mut ext4_reader = Ext4Reader::new(buf, 4096, 0).unwrap();
462        let info = ext4_reader.extents(676).unwrap().unwrap();
463        assert_eq!(info.extent_descriptors.len(), 3);
464    }
465}