pub const EXT4_EXT_MAGIC: u16 = 0xF30A;
pub const MAX_EXTENTS_IN_INODE: usize = 4;
pub const MAX_INDICES_IN_INODE: usize = 4;
pub const MAX_LEN_PER_EXTENT: u16 = 32_768;
#[derive(Debug, Clone, Copy)]
pub struct ExtentRun {
pub logical: u32,
pub len: u16,
pub physical: u64,
}
#[derive(Debug, Clone, Copy)]
pub struct ExtentIdx {
pub block: u32,
pub leaf: u64,
}
pub fn entries_per_leaf_block(block_size: u32) -> usize {
((block_size as usize) - 12) / 12
}
pub fn encode_header(entries: u16, max: u16, depth: u16) -> [u8; 12] {
let mut out = [0u8; 12];
out[0..2].copy_from_slice(&EXT4_EXT_MAGIC.to_le_bytes());
out[2..4].copy_from_slice(&entries.to_le_bytes());
out[4..6].copy_from_slice(&max.to_le_bytes());
out[6..8].copy_from_slice(&depth.to_le_bytes());
out
}
pub fn encode_leaf(run: ExtentRun) -> [u8; 12] {
assert!(
run.len <= MAX_LEN_PER_EXTENT,
"extent length {} exceeds initialized cap {}",
run.len,
MAX_LEN_PER_EXTENT
);
let mut out = [0u8; 12];
out[0..4].copy_from_slice(&run.logical.to_le_bytes());
out[4..6].copy_from_slice(&run.len.to_le_bytes());
out[6..8].copy_from_slice(&((run.physical >> 32) as u16).to_le_bytes());
out[8..12].copy_from_slice(&(run.physical as u32).to_le_bytes());
out
}
pub fn coalesce(data_blocks: &[u32]) -> Vec<ExtentRun> {
let mut out: Vec<ExtentRun> = Vec::new();
for (i, &phys) in data_blocks.iter().enumerate() {
if phys == 0 {
continue; }
let logical = i as u32;
if let Some(last) = out.last_mut() {
let next_phys_in_run = last.physical + last.len as u64;
if next_phys_in_run == phys as u64
&& last.len < MAX_LEN_PER_EXTENT
&& (last.logical + last.len as u32) == logical
{
last.len += 1;
continue;
}
}
out.push(ExtentRun {
logical,
len: 1,
physical: phys as u64,
});
}
out
}
pub fn pack_into_iblock(runs: &[ExtentRun]) -> crate::Result<[u8; 60]> {
if runs.len() > MAX_EXTENTS_IN_INODE {
return Err(crate::Error::Unsupported(format!(
"ext4: file requires {} extents, max {} per depth-0 tree (multi-level trees not yet implemented)",
runs.len(),
MAX_EXTENTS_IN_INODE
)));
}
let mut out = [0u8; 60];
let hdr = encode_header(runs.len() as u16, MAX_EXTENTS_IN_INODE as u16, 0);
out[0..12].copy_from_slice(&hdr);
for (i, run) in runs.iter().enumerate() {
let off = 12 + i * 12;
out[off..off + 12].copy_from_slice(&encode_leaf(*run));
}
Ok(out)
}
pub fn encode_idx(idx: ExtentIdx) -> [u8; 12] {
let mut out = [0u8; 12];
out[0..4].copy_from_slice(&idx.block.to_le_bytes());
out[4..8].copy_from_slice(&(idx.leaf as u32).to_le_bytes());
out[8..10].copy_from_slice(&((idx.leaf >> 32) as u16).to_le_bytes());
out
}
pub fn decode_idx(buf: &[u8]) -> ExtentIdx {
let block = u32::from_le_bytes(buf[0..4].try_into().unwrap());
let leaf_lo = u32::from_le_bytes(buf[4..8].try_into().unwrap()) as u64;
let leaf_hi = u16::from_le_bytes(buf[8..10].try_into().unwrap()) as u64;
ExtentIdx {
block,
leaf: (leaf_hi << 32) | leaf_lo,
}
}
pub fn pack_idx_into_iblock(indices: &[ExtentIdx]) -> crate::Result<[u8; 60]> {
if indices.len() > MAX_INDICES_IN_INODE {
return Err(crate::Error::Unsupported(format!(
"ext4: depth-1 tree requires {} idx entries, max {} inline (depth > 1 not implemented)",
indices.len(),
MAX_INDICES_IN_INODE
)));
}
let mut out = [0u8; 60];
let hdr = encode_header(indices.len() as u16, MAX_INDICES_IN_INODE as u16, 1);
out[0..12].copy_from_slice(&hdr);
for (i, idx) in indices.iter().enumerate() {
let off = 12 + i * 12;
out[off..off + 12].copy_from_slice(&encode_idx(*idx));
}
Ok(out)
}
pub fn encode_leaf_block(runs: &[ExtentRun], block_size: u32) -> crate::Result<Vec<u8>> {
let max = entries_per_leaf_block(block_size);
if runs.len() > max {
return Err(crate::Error::Unsupported(format!(
"ext4: leaf block would need {} entries, max {} per {}-byte block (depth > 1 not implemented)",
runs.len(),
max,
block_size,
)));
}
let mut out = vec![0u8; block_size as usize];
let hdr = encode_header(runs.len() as u16, max as u16, 0);
out[0..12].copy_from_slice(&hdr);
for (i, r) in runs.iter().enumerate() {
let off = 12 + i * 12;
out[off..off + 12].copy_from_slice(&encode_leaf(*r));
}
Ok(out)
}
pub fn decode_leaf_block(buf: &[u8]) -> crate::Result<(ExtentHeader, Vec<ExtentRun>)> {
let header = decode_header(&buf[..12])?;
if header.depth != 0 {
return Err(crate::Error::Unsupported(format!(
"ext4: nested extent block has depth {} (only depth 0 leaves supported)",
header.depth
)));
}
let max = entries_per_leaf_block(buf.len() as u32);
if header.entries as usize > max {
return Err(crate::Error::InvalidImage(format!(
"ext4: leaf block claims {} entries, max {}",
header.entries, max
)));
}
let mut runs = Vec::with_capacity(header.entries as usize);
for i in 0..header.entries as usize {
let off = 12 + i * 12;
runs.push(decode_leaf(&buf[off..off + 12]));
}
Ok((header, runs))
}
pub fn decode_idx_iblock(buf: &[u8; 60]) -> crate::Result<(ExtentHeader, Vec<ExtentIdx>)> {
let header = decode_header(&buf[..12])?;
if header.entries as usize > MAX_INDICES_IN_INODE {
return Err(crate::Error::InvalidImage(format!(
"ext4: inline extent header claims {} idx entries, max is {}",
header.entries, MAX_INDICES_IN_INODE
)));
}
let mut indices = Vec::with_capacity(header.entries as usize);
for i in 0..header.entries as usize {
let off = 12 + i * 12;
indices.push(decode_idx(&buf[off..off + 12]));
}
Ok((header, indices))
}
pub fn iblock_to_bytes(slots: &[u32; super::constants::N_BLOCKS]) -> [u8; 60] {
let mut out = [0u8; 60];
for (i, slot) in slots.iter().enumerate() {
let off = i * 4;
out[off..off + 4].copy_from_slice(&slot.to_le_bytes());
}
out
}
pub fn bytes_to_iblock(bytes: &[u8; 60]) -> [u32; super::constants::N_BLOCKS] {
let mut out = [0u32; super::constants::N_BLOCKS];
for (i, slot) in out.iter_mut().enumerate() {
let off = i * 4;
*slot = u32::from_le_bytes(bytes[off..off + 4].try_into().unwrap());
}
out
}
#[derive(Debug, Clone, Copy)]
pub struct ExtentHeader {
pub entries: u16,
pub max: u16,
pub depth: u16,
}
pub fn decode_header(buf: &[u8]) -> crate::Result<ExtentHeader> {
if buf.len() < 12 {
return Err(crate::Error::InvalidImage(
"ext4: extent header buffer too small".into(),
));
}
let magic = u16::from_le_bytes(buf[0..2].try_into().unwrap());
if magic != EXT4_EXT_MAGIC {
return Err(crate::Error::InvalidImage(format!(
"ext4: extent header magic {magic:#06x} != {:#06x}",
EXT4_EXT_MAGIC
)));
}
Ok(ExtentHeader {
entries: u16::from_le_bytes(buf[2..4].try_into().unwrap()),
max: u16::from_le_bytes(buf[4..6].try_into().unwrap()),
depth: u16::from_le_bytes(buf[6..8].try_into().unwrap()),
})
}
pub fn decode_leaf(buf: &[u8]) -> ExtentRun {
let ee_block = u32::from_le_bytes(buf[0..4].try_into().unwrap());
let ee_len = u16::from_le_bytes(buf[4..6].try_into().unwrap());
let ee_start_hi = u16::from_le_bytes(buf[6..8].try_into().unwrap()) as u64;
let ee_start_lo = u32::from_le_bytes(buf[8..12].try_into().unwrap()) as u64;
ExtentRun {
logical: ee_block,
len: ee_len,
physical: (ee_start_hi << 32) | ee_start_lo,
}
}
pub fn decode_depth0_iblock(buf: &[u8; 60]) -> crate::Result<(ExtentHeader, Vec<ExtentRun>)> {
let header = decode_header(&buf[..12])?;
if header.entries as usize > MAX_EXTENTS_IN_INODE {
return Err(crate::Error::InvalidImage(format!(
"ext4: inline extent header claims {} entries, max is {}",
header.entries, MAX_EXTENTS_IN_INODE
)));
}
let mut runs = Vec::with_capacity(header.entries as usize);
if header.depth == 0 {
for i in 0..header.entries as usize {
let off = 12 + i * 12;
runs.push(decode_leaf(&buf[off..off + 12]));
}
}
Ok((header, runs))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_layout() {
let h = encode_header(2, 4, 0);
assert_eq!(&h[0..2], &EXT4_EXT_MAGIC.to_le_bytes());
assert_eq!(u16::from_le_bytes(h[2..4].try_into().unwrap()), 2);
assert_eq!(u16::from_le_bytes(h[4..6].try_into().unwrap()), 4);
assert_eq!(u16::from_le_bytes(h[6..8].try_into().unwrap()), 0);
}
#[test]
fn leaf_layout() {
let leaf = encode_leaf(ExtentRun {
logical: 0,
len: 12,
physical: 0x1_2345_6789,
});
assert_eq!(u32::from_le_bytes(leaf[0..4].try_into().unwrap()), 0);
assert_eq!(u16::from_le_bytes(leaf[4..6].try_into().unwrap()), 12);
assert_eq!(u16::from_le_bytes(leaf[6..8].try_into().unwrap()), 0x0001);
assert_eq!(
u32::from_le_bytes(leaf[8..12].try_into().unwrap()),
0x2345_6789
);
}
#[test]
fn coalesce_contiguous() {
let blocks: Vec<u32> = (100..112).collect(); let runs = coalesce(&blocks);
assert_eq!(runs.len(), 1);
assert_eq!(runs[0].logical, 0);
assert_eq!(runs[0].len, 12);
assert_eq!(runs[0].physical, 100);
}
#[test]
fn coalesce_with_gap() {
let blocks = vec![100, 101, 102, 200, 201];
let runs = coalesce(&blocks);
assert_eq!(runs.len(), 2);
assert_eq!(
(runs[0].logical, runs[0].len, runs[0].physical),
(0, 3, 100)
);
assert_eq!(
(runs[1].logical, runs[1].len, runs[1].physical),
(3, 2, 200)
);
}
#[test]
fn pack_rejects_too_many_extents() {
let runs: Vec<_> = (0..5)
.map(|i| ExtentRun {
logical: i * 10,
len: 1,
physical: 1000 + i as u64 * 10,
})
.collect();
let err = pack_into_iblock(&runs).unwrap_err();
assert!(matches!(err, crate::Error::Unsupported(_)));
}
#[test]
fn pack_roundtrip_one_extent() {
let runs = vec![ExtentRun {
logical: 0,
len: 12,
physical: 100,
}];
let packed = pack_into_iblock(&runs).unwrap();
assert_eq!(&packed[0..2], &EXT4_EXT_MAGIC.to_le_bytes());
assert_eq!(u16::from_le_bytes(packed[2..4].try_into().unwrap()), 1);
assert_eq!(u16::from_le_bytes(packed[4..6].try_into().unwrap()), 4);
assert_eq!(u16::from_le_bytes(packed[6..8].try_into().unwrap()), 0);
assert_eq!(u32::from_le_bytes(packed[12..16].try_into().unwrap()), 0);
assert_eq!(u16::from_le_bytes(packed[16..18].try_into().unwrap()), 12);
assert_eq!(u32::from_le_bytes(packed[20..24].try_into().unwrap()), 100);
}
}