Skip to main content

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