ext4fs-core 0.1.0

Forensic-grade ext4 filesystem parser
Documentation
#![forbid(unsafe_code)]

use crate::error::{Ext4Error, Result};
use crate::ondisk::superblock::EXT4_CRC32C;
use bitflags::bitflags;
use crc::Crc;

// ---------------------------------------------------------------------------
// Helper functions for little-endian reads
// ---------------------------------------------------------------------------

fn le16(buf: &[u8], off: usize) -> u16 {
    u16::from_le_bytes([buf[off], buf[off + 1]])
}

fn le32(buf: &[u8], off: usize) -> u32 {
    u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
}

// ---------------------------------------------------------------------------
// Group descriptor flags
// ---------------------------------------------------------------------------

bitflags! {
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub struct GroupDescFlags: u16 {
        const INODE_UNINIT  = 0x0001;
        const BLOCK_UNINIT  = 0x0002;
        const INODE_ZEROED  = 0x0004;
    }
}

// ---------------------------------------------------------------------------
// GroupDescriptor — normalized 32-bit + optional 64-bit hi fields
// ---------------------------------------------------------------------------

#[derive(Debug, Clone)]
pub struct GroupDescriptor {
    pub block_bitmap: u64,
    pub inode_bitmap: u64,
    pub inode_table: u64,
    pub free_blocks_count: u32,
    pub free_inodes_count: u32,
    pub used_dirs_count: u32,
    pub flags: GroupDescFlags,
    pub itable_unused: u32,
    pub checksum: u16,
}

impl GroupDescriptor {
    pub fn parse(buf: &[u8], desc_size: u16) -> Result<Self> {
        let min_len = desc_size as usize;
        if buf.len() < min_len {
            return Err(Ext4Error::TooShort {
                structure: "GroupDescriptor",
                expected: min_len,
                found: buf.len(),
            });
        }

        // Low 32-bit halves (always present for desc_size >= 32)
        let block_bitmap_lo = u64::from(le32(buf, 0x00));
        let inode_bitmap_lo = u64::from(le32(buf, 0x04));
        let inode_table_lo = u64::from(le32(buf, 0x08));
        let free_blocks_lo = u32::from(le16(buf, 0x0C));
        let free_inodes_lo = u32::from(le16(buf, 0x0E));
        let used_dirs_lo = u32::from(le16(buf, 0x10));
        let flags_raw = le16(buf, 0x12);
        let itable_unused_lo = u32::from(le16(buf, 0x1C));
        let checksum = le16(buf, 0x1E);

        // High 32-bit halves (present only for desc_size >= 64)
        let (
            block_bitmap,
            inode_bitmap,
            inode_table,
            free_blocks_count,
            free_inodes_count,
            used_dirs_count,
            itable_unused,
        ) = if desc_size >= 64 {
            let bb_hi = u64::from(le32(buf, 0x20)) << 32;
            let ib_hi = u64::from(le32(buf, 0x24)) << 32;
            let it_hi = u64::from(le32(buf, 0x28)) << 32;
            let fb_hi = u32::from(le16(buf, 0x2C)) << 16;
            let fi_hi = u32::from(le16(buf, 0x2E)) << 16;
            let ud_hi = u32::from(le16(buf, 0x30)) << 16;
            let iu_hi = u32::from(le16(buf, 0x32)) << 16;
            (
                bb_hi | block_bitmap_lo,
                ib_hi | inode_bitmap_lo,
                it_hi | inode_table_lo,
                fb_hi | free_blocks_lo,
                fi_hi | free_inodes_lo,
                ud_hi | used_dirs_lo,
                iu_hi | itable_unused_lo,
            )
        } else {
            (
                block_bitmap_lo,
                inode_bitmap_lo,
                inode_table_lo,
                free_blocks_lo,
                free_inodes_lo,
                used_dirs_lo,
                itable_unused_lo,
            )
        };

        let flags = GroupDescFlags::from_bits_truncate(flags_raw);

        Ok(Self {
            block_bitmap,
            inode_bitmap,
            inode_table,
            free_blocks_count,
            free_inodes_count,
            used_dirs_count,
            flags,
            itable_unused,
            checksum,
        })
    }

    /// Verify the group descriptor CRC32C checksum.
    ///
    /// For `METADATA_CSUM` filesystems:
    ///   seed = checksum_seed if nonzero, else crc32c(uuid)
    ///   crc = crc32c(seed, le32(group_number))
    ///   crc = crc32c(crc, descriptor_bytes with checksum field zeroed)
    ///   stored checksum = low 16 bits of crc32c result
    ///
    /// The checksum field is 2 bytes at offset 0x1E.
    pub fn verify_checksum(
        &self,
        raw_buf: &[u8],
        uuid: &[u8; 16],
        group: u32,
        csum_seed: u32,
    ) -> bool {
        let crc32c = Crc::<u32>::new(&EXT4_CRC32C);

        // Seed: either stored checksum_seed or CRC32C(UUID)
        let seed = if csum_seed != 0 {
            csum_seed
        } else {
            let mut d = crc32c.digest();
            d.update(uuid);
            d.finalize()
        };

        // NOTE: Crc::digest_with_initial() applies reverse_bits() to
        // the initial value (because refin=true), so to set the raw CRC
        // register to `seed` we must pass seed.reverse_bits().
        let mut digest = crc32c.digest_with_initial(seed.reverse_bits());
        digest.update(&group.to_le_bytes());

        // Hash descriptor bytes, skipping the 2-byte checksum at offset 0x1E
        digest.update(&raw_buf[..0x1E]);
        if raw_buf.len() > 0x20 {
            digest.update(&[0u8; 2]); // zero placeholder for checksum field
            digest.update(&raw_buf[0x20..]);
        }

        let computed = digest.finalize();
        (computed & 0xFFFF) as u16 == self.checksum
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_32byte_descriptor() {
        let mut buf = vec![0u8; 32];
        buf[0x00] = 100; // bg_block_bitmap_lo
        buf[0x04] = 101; // bg_inode_bitmap_lo
        buf[0x08] = 102; // bg_inode_table_lo
        buf[0x0C] = 50; // bg_free_blocks_count_lo
        buf[0x0E] = 30; // bg_free_inodes_count_lo
        buf[0x12] = 0x04; // bg_flags (INODE_ZEROED)

        let gd = GroupDescriptor::parse(&buf, 32).unwrap();
        assert_eq!(gd.block_bitmap, 100);
        assert_eq!(gd.inode_bitmap, 101);
        assert_eq!(gd.inode_table, 102);
        assert_eq!(gd.free_blocks_count, 50);
        assert_eq!(gd.free_inodes_count, 30);
        assert!(gd.flags.contains(GroupDescFlags::INODE_ZEROED));
    }

    #[test]
    fn parse_64byte_descriptor() {
        let mut buf = vec![0u8; 64];
        buf[0x00] = 100;
        buf[0x04] = 101;
        buf[0x08] = 102;
        buf[0x20] = 1; // bg_block_bitmap_hi
        buf[0x24] = 2; // bg_inode_bitmap_hi
        buf[0x28] = 3; // bg_inode_table_hi

        let gd = GroupDescriptor::parse(&buf, 64).unwrap();
        assert_eq!(gd.block_bitmap, (1u64 << 32) | 0x64);
        assert_eq!(gd.inode_bitmap, (2u64 << 32) | 0x65);
        assert_eq!(gd.inode_table, (3u64 << 32) | 0x66);
    }

    #[test]
    fn reject_too_short() {
        let buf = vec![0u8; 10];
        let err = GroupDescriptor::parse(&buf, 32).unwrap_err();
        assert!(matches!(err, crate::error::Ext4Error::TooShort { .. }));
    }

    #[test]
    fn verify_group_descriptor_checksum_on_forensic_img() {
        let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/forensic.img");
        let data = if let Ok(d) = std::fs::read(path) {
            d
        } else {
            eprintln!("skip: forensic.img not found");
            return;
        };
        use crate::ondisk::superblock::Superblock;
        let sb = Superblock::parse(&data[1024..]).unwrap();
        assert!(
            sb.has_metadata_csum(),
            "forensic.img should have metadata_csum"
        );

        let desc_size = sb.desc_size;
        // GDT starts at block 1 for 4096-byte block size (block 0 is boot+superblock)
        let gdt_offset = sb.block_size as usize;

        // Verify first group descriptor
        let gd_buf = &data[gdt_offset..gdt_offset + desc_size as usize];
        let gd = GroupDescriptor::parse(gd_buf, desc_size).unwrap();
        assert!(
            gd.verify_checksum(gd_buf, &sb.uuid, 0, sb.checksum_seed),
            "group 0 descriptor checksum should verify"
        );
    }
}