pub const EXT4_EXT_MAGIC: u16 = 0xF30A;
pub const MAX_EXTENTS_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,
}
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() {
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)
}
#[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);
}
}