diff --git a/.gitignore b/.gitignore index ea8c4bf..54b52f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/tests diff --git a/Cargo.lock b/Cargo.lock index c649027..06f6295 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,5 +3,84 @@ version = 4 [[package]] -name = "ext4-rs" +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "ext4" version = "0.1.0" +dependencies = [ + "anyhow", + "bitflags", + "static_assertions", + "thiserror", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" diff --git a/Cargo.toml b/Cargo.toml index 36fa7bf..0bea797 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,14 @@ [package] -name = "ext4-rs" +name = "ext4" version = "0.1.0" edition = "2024" +[[bin]] +name = "dump" +path = "src/dump.rs" + [dependencies] +anyhow = "1.0.98" +bitflags = "2.9.1" +static_assertions = "1.1.0" +thiserror = "2.0.12" diff --git a/README.md b/README.md new file mode 100644 index 0000000..1567e06 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +for specification, see: [https://docs.kernel.org/filesystems/ext4/](https://docs.kernel.org/filesystems/ext4/) + +[https://blogs.oracle.com/linux/post/understanding-ext4-disk-layout-part-1](https://blogs.oracle.com/linux/post/understanding-ext4-disk-layout-part-1) diff --git a/src/dump.rs b/src/dump.rs new file mode 100644 index 0000000..4a8b3b9 --- /dev/null +++ b/src/dump.rs @@ -0,0 +1,48 @@ +use std::io::{Read, Seek}; + +use ext4::{Superblock, group_has_superblock}; + +pub fn main() -> anyhow::Result<()> { + let args = std::env::args(); + + if args.len() != 2 { + anyhow::bail!("usage: dump "); + } + + let mut file = std::fs::File::open(args.skip(1).next().unwrap())?; + + // skip first 1024 bytes + file.seek(std::io::SeekFrom::Start(1024))?; + + let mut buf = [0; 1024]; + + file.read_exact(&mut buf)?; + + let superblock = Superblock::from_bytes(&buf)?; + + println!("Superblock 0 (offset: 1024): {:?}", superblock); + + let num_groups = superblock.blocks_count() / superblock.blocks_per_group() as u64; + + let group_size = (superblock.blocks_per_group() as u64) * superblock.blocksize(); + + println!("group_size: 0x{group_size:08X}"); + + file.seek(std::io::SeekFrom::Start(0))?; + + for group in 1..num_groups { + if group_has_superblock(group) { + let offset = group * group_size; + + file.seek(std::io::SeekFrom::Start(offset))?; + + file.read_exact(&mut buf)?; + + let superblock2 = Superblock::from_bytes(&buf)?; + + println!("Superblock {group: >2} (offset: 0x{:08X})", offset); + } + } + + Ok(()) +} diff --git a/src/group_descriptor.rs b/src/group_descriptor.rs new file mode 100644 index 0000000..b4dc30e --- /dev/null +++ b/src/group_descriptor.rs @@ -0,0 +1,55 @@ +use bitflags::bitflags; +use static_assertions::const_assert_eq; + +// #[derive(Debug, thiserror::Error)] +// pub struct GroupDescriptorError {} + +bitflags! { + pub struct Flags: u16 { + const INODE_UNINIT = 0x1; // inode table and bitmap are not initialized + const BLOCK_UNINIT = 0x2; // block bitmap is not initialized + const INODE_ZEROED = 0x4; // inode table is zeroed + + const _ = !0; + } +} + +#[derive(Debug)] +#[repr(C)] +pub struct GroupDescriptor { + block_bitmap_lo: u32, + inode_bitmap_lo: u32, + inode_table_lo: u32, + free_blocks_count_lo: u16, + free_inodes_count_lo: u16, + used_dirs_count_lo: u16, + flags: u16, + exclude_bitmap_lo: u32, + block_bitmap_csum_lo: u16, + inode_bitmap_csum_lo: u16, + itable_unused_lo: u16, + checksum: u16, + block_bitmap_hi: u32, + inode_bitmap_hblock_bitmap_csum_hii: u32, + inode_table_hi: u32, + free_blocks_count_hi: u16, + free_inodes_count_hi: u16, + used_dirs_count_hi: u16, + itable_unused_hi: u16, + exclude_bitmap_hi: u32, + block_bitmap_csum_hi: u16, + inode_bitmap_csum_hi: u16, + reserved: u32, +} + +const_assert_eq!(std::mem::size_of::(), 64); + +impl GroupDescriptor { + pub fn from_bytes(bytes: &[u8]) -> Result { + assert_eq!(bytes.len(), std::mem::size_of::()); + + let group_desc = unsafe { std::ptr::read(bytes.as_ptr() as *const GroupDescriptor) }; + + Ok(group_desc) + } +} diff --git a/src/lib.rs b/src/lib.rs index 8b13789..77e0f38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,94 @@ +use std::io::SeekFrom; +pub use crate::superblock::{SUPERBLOCK_SIZE, Superblock}; +pub use crate::utils::group_has_superblock; + +mod group_descriptor; +mod mmp; +mod superblock; +mod utils; + +pub trait FileLike { + fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> anyhow::Result<()>; + + fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> anyhow::Result<()>; + + fn read_at_offset_const(&mut self, offset: u64) -> anyhow::Result<[u8; N]> { + let mut buf = [0; N]; + + self.read_at_offset(offset, &mut buf)?; + + Ok(buf) + } +} + +impl FileLike for F { + fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> anyhow::Result<()> { + self.seek(SeekFrom::Start(offset))?; + + self.read_exact(buf)?; + + Ok(()) + } + + fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> anyhow::Result<()> { + self.seek(SeekFrom::Start(offset))?; + + self.write_all(bytes)?; + + Ok(()) + } +} + +// impl FileLike for &mut [u8] { +// fn read_at_offset(&mut self, offset: u64, buf: &mut [u8]) -> anyhow::Result<()> { +// anyhow::ensure!( +// offset as usize + buf.len() <= self.len(), +// "reading {} bytes at offset {} is out of bounds for slice of len {}", +// buf.len(), +// offset, +// self.len() +// ); +// +// buf.copy_from_slice(&self[offset as usize..][..buf.len()]); +// +// Ok(()) +// } +// +// fn write_at_offset(&mut self, offset: u64, bytes: &[u8]) -> anyhow::Result<()> { +// anyhow::ensure!( +// offset as usize + bytes.len() <= self.len(), +// "writing {} bytes at offset {} is out of bounds for slice of len {}", +// bytes.len(), +// offset, +// self.len() +// ); +// +// self[offset as usize..][..bytes.len()].copy_from_slice(bytes); +// +// Ok(()) +// } +// } + +pub struct Ext4Fs { + #[allow(dead_code)] + file: F, + + superblock: superblock::Superblock, +} + +impl Ext4Fs { + pub fn load(file: F) -> Result, anyhow::Error> { + let mut file = file; + + let superblock_bytes = file.read_at_offset_const::(1024)?; + + let superblock = Superblock::from_bytes(&superblock_bytes)?; + + Ok(Ext4Fs { file, superblock }) + } + + pub fn super_block(&self) -> &Superblock { + &self.superblock + } +} diff --git a/src/mmp.rs b/src/mmp.rs new file mode 100644 index 0000000..eb7c37b --- /dev/null +++ b/src/mmp.rs @@ -0,0 +1,23 @@ +#[derive(Debug)] +#[repr(C)] +pub struct Mmp { + magic: u32, // Magic number for MMP (0x004D4D50 (“MMP”)) + seq: u32, // Sequence number, updated periodically + time: u64, // Time that the MMP block was last updated + node_name: [u8; 64], // Hostname of the node that opened the filesystem + bdev_name: [u8; 32], // Block device name of the filesystem + check_interval: u16, // The MMP re-check interval, in seconds + pad1: u16, // zero + pad2: [u32; 226], // zero + checksum: u32, // Checksum of the MMP block +} + +impl Mmp { + pub fn from_bytes(bytes: &[u8]) -> Result { + assert_eq!(bytes.len(), std::mem::size_of::()); + + let group_desc = unsafe { std::ptr::read(bytes.as_ptr() as *const Mmp) }; + + Ok(group_desc) + } +} diff --git a/src/superblock.rs b/src/superblock.rs new file mode 100644 index 0000000..5a7088b --- /dev/null +++ b/src/superblock.rs @@ -0,0 +1,429 @@ +use bitflags::bitflags; +use static_assertions::const_assert_eq; + +use crate::utils::{combine_lo_hi, crc32}; + +bitflags! { + pub struct State: u16 { + const VALID_FS = 0x0001; // cleanly unmounted + const ERROR_FS = 0x0002; // errors detected + const ORPHAN_FS = 0x0004; // orphans being recovered + + const _ = !0; + } +} + +#[non_exhaustive] +#[repr(u32)] +pub enum CreatorOs { + Linux = 0, + Hurd = 1, + Masix = 2, + FreeBSD = 3, + Lites = 4, +} + +#[non_exhaustive] +#[repr(u32)] +pub enum Revision { + GoodOldExt4 = 0, + DynamicInodes = 1, +} + +bitflags! { + pub struct CompatibleFlags: u32 { + const DIR_PREALLOC = 0x1; // Directory preallocation + const IMAGIC_INODES = 0x2; // “imagic inodes”. Not clear from the code what this does + const HAS_JOURNAL = 0x4; // Has a journal + const EXT_ATTR = 0x8; // Supports extended attributes + const RESIZE_INODE = 0x10; // Has reserved GDT blocks for filesystem expansion + const DIR_INDEX = 0x20; // Has directory indices + const LAZY_BG = 0x40; /* “Lazy BG”. Not in Linux kernel, seems to have been for + uninitialized block groups? */ + const EXCLUDE_INODE = 0x80; // “Exclude inode”. Not used. + const EXCLUDE_BITMAP = 0x100; /* “Exclude bitmap”. Seems to be used to indicate the + presence of snapshot-related exclude bitmaps? Not in + kernel or used in e2fsprogs */ + const SPARSE_SUPER2 = 0x200; /* Sparse Super Block, v2. If this flag is set, the SB + field s_backup_bgs points to the two block that + contain backup superblocks */ + const FAST_COMMIT = 0x400; /* Fast commits supported. Although fast commits blocks + are backward incompatible, fast commit blocks are not + always present in the journal. If fast commit blocks + are present in the journal, JBD2 incompat feature + (JBD2_FEATURE_INFAST_COMMIT) gets set */ + const STABLE_INODES = 0x800; + const ORPHAN_FILE = 0x1000; /* Orphan file allocated. This is the special file for + more efficient tracking of unlinked but still open + inodes. When there may be any entries the file, we + additionally set proper rocompat feature + ORPHAN_PRESENT */ + + const _ = !0; + } +} + +bitflags! { + pub struct IncompatibleFeatures: u32 { + const COMPRESSION = 0x1; // Compression + const FILETYPE = 0x2; /* Directory entries record the file type. See + ext4_dir_entry_2 below */ + const RECOVER = 0x4; // Filesystem needs recovery + const JOURNAL_DEV = 0x8; // Filesystem has a separate journal device + const META_BG = 0x10; /* Meta block groups. See the earlier discussion of this + feature */ + const EXTENTS = 0x40; // Files in this filesystem use extents + const _64BIT = 0x80; // Enable a filesystem size of 2^64 blocks + const MMP = 0x100; // Multiple mount protection + const FLEX_BG = 0x200; /* Flexible block groups. See the earlier discussion of this + feature */ + const EA_INODE = 0x400; // Inodes can be used to store large extended attribute values + const DIRDATA = 0x1_000; // Data in directory entry + const CSUM_SEED = 0x2_000; /* Metadata checksum seed is stored in the superblock. This + feature enables the administrator to change the UUID of a + metadata_csum filesystem while the filesystem is mounted; + without it, the checksum definition requires all metadata + blocks to be rewritten */ + const LARGEDIR = 0x4_000; /* Large directory >2GB or 3-level htree (LARGEDIR). Prior to + this feature, directories could not be larger than 4GiB and + could not have an htree more than 2 levels deep. + If this feature is enabled, directories can be larger than + 4GiB and have a maximum htree depth of 3. */ + const INLINE_DATA = 0x8_000; // Data in inode + const ENCRYPT = 0x10_000; // Encrypted inodes are present on the filesystem + + const _ = !0; + } +} + +bitflags! { + pub struct RoCompatibleFeatures: u32 { + const SPARSE_SUPER = 0x1; /* Sparse superblocks. See the earlier discussion of this + feature */ + const LARGE_FILE = 0x2; /* This filesystem has been used to store a file greater + than 2GiB */ + const BTREE_DIR = 0x4; // Not used in kernel or e2fsprogs + const HUGE_FILE = 0x8; /* This filesystem has files whose sizes are represented + in units of logical blocks, not 512-byte sectors. This + implies a very large file indeed! */ + const GDT_CSUM = 0x10; /* Group descriptors have checksums. In addition to + detecting corruption, this is useful for lazy + formatting with uninitialized groups */ + const DIR_NLINK = 0x20; /* Indicates that the old ext3 32,000 subdirectory limit + no longer applies. + A directory’s i_links_count will be set to 1 if it is + incremented past 64,999*/ + const EXTRA_ISIZE = 0x40; // Indicates that large inodes exist on this filesystem + const HAS_SNAPSHOT = 0x80; // This filesystem has a snapshot + const QUOTA = 0x100; // Quota + const BIGALLOC = 0x200; /* This filesystem supports “bigalloc”, which means that + file extents are tracked in units of clusters + (of blocks) instead of blocks */ + const METADATA_CSUM = 0x400; /* This filesystem supports metadata checksumming. + (implies RO_COMPAT_GDT_CSUM, though GDT_CSUM must not + be set) */ + const REPLICA = 0x800; /* Filesystem supports replicas. This feature is neither + in the kernel nor e2fsprogs. */ + const READONLY = 0x1_000; /* Read-only filesystem image; the kernel will not mount + this image read-write and most tools will refuse to + write to the image */ + const PROJECT = 0x2_000; // Filesystem tracks project quotas + const VERITY = 0x8_000; // Verity inodes may be present on the filesystem + const ORPHAN_PRESENT = 0x10_000; /* Indicates orphan file may have valid orphan entries and + thus we need to clean them up when mounting the + filesystem */ + + const _ = !0; + } +} + +#[derive(Debug)] +#[repr(u8)] +#[non_exhaustive] +pub enum DefHashVersion { + Legacy = 0x0, + HalfMd4 = 0x1, + Tea = 0x2, + LegacyUnsigned = 0x3, + HalfMd4Unsigned = 0x4, + TeaUnsigned = 0x5, +} + +bitflags! { + pub struct DefaultMountOpts: u32 { + const EXT4_DEFM_DEBUG = 0x1; + const EXT4_DEFM_BSDGROUPS = 0x2; + const EXT4_DEFM_XATTR_USER = 0x4; + const EXT4_DEFM_ACL = 0x8; + const EXT4_DEFM_UID16 = 0x10; + const EXT4_DEFM_JMODE_DATA = 0x20; + const EXT4_DEFM_JMODE_ORDERED = 0x40; + const EXT4_DEFM_JMODE_WBACK = 0x60; + const EXT4_DEFM_NOBARRIER = 0x100; + const EXT4_DEFM_BLOCK_VALIDITY = 0x200; + const EXT4_DEFM_DISCARD = 0x400; + const EXT4_DEFM_NODELALLOC = 0x800; + + const _ = !0; + } +} + +bitflags! { + pub struct Flags: u32 { + const SignedHash = 0x1; // Signed dirhash in use + const UnsignedHash = 0x2; // Unsigned dirhash in use + const TestFilesys = 0x4; // to test development code + + const _ = !0; + } +} + +#[derive(Debug)] +#[non_exhaustive] +#[repr(u32)] +pub enum EncryptAlgos { + Invalid = 0x0, + Aes256Xts = 0x1, + Aes256Gcm = 0x2, + Aes256Cbc = 0x3, +} + +#[derive(Debug, thiserror::Error)] +pub enum SuperblockError { + #[error("invalid value {value} for field {field}")] + InvalidFieldValue { field: String, value: String }, + #[error("invalid checksum: expected {expected:X}, but found {actual:X}")] + InvalidChecksum { expected: u32, actual: u32 }, +} + +#[derive(Debug)] +#[repr(C)] +pub struct Superblock { + inodes_count: u32, // Total inode count + blocks_count_lo: u32, // Total block count + r_blocks_count_lo: u32, // This number of blocks can only be allocated by the super-user + free_blocks_count_lo: u32, // Free block count + free_inodes_count: u32, // Free inode count + first_data_block: u32, /* First data block. This must be at least 1 for 1k-block + * filesystems and is typically 0 for all other block sizes */ + log_block_size: u32, // Block size is 2 ^ (10 + s_log_block_size) + log_cluster_size: u32, /* Cluster size is 2 ^ (10 + s_log_cluster_size) blocks if bigalloc + * is enabled. Otherwise s_log_cluster_size must equal + * s_log_block_size */ + blocks_per_groups: u32, // Blocks per group + clusters_per_group: u32, /* Clusters per group, if bigalloc is enabled. Otherwise + * s_clusters_per_group must equal s_blocks_per_group */ + inodes_per_group: u32, // Inodes per group + mtime: u32, // Mount time, in seconds since the epoch + wtime: u32, // Write time, in seconds since the epoch + mnt_count: u16, // Number of mounts since the last fsck + max_mnt_count: u16, // Number of mounts beyond which a fsck is needed + magic: u16, // Magic signature, 0xEF53 + state: u16, // File system state. See super_state for more info + errors: u16, // Behaviour when detecting errors. See super_errors for more info + minor_rev_level: u16, // Minor revision level + lastcheck: u32, // Time of last check, in seconds since the epoch + checkinterval: u32, // Maximum time between checks, in seconds + creator_os: u32, // Creator OS. See the table super_creator for more info + rev_level: u32, // Revision level. See the table super_revision for more info + def_resuid: u16, // Default uid for reserved blocks + def_resgid: u16, // Default gid for reserved blocks + + // These fields are for EXT4_DYNAMIC_REV superblocks only. + // + // Note: the difference between the compatible feature set and the + // incompatible feature set is that if there is a bit set in the + // incompatible feature set that the kernel doesn’t know about, it + // should refuse to mount the filesystem. + // + // e2fsck’s requirements are more strict; if it doesn’t know about a + // feature in either the compatible or incompatible feature set, it + // must abort and not try to meddle with things it doesn’t + // understand... + first_ino: u32, // First non-reserved inode + inode_size: u16, // Size of inode structure, in bytes + block_group_nr: u16, // Block group # of this superblock + feature_compat: u32, /* Compatible feature set flags. Kernel can still read/write this fs + * even if it doesn’t understand a flag; fsck should not do that. See + * the super_compat table for more info */ + feature_incompat: u32, /* Incompatible feature set. If the kernel or fsck doesn’t understand + * one of these bits, it should stop. See the super_incompat table + * for more info. */ + feature_ro_compat: u32, /* Readonly-compatible feature set. If the kernel doesn’t understand + * one of these bits, it can still mount read-only. See the + * super_rocompat table for more info */ + uuid: [u8; 16], // 128-bit UUID for volume + volume_name: [u8; 16], // Volume label + last_mounted: [u8; 64], // Directory where filesystem was last mounted + algorithm_usage_bitmap: u32, // For compression (Not used in e2fsprogs/Linux) + + // Performance hints. Directory preallocation should only happen if + // the EXT4_FEATURE_INCOMPAT_DIR_PREALLOC flag is on. + prealloc_blocks: u8, /* #. of blocks to try to preallocate for ... files? (Not used in + * e2fsprogs/Linux) */ + prealloc_dir_blocks: u8, /* #. of blocks to preallocate for directories. (Not used in + * e2fsprogs/Linux) */ + reserved_gdt_blocks: u16, // Number of reserved GDT entries for future filesystem expansion + + // Journalling support is valid only if + // EXT4_FEATURE_COMPAT_HAS_JOURNAL is set + journal_uuid: [u8; 16], // UUID of journal superblock + journal_inum: u32, // inode number of journal file + journal_dev: u32, /* Device number of journal file, if the external journal feature + * flag is set */ + last_orphan: u32, // Start of list of orphaned inodes to delete + hash_seed: [u32; 4], // HTREE hash seed + def_hash_version: u8, /* Default hash algorithm to use for directory hashes. See + * super_def_hash for more info */ + jnl_backup_type: u8, /* If this value is 0 or EXT3_JNL_BACKUP_BLOCKS (1), then the + * s_jnl_blocks field contains a duplicate copy of the inode’s + * i_block[] array and i_size */ + desc_size: u16, /* Size of group descriptors, in bytes, if the 64bit incompat feature flag + * is set */ + default_mount_opts: u32, // Default mount options. See the super_mountopts table for more info + first_meta_bg: u32, // First metablock block group, if the meta_bg feature is enabled + mkfs_time: u32, // When the filesystem was created, in seconds since the epoch + jnl_blocks: [u32; 17], /* Backup copy of the journal inode’s i_block[] array in the first + * 15 elements and i_size_high and i_size in the 16th and 17th + * elements, respectively */ + + // 64bit support is valid only if EXT4_FEATURE_COMPAT_64BIT is set + blocks_count_hi: u32, // High 32-bits of the block count + r_blocks_count_hi: u32, // High 32-bits of the reserved block count + free_blocks_count_hi: u32, // High 32-bits of the free block count + min_extra_isize: u16, // All inodes have at least # bytes + want_extra_isize: u16, // New inodes should reserve # bytes + flags: u32, // Miscellaneous flags. See the super_flags table for more info + raid_stride: u16, /* RAID stride. This is the number of logical blocks read from or + * written to the disk before moving to the next disk. This + * affects the placement of filesystem metadata, which will + * hopefully make RAID storage faster */ + mmp_interval: u16, /* #. seconds to wait in multi-mount prevention (MMP) checking. In + * theory, MMP is a mechanism to record in the superblock which host and + * device have mounted the filesystem, in order to prevent multiple + * mounts. This feature does not seem to be implemented... */ + mmp_block: u64, // Block # for multi-mount protection data + raid_stripe_width: u32, /* RAID stripe width. This is the number of logical blocks read from + * or written to the disk before coming back to the current disk. + * This is used by the block allocator to try to reduce the number + * of read-modify-write operations in a RAID5/6 */ + log_groups_per_flex: u8, // Size of a flexible block group is 2 ^ s_log_groups_per_flex + checksum_type: u8, /* Metadata checksum algorithm type. The only valid value is 1 + * (crc32c) */ + encryption_level: u8, // Versioning level for encryption + reserved_pad: u8, // Padding to next 32bits + kbytes_written: u64, // Number of KiB written to this filesystem over its lifetime + snapshot_inum: u32, /* inode number of active snapshot. (Not used in + * e2fsprogs/Linux.) */ + snapshot_id: i32, // Sequential ID of active snapshot. (Not used in e2fsprogs/Linux.) + snapshot_r_blocks_count: u64, /* Number of blocks reserved for active snapshot’s future use. + * (Not used in e2fsprogs/Linux.) */ + snapshot_list: u32, /* inode number of the head of the on-disk snapshot list. (Not used in + * e2fsprogs/Linux.) */ + error_count: u32, // Number of errors seen + first_error_time: u32, // First time an error happened, in seconds since the epoch + first_error_ino: u32, // inode involved in first error + first_error_block: u64, // Number of block involved of first error + first_error_func: [u8; 32], // Name of function where the error happened + first_error_line: u32, // Line number where error happened + last_error_time: u32, // Time of most recent error, in seconds since the epoch + last_error_ino: u32, // inode involved in most recent error + last_error_line: u32, // Line number where most recent error happened + last_error_block: u64, // Number of block involved in most recent error + last_error_func: [u8; 32], // Name of function where the most recent error happened + mount_opts: [u8; 64], // ASCIIZ string of mount options + usr_quota_inum: u32, // Inode number of user quota file + grp_quota_inum: u32, // Inode number of group quota file + overhead_blocks: u32, /* Overhead blocks/clusters in fs. (Huh? This field is always + * zero, which means that the + * kernel calculates it dynamically.) */ + backup_bgs: [u32; 2], // Block groups containing superblock backups (if sparse_super2) + encrypt_algos: [u8; 4], /* Encryption algorithms in use. There can be up to four algorithms in + * use at any time; valid algorithm codes are given in the + * super_encrypt table below */ + encrypt_pw_salt: [u8; 16], // Salt for the string2key algorithm for encryption + lpf_ino: u32, // Inode number of lost+found + prj_quota_inum: u32, // Inode that tracks project quotas + checksum_seed: u32, /* Checksum seed used for metadata_csum calculations. This value + * is crc32c(~0, $orig_fs_uuid) */ + wtime_hi: u8, // Upper 8 bits of the s_wtime field + mtime_hi: u8, // Upper 8 bits of the s_mtime field + mkfs_time_hi: u8, // Upper 8 bits of the s_mkfs_time field + lastcheck_hi: u8, // Upper 8 bits of the s_lastcheck field + first_error_time_hi: u8, // Upper 8 bits of the s_first_error_time field + last_error_time_hi: u8, // Upper 8 bits of the s_last_error_time field + first_error_errcode: u8, + last_error_errcode: u8, + encoding: u16, // Filename charset encoding + encoding_flags: u16, // Filename charset encoding flags + orphan_file_inum: u32, // Orphan file inode number + reserved: [u32; 94], // Padding to the end of the block + checksum: u32, // Superblock checksum +} + +// offset of checksum + size of checksum +pub const SUPERBLOCK_SIZE: usize = 1024; + +const_assert_eq!(std::mem::size_of::(), SUPERBLOCK_SIZE); + +impl Superblock { + pub fn from_bytes(bytes: &[u8]) -> Result { + assert_eq!(bytes.len(), std::mem::size_of::()); + + let super_block = unsafe { std::ptr::read(bytes.as_ptr() as *const Superblock) }; + + let checksum = super_block.calc_checksum(); + + if super_block.checksum != checksum { + return Err(SuperblockError::InvalidChecksum { + expected: checksum, + actual: super_block.checksum, + }); + } + + Ok(super_block) + } + + pub fn calc_checksum(&self) -> u32 { + // let mut crc: u32 = !0; + + let bytes = unsafe { + let ptr = self as *const Superblock as *const u8; + let len = std::mem::size_of::(); + + let slice = std::slice::from_raw_parts(ptr, len); + + &slice[..len - 4] + }; + + crc32(bytes) + } + + pub fn inodes_count(&self) -> u32 { + self.inodes_count + } + + pub fn blocks_count(&self) -> u64 { + combine_lo_hi(self.blocks_count_lo, self.blocks_count_hi) + } + + pub fn r_blocks_count(&self) -> u64 { + combine_lo_hi(self.r_blocks_count_lo, self.r_blocks_count_hi) + } + + pub fn free_blocks_count(&self) -> u64 { + combine_lo_hi(self.free_blocks_count_lo, self.free_blocks_count_hi) + } + + pub fn free_inodes_count(&self) -> u32 { + self.free_inodes_count + } + + pub fn blocksize(&self) -> u64 { + 1 << (10 + self.log_block_size) + } + + pub fn blocks_per_group(&self) -> u32 { + self.blocks_per_groups + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..5c93aa8 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,83 @@ +const CRC_TABLE: [u32; 256] = [ + 0x00000000, 0xf26b8303, 0xe13b70f7, 0x1350f3f4, 0xc79a971f, 0x35f1141c, 0x26a1e7e8, 0xd4ca64eb, + 0x8ad958cf, 0x78b2dbcc, 0x6be22838, 0x9989ab3b, 0x4d43cfd0, 0xbf284cd3, 0xac78bf27, 0x5e133c24, + 0x105ec76f, 0xe235446c, 0xf165b798, 0x030e349b, 0xd7c45070, 0x25afd373, 0x36ff2087, 0xc494a384, + 0x9a879fa0, 0x68ec1ca3, 0x7bbcef57, 0x89d76c54, 0x5d1d08bf, 0xaf768bbc, 0xbc267848, 0x4e4dfb4b, + 0x20bd8ede, 0xd2d60ddd, 0xc186fe29, 0x33ed7d2a, 0xe72719c1, 0x154c9ac2, 0x061c6936, 0xf477ea35, + 0xaa64d611, 0x580f5512, 0x4b5fa6e6, 0xb93425e5, 0x6dfe410e, 0x9f95c20d, 0x8cc531f9, 0x7eaeb2fa, + 0x30e349b1, 0xc288cab2, 0xd1d83946, 0x23b3ba45, 0xf779deae, 0x05125dad, 0x1642ae59, 0xe4292d5a, + 0xba3a117e, 0x4851927d, 0x5b016189, 0xa96ae28a, 0x7da08661, 0x8fcb0562, 0x9c9bf696, 0x6ef07595, + 0x417b1dbc, 0xb3109ebf, 0xa0406d4b, 0x522bee48, 0x86e18aa3, 0x748a09a0, 0x67dafa54, 0x95b17957, + 0xcba24573, 0x39c9c670, 0x2a993584, 0xd8f2b687, 0x0c38d26c, 0xfe53516f, 0xed03a29b, 0x1f682198, + 0x5125dad3, 0xa34e59d0, 0xb01eaa24, 0x42752927, 0x96bf4dcc, 0x64d4cecf, 0x77843d3b, 0x85efbe38, + 0xdbfc821c, 0x2997011f, 0x3ac7f2eb, 0xc8ac71e8, 0x1c661503, 0xee0d9600, 0xfd5d65f4, 0x0f36e6f7, + 0x61c69362, 0x93ad1061, 0x80fde395, 0x72966096, 0xa65c047d, 0x5437877e, 0x4767748a, 0xb50cf789, + 0xeb1fcbad, 0x197448ae, 0x0a24bb5a, 0xf84f3859, 0x2c855cb2, 0xdeeedfb1, 0xcdbe2c45, 0x3fd5af46, + 0x7198540d, 0x83f3d70e, 0x90a324fa, 0x62c8a7f9, 0xb602c312, 0x44694011, 0x5739b3e5, 0xa55230e6, + 0xfb410cc2, 0x092a8fc1, 0x1a7a7c35, 0xe811ff36, 0x3cdb9bdd, 0xceb018de, 0xdde0eb2a, 0x2f8b6829, + 0x82f63b78, 0x709db87b, 0x63cd4b8f, 0x91a6c88c, 0x456cac67, 0xb7072f64, 0xa457dc90, 0x563c5f93, + 0x082f63b7, 0xfa44e0b4, 0xe9141340, 0x1b7f9043, 0xcfb5f4a8, 0x3dde77ab, 0x2e8e845f, 0xdce5075c, + 0x92a8fc17, 0x60c37f14, 0x73938ce0, 0x81f80fe3, 0x55326b08, 0xa759e80b, 0xb4091bff, 0x466298fc, + 0x1871a4d8, 0xea1a27db, 0xf94ad42f, 0x0b21572c, 0xdfeb33c7, 0x2d80b0c4, 0x3ed04330, 0xccbbc033, + 0xa24bb5a6, 0x502036a5, 0x4370c551, 0xb11b4652, 0x65d122b9, 0x97baa1ba, 0x84ea524e, 0x7681d14d, + 0x2892ed69, 0xdaf96e6a, 0xc9a99d9e, 0x3bc21e9d, 0xef087a76, 0x1d63f975, 0x0e330a81, 0xfc588982, + 0xb21572c9, 0x407ef1ca, 0x532e023e, 0xa145813d, 0x758fe5d6, 0x87e466d5, 0x94b49521, 0x66df1622, + 0x38cc2a06, 0xcaa7a905, 0xd9f75af1, 0x2b9cd9f2, 0xff56bd19, 0x0d3d3e1a, 0x1e6dcdee, 0xec064eed, + 0xc38d26c4, 0x31e6a5c7, 0x22b65633, 0xd0ddd530, 0x0417b1db, 0xf67c32d8, 0xe52cc12c, 0x1747422f, + 0x49547e0b, 0xbb3ffd08, 0xa86f0efc, 0x5a048dff, 0x8ecee914, 0x7ca56a17, 0x6ff599e3, 0x9d9e1ae0, + 0xd3d3e1ab, 0x21b862a8, 0x32e8915c, 0xc083125f, 0x144976b4, 0xe622f5b7, 0xf5720643, 0x07198540, + 0x590ab964, 0xab613a67, 0xb831c993, 0x4a5a4a90, 0x9e902e7b, 0x6cfbad78, 0x7fab5e8c, 0x8dc0dd8f, + 0xe330a81a, 0x115b2b19, 0x020bd8ed, 0xf0605bee, 0x24aa3f05, 0xd6c1bc06, 0xc5914ff2, 0x37faccf1, + 0x69e9f0d5, 0x9b8273d6, 0x88d28022, 0x7ab90321, 0xae7367ca, 0x5c18e4c9, 0x4f48173d, 0xbd23943e, + 0xf36e6f75, 0x0105ec76, 0x12551f82, 0xe03e9c81, 0x34f4f86a, 0xc69f7b69, 0xd5cf889d, 0x27a40b9e, + 0x79b737ba, 0x8bdcb4b9, 0x988c474d, 0x6ae7c44e, 0xbe2da0a5, 0x4c4623a6, 0x5f16d052, 0xad7d5351, +]; + +pub fn crc32(bytes: &[u8]) -> u32 { + let mut crc: u32 = !0; + + for &byte in bytes { + crc = (crc >> 8) ^ CRC_TABLE[(crc as u8 ^ byte) as usize]; + } + + crc +} + +// a group has a superblock if it is a power of 3, of 5, or of 7 +// (see https://docs.kernel.org/filesystems/ext4/globals.html#super-block) +pub fn group_has_superblock(group_num: u64) -> bool { + // this function calculates the max power of n that can be represented by a u64 + const fn max_power_in_u64(n: u64) -> u64 { + let mut max = n; + + while let Some(new_max) = max.checked_mul(n) { + max = new_max; + } + + max + } + + const MAX_POWER_OF_3: u64 = max_power_in_u64(3); + const MAX_POWER_OF_5: u64 = max_power_in_u64(5); + const MAX_POWER_OF_7: u64 = max_power_in_u64(7); + + // fast path: groups 0 and 1 have superblocks + if group_num <= 1 { + return true; + } + + // fast path: even groups above 1 never have superblocks + if group_num % 2 == 0 { + return false; + } + + // the way this works is that a smaller power of n will always divide a larger power of n, thus + // for u64s, x is a power of n iff it divides the largest power of n + MAX_POWER_OF_3 % group_num == 0 + || MAX_POWER_OF_5 % group_num == 0 + || MAX_POWER_OF_7 % group_num == 0 +} + +pub fn combine_lo_hi(lo: u32, hi: u32) -> u64 { + lo as u64 | ((hi as u64) << 32) +}