wxtla 0.3.1

Wired eXploring Target Layer Accessor
Documentation
//! ext-family superblock and group-descriptor parsing.

use crate::{ByteSource, Error, Result};

pub(crate) const SUPERBLOCK_OFFSET: u64 = 1024;
pub(crate) const SUPERBLOCK_SIZE: usize = 1024;
const SUPERBLOCK_MAGIC: u16 = 0xEF53;
const FEATURE_INCOMPAT_EXTENTS: u32 = 0x0000_0040;

pub(crate) const INODE_FLAG_EXTENTS: u32 = 0x0008_0000;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExtVariant {
  Ext2,
  Ext3,
  Ext4,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExtSuperblock {
  pub variant: ExtVariant,
  pub blocks_count: u64,
  pub block_size: u32,
  pub blocks_per_group: u32,
  pub inodes_per_group: u32,
  pub inode_size: u16,
  pub first_data_block: u32,
  pub compatible_features: u32,
  pub incompatible_features: u32,
  pub readonly_compatible_features: u32,
  pub descriptor_size: u16,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExtGroupDescriptor {
  pub inode_table_block: u64,
}

impl ExtSuperblock {
  pub fn read(source: &dyn ByteSource) -> Result<Self> {
    let bytes = source.read_bytes_at(SUPERBLOCK_OFFSET, SUPERBLOCK_SIZE)?;
    Self::from_bytes(&bytes)
  }

  pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
    let superblock: &[u8; SUPERBLOCK_SIZE] = bytes
      .try_into()
      .map_err(|_| Error::invalid_format("ext superblock must be exactly 1024 bytes"))?;
    Self::from_superblock(superblock)
  }

  pub fn from_superblock(superblock: &[u8; SUPERBLOCK_SIZE]) -> Result<Self> {
    let magic = le_u16(&superblock[56..58]);
    if magic != SUPERBLOCK_MAGIC {
      return Err(Error::invalid_format(
        "ext superblock magic is missing".to_string(),
      ));
    }

    let log_block_size = le_u32(&superblock[24..28]);
    let block_size = 1024u32
      .checked_shl(log_block_size)
      .ok_or_else(|| Error::invalid_range("ext block size overflow"))?;
    if block_size < 1024 {
      return Err(Error::invalid_format(
        "ext block size must be at least 1024 bytes".to_string(),
      ));
    }

    let blocks_count_lo = u64::from(le_u32(&superblock[4..8]));
    let incompatible_features = le_u32(&superblock[96..100]);
    let readonly_compatible_features = le_u32(&superblock[100..104]);
    let blocks_count_hi = if incompatible_features & 0x80 != 0 {
      u64::from(le_u32(&superblock[336..340]))
    } else {
      0
    };
    let blocks_count = blocks_count_lo | (blocks_count_hi << 32);
    if blocks_count == 0 {
      return Err(Error::invalid_format(
        "ext block count must be non-zero".to_string(),
      ));
    }

    let blocks_per_group = le_u32(&superblock[32..36]);
    let inodes_per_group = le_u32(&superblock[40..44]);
    if blocks_per_group == 0 || inodes_per_group == 0 {
      return Err(Error::invalid_format(
        "ext group sizes must be non-zero".to_string(),
      ));
    }

    let inode_size = le_u16(&superblock[88..90]);
    if inode_size < 128 {
      return Err(Error::invalid_format(
        "ext inode size must be at least 128 bytes".to_string(),
      ));
    }

    let compatible_features = le_u32(&superblock[92..96]);
    let variant = if incompatible_features & FEATURE_INCOMPAT_EXTENTS != 0 {
      ExtVariant::Ext4
    } else if compatible_features & 0x0000_0004 != 0 {
      ExtVariant::Ext3
    } else {
      ExtVariant::Ext2
    };
    let descriptor_size = if incompatible_features & 0x80 != 0 {
      let descriptor_size = le_u16(&superblock[254..256]);
      if descriptor_size < 32 {
        return Err(Error::invalid_format(
          "ext64 group descriptor size must be at least 32 bytes".to_string(),
        ));
      }
      descriptor_size
    } else {
      32
    };

    Ok(Self {
      variant,
      blocks_count,
      block_size,
      blocks_per_group,
      inodes_per_group,
      inode_size,
      first_data_block: le_u32(&superblock[20..24]),
      compatible_features,
      incompatible_features,
      readonly_compatible_features,
      descriptor_size,
    })
  }

  pub fn block_size_u64(&self) -> u64 {
    u64::from(self.block_size)
  }

  pub fn block_offset(&self, block: u64) -> Result<u64> {
    block
      .checked_mul(self.block_size_u64())
      .ok_or_else(|| Error::invalid_range("ext block offset overflow"))
  }

  pub fn group_count(&self) -> u64 {
    self.blocks_count.div_ceil(u64::from(self.blocks_per_group))
  }

  pub fn group_descriptor_table_offset(&self) -> u64 {
    if self.block_size == 1024 {
      2 * self.block_size_u64()
    } else {
      self.block_size_u64()
    }
  }
}

pub(crate) fn read_group_descriptors(
  source: &dyn ByteSource, superblock: &ExtSuperblock,
) -> Result<Vec<ExtGroupDescriptor>> {
  let descriptor_size = usize::from(superblock.descriptor_size);
  let group_count = usize::try_from(superblock.group_count())
    .map_err(|_| Error::invalid_range("ext group count is too large"))?;
  let table_size = descriptor_size
    .checked_mul(group_count)
    .ok_or_else(|| Error::invalid_range("ext descriptor table size overflow"))?;
  let table = source.read_bytes_at(superblock.group_descriptor_table_offset(), table_size)?;
  let mut descriptors = Vec::with_capacity(group_count);

  for descriptor in table.chunks_exact(descriptor_size) {
    let inode_table_lo = u64::from(le_u32(&descriptor[8..12]));
    let inode_table_hi = if descriptor_size >= 44 {
      u64::from(le_u32(&descriptor[40..44]))
    } else {
      0
    };
    descriptors.push(ExtGroupDescriptor {
      inode_table_block: inode_table_lo | (inode_table_hi << 32),
    });
  }

  Ok(descriptors)
}

fn le_u16(bytes: &[u8]) -> u16 {
  let mut raw = [0u8; 2];
  raw.copy_from_slice(bytes);
  u16::from_le_bytes(raw)
}

fn le_u32(bytes: &[u8]) -> u32 {
  let mut raw = [0u8; 4];
  raw.copy_from_slice(bytes);
  u32::from_le_bytes(raw)
}

#[cfg(test)]
mod tests {
  use std::path::Path;

  use super::*;

  fn fixture_path(relative: &str) -> std::path::PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
      .join("formats")
      .join("ext")
      .join("libfsext")
      .join(relative)
  }

  #[test]
  fn classifies_ext4_when_extents_are_enabled() {
    let mut superblock = [0u8; SUPERBLOCK_SIZE];
    superblock[24..28].copy_from_slice(&0u32.to_le_bytes());
    superblock[4..8].copy_from_slice(&4096u32.to_le_bytes());
    superblock[32..36].copy_from_slice(&8192u32.to_le_bytes());
    superblock[40..44].copy_from_slice(&1024u32.to_le_bytes());
    superblock[56..58].copy_from_slice(&SUPERBLOCK_MAGIC.to_le_bytes());
    superblock[88..90].copy_from_slice(&256u16.to_le_bytes());
    superblock[96..100].copy_from_slice(&0x40u32.to_le_bytes());

    let parsed = ExtSuperblock::from_superblock(&superblock).unwrap();
    assert_eq!(parsed.variant, ExtVariant::Ext4);
  }

  #[test]
  fn parses_libfsext_superblock_fixture() {
    let bytes = std::fs::read(fixture_path("superblock.1")).unwrap();
    let superblock = ExtSuperblock::from_bytes(&bytes).unwrap();

    assert_eq!(superblock.variant, ExtVariant::Ext3);
    assert_eq!(superblock.blocks_count, 5120);
    assert_eq!(superblock.block_size, 1024);
    assert_eq!(superblock.blocks_per_group, 8192);
    assert_eq!(superblock.inode_size, 128);
    assert_eq!(superblock.compatible_features, 0x4);
    assert_eq!(superblock.incompatible_features, 0x2);
    assert_eq!(superblock.readonly_compatible_features, 0x1);
  }
}