use std::io::{Read, Seek, SeekFrom};
use crate::bytes::{le_u32, le_u64};
const SM_BLOCKS_PER_CHUNK: usize = 36; const SM_CHUNKS_PER_CIB: usize = 40; const SM_DEV0: usize = 48; const DEV_BLOCK_COUNT: usize = 0; const DEV_CHUNK_COUNT: usize = 8; const DEV_CIB_COUNT: usize = 16; const DEV_CAB_COUNT: usize = 20; const DEV_ADDR_OFFSET: usize = 32; const CIB_CHUNK_INFO_COUNT: usize = 36; const CIB_CHUNK_INFO: usize = 40; const CHUNK_INFO_LEN: usize = 32;
const CI_BITMAP_ADDR: usize = 24;
pub(crate) fn read_obj_block<R: Read + Seek>(
reader: &mut R,
paddr: u64,
block_size: usize,
) -> crate::Result<Vec<u8>> {
let mut buf = vec![0u8; block_size];
let offset = paddr.saturating_mul(block_size as u64);
reader.seek(std::io::SeekFrom::Start(offset))?;
reader.read_exact(&mut buf)?;
let stored = crate::object::fletcher64_stored(&buf);
let computed = crate::object::fletcher64_checksum(&buf);
if stored != computed {
return Err(crate::ApfsError::ChecksumMismatch {
block: le_u64(&buf, 8),
stored,
computed,
});
}
Ok(buf)
}
pub fn is_block_free<R: Read + Seek>(
reader: &mut R,
spaceman_paddr: u64,
block: u64,
block_size: usize,
) -> crate::Result<bool> {
let sm = read_obj_block(reader, spaceman_paddr, block_size)?;
let blocks_per_chunk = u64::from(le_u32(&sm, SM_BLOCKS_PER_CHUNK));
let chunks_per_cib = u64::from(le_u32(&sm, SM_CHUNKS_PER_CIB));
let block_count = le_u64(&sm, SM_DEV0 + DEV_BLOCK_COUNT);
let chunk_count = le_u64(&sm, SM_DEV0 + DEV_CHUNK_COUNT);
let cib_count = u64::from(le_u32(&sm, SM_DEV0 + DEV_CIB_COUNT));
let cab_count = u64::from(le_u32(&sm, SM_DEV0 + DEV_CAB_COUNT));
let addr_offset = le_u32(&sm, SM_DEV0 + DEV_ADDR_OFFSET) as usize;
let range = |field, value, cap| crate::ApfsError::FieldOutOfRange {
structure: "spaceman_phys",
field,
value,
cap,
};
if blocks_per_chunk == 0 || chunks_per_cib == 0 {
return Err(range("sm_blocks_per_chunk/sm_chunks_per_cib", 0, 1));
}
if block >= block_count {
return Err(range("block", block, block_count));
}
if cab_count != 0 {
return Err(crate::ApfsError::UnsupportedSpacemanCab { count: cab_count });
}
let chunk_index = block / blocks_per_chunk;
if chunk_index >= chunk_count {
return Err(range("chunk_index", chunk_index, chunk_count));
}
let cib_idx = chunk_index / chunks_per_cib;
let within_cib = (chunk_index % chunks_per_cib) as usize;
if cib_idx >= cib_count {
return Err(range("cib_index", cib_idx, cib_count));
}
let cib_paddr = le_u64(&sm, addr_offset + cib_idx as usize * 8);
let cib = read_obj_block(reader, cib_paddr, block_size)?;
let ci_count = le_u32(&cib, CIB_CHUNK_INFO_COUNT) as usize;
if within_cib >= ci_count {
return Err(range(
"chunk_info_index",
within_cib as u64,
ci_count as u64,
));
}
let ci_off = CIB_CHUNK_INFO + within_cib * CHUNK_INFO_LEN;
let bitmap_addr = le_u64(&cib, ci_off + CI_BITMAP_ADDR);
if bitmap_addr == 0 {
return Ok(true);
}
let mut bitmap = vec![0u8; block_size];
let offset = bitmap_addr.saturating_mul(block_size as u64);
reader.seek(std::io::SeekFrom::Start(offset))?;
reader.read_exact(&mut bitmap)?;
let bit = (block % blocks_per_chunk) as usize;
let allocated = bitmap
.get(bit / 8)
.is_some_and(|byte| byte & (1u8 << (bit % 8)) != 0);
Ok(!allocated)
}
#[must_use]
pub fn free_runs(bitmap: &[u8], blocks: u64, free_bit_is_one: bool) -> Vec<(u64, u64)> {
let allocated_fill: u8 = if free_bit_is_one { 0x00 } else { 0xFF };
let mut runs = Vec::new();
let mut start: Option<u64> = None;
for i in 0..blocks {
let byte = bitmap
.get((i / 8) as usize)
.copied()
.unwrap_or(allocated_fill);
let bit_set = byte & (1u8 << (i % 8) as u32) != 0;
let is_free = bit_set == free_bit_is_one;
match (is_free, start) {
(true, None) => start = Some(i),
(false, Some(s)) => {
runs.push((s, i - s));
start = None;
}
(true, Some(_)) | (false, None) => {}
}
}
if let Some(s) = start {
runs.push((s, blocks.saturating_sub(s)));
}
runs
}
fn push_free_run(runs: &mut Vec<(u64, u64)>, first: u64, len: u64) {
if len == 0 {
return; }
if let Some(last) = runs.last_mut() {
if last.0.saturating_add(last.1) == first {
last.1 = last.1.saturating_add(len);
return;
}
}
runs.push((first, len));
}
pub fn free_block_runs<R: Read + Seek>(
reader: &mut R,
spaceman_paddr: u64,
block_size: usize,
) -> crate::Result<Vec<(u64, u64)>> {
let sm = read_obj_block(reader, spaceman_paddr, block_size)?;
let blocks_per_chunk = u64::from(le_u32(&sm, SM_BLOCKS_PER_CHUNK));
let chunks_per_cib = u64::from(le_u32(&sm, SM_CHUNKS_PER_CIB));
let block_count = le_u64(&sm, SM_DEV0 + DEV_BLOCK_COUNT);
let chunk_count = le_u64(&sm, SM_DEV0 + DEV_CHUNK_COUNT);
let cib_count = u64::from(le_u32(&sm, SM_DEV0 + DEV_CIB_COUNT));
let cab_count = u64::from(le_u32(&sm, SM_DEV0 + DEV_CAB_COUNT));
let addr_offset = le_u32(&sm, SM_DEV0 + DEV_ADDR_OFFSET) as usize;
let range = |field, value, cap| crate::ApfsError::FieldOutOfRange {
structure: "spaceman_phys",
field,
value,
cap,
};
if blocks_per_chunk == 0 || chunks_per_cib == 0 {
return Err(range("sm_blocks_per_chunk/sm_chunks_per_cib", 0, 1));
}
if cab_count != 0 {
return Err(crate::ApfsError::UnsupportedSpacemanCab { count: cab_count });
}
let mut runs: Vec<(u64, u64)> = Vec::new();
for cib_idx in 0..cib_count {
let cib_paddr = le_u64(&sm, addr_offset + cib_idx as usize * 8);
let cib = read_obj_block(reader, cib_paddr, block_size)?;
let ci_count = le_u32(&cib, CIB_CHUNK_INFO_COUNT) as usize;
for within in 0..ci_count {
let chunk_index = cib_idx.saturating_mul(chunks_per_cib) + within as u64;
if chunk_index >= chunk_count {
break;
}
let chunk_base = chunk_index.saturating_mul(blocks_per_chunk);
if chunk_base >= block_count {
break;
}
let blocks_in_chunk = blocks_per_chunk.min(block_count - chunk_base);
let ci_off = CIB_CHUNK_INFO + within * CHUNK_INFO_LEN;
let bitmap_addr = le_u64(&cib, ci_off + CI_BITMAP_ADDR);
if bitmap_addr == 0 {
push_free_run(&mut runs, chunk_base, blocks_in_chunk);
continue;
}
let mut bitmap = vec![0u8; block_size];
let offset = bitmap_addr.saturating_mul(block_size as u64);
reader.seek(SeekFrom::Start(offset))?;
reader.read_exact(&mut bitmap)?;
for (start, len) in free_runs(&bitmap, blocks_in_chunk, false) {
push_free_run(&mut runs, chunk_base + start, len);
}
}
}
Ok(runs)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
const BS: usize = 4096;
#[allow(clippy::too_many_arguments)]
fn spaceman_image(
blocks_per_chunk: u32,
chunks_per_cib: u32,
block_count: u64,
chunk_count: u64,
cib_count: u32,
cab_count: u32,
addr_offset: u32,
cib0: u64,
) -> Vec<u8> {
let mut img = vec![0u8; BS * 4];
let sm = &mut img[0..BS];
sm[24..28].copy_from_slice(&5u32.to_le_bytes()); sm[SM_BLOCKS_PER_CHUNK..SM_BLOCKS_PER_CHUNK + 4]
.copy_from_slice(&blocks_per_chunk.to_le_bytes());
sm[SM_CHUNKS_PER_CIB..SM_CHUNKS_PER_CIB + 4].copy_from_slice(&chunks_per_cib.to_le_bytes());
let d = SM_DEV0;
sm[d + DEV_BLOCK_COUNT..d + DEV_BLOCK_COUNT + 8]
.copy_from_slice(&block_count.to_le_bytes());
sm[d + DEV_CHUNK_COUNT..d + DEV_CHUNK_COUNT + 8]
.copy_from_slice(&chunk_count.to_le_bytes());
sm[d + DEV_CIB_COUNT..d + DEV_CIB_COUNT + 4].copy_from_slice(&cib_count.to_le_bytes());
sm[d + DEV_CAB_COUNT..d + DEV_CAB_COUNT + 4].copy_from_slice(&cab_count.to_le_bytes());
sm[d + DEV_ADDR_OFFSET..d + DEV_ADDR_OFFSET + 4]
.copy_from_slice(&addr_offset.to_le_bytes());
let ao = addr_offset as usize;
sm[ao..ao + 8].copy_from_slice(&cib0.to_le_bytes());
let cks = crate::object::fletcher64_checksum(&img[0..BS]);
img[0..8].copy_from_slice(&cks.to_le_bytes());
img
}
#[test]
fn read_obj_block_rejects_a_bad_checksum() {
let mut img = vec![0u8; BS];
img[8..16].copy_from_slice(&0x1234u64.to_le_bytes()); let mut r = Cursor::new(img);
let got = read_obj_block(&mut r, 0, BS);
let Err(crate::ApfsError::ChecksumMismatch { block, .. }) = got else {
unreachable!("bad checksum must be ChecksumMismatch, got {got:?}") };
assert_eq!(block, 0x1234);
}
#[test]
fn zero_geometry_is_out_of_range() {
let img = spaceman_image(0, 0, 100, 10, 1, 0, 256, 0);
let mut r = Cursor::new(img);
assert!(matches!(
is_block_free(&mut r, 0, 0, BS),
Err(crate::ApfsError::FieldOutOfRange { .. })
));
}
#[test]
fn block_past_device_is_out_of_range() {
let img = spaceman_image(8, 4, 100, 10, 1, 0, 256, 0);
let mut r = Cursor::new(img);
let got = is_block_free(&mut r, 0, 999, BS);
let Err(crate::ApfsError::FieldOutOfRange { field, value, .. }) = got else {
unreachable!("block OOR ⇒ FieldOutOfRange: {got:?}") };
assert_eq!(field, "block");
assert_eq!(value, 999);
}
#[test]
fn cab_tier_is_unsupported_and_loud() {
let img = spaceman_image(8, 4, 100, 10, 1, 3, 256, 0);
let mut r = Cursor::new(img);
let got = is_block_free(&mut r, 0, 0, BS);
let Err(crate::ApfsError::UnsupportedSpacemanCab { count }) = got else {
unreachable!("CAB tier ⇒ UnsupportedSpacemanCab: {got:?}") };
assert_eq!(count, 3);
}
#[test]
fn chunk_index_past_chunk_count_is_out_of_range() {
let img = spaceman_image(8, 4, 1000, 2, 1, 0, 256, 0);
let mut r = Cursor::new(img);
let got = is_block_free(&mut r, 0, 64, BS);
let Err(crate::ApfsError::FieldOutOfRange { field, .. }) = got else {
unreachable!("chunk_index OOR ⇒ FieldOutOfRange: {got:?}") };
assert_eq!(field, "chunk_index");
}
#[test]
fn cib_index_past_cib_count_is_out_of_range() {
let img = spaceman_image(8, 4, 1000, 100, 1, 0, 256, 0);
let mut r = Cursor::new(img);
let got = is_block_free(&mut r, 0, 32, BS);
let Err(crate::ApfsError::FieldOutOfRange { field, .. }) = got else {
unreachable!("bad cib_index must be FieldOutOfRange, got {got:?}") };
assert_eq!(field, "cib_index");
}
#[test]
fn zero_bitmap_addr_means_whole_chunk_free() {
let mut img = spaceman_image(8, 4, 1000, 10, 1, 0, 256, 1);
{
let cib = &mut img[BS..2 * BS];
cib[CIB_CHUNK_INFO_COUNT..CIB_CHUNK_INFO_COUNT + 4]
.copy_from_slice(&1u32.to_le_bytes());
let cks = crate::object::fletcher64_checksum(cib);
cib[0..8].copy_from_slice(&cks.to_le_bytes());
}
let mut r = Cursor::new(img);
assert!(
is_block_free(&mut r, 0, 0, BS).expect("is_block_free"),
"a zero ci_bitmap_addr marks the whole chunk free"
);
}
#[test]
fn chunk_info_index_past_count_is_out_of_range() {
let mut img = spaceman_image(8, 4, 1000, 10, 1, 0, 256, 1);
{
let cib = &mut img[BS..2 * BS];
let cks = crate::object::fletcher64_checksum(cib);
cib[0..8].copy_from_slice(&cks.to_le_bytes());
}
let mut r = Cursor::new(img);
let got = is_block_free(&mut r, 0, 0, BS);
let Err(crate::ApfsError::FieldOutOfRange { field, .. }) = got else {
unreachable!("chunk_info_index OOR ⇒ FieldOutOfRange: {got:?}") };
assert_eq!(field, "chunk_info_index");
}
#[test]
fn free_runs_apfs_polarity_clear_bit_is_free() {
let runs = free_runs(&[0x1E], 8, false);
assert_eq!(runs, vec![(0, 1), (5, 3)]);
}
#[test]
fn free_runs_inverse_polarity_set_bit_is_free() {
let runs = free_runs(&[0x1E], 8, true);
assert_eq!(runs, vec![(1, 4)]);
}
#[test]
fn free_runs_honors_blocks_cap_and_ignores_padding() {
let runs = free_runs(&[0x00, 0x00], 3, false);
assert_eq!(runs, vec![(0, 3)]);
}
#[test]
fn free_runs_missing_bytes_are_allocated_never_fabricated() {
let runs = free_runs(&[0x00], 16, false);
assert_eq!(runs, vec![(0, 8)]);
let runs = free_runs(&[0xFF], 16, true);
assert_eq!(runs, vec![(0, 8)]);
}
#[test]
fn free_runs_empty_when_all_allocated() {
assert!(free_runs(&[0xFF, 0xFF], 16, false).is_empty());
}
fn two_chunk_image(chunk0_bitmap_byte: u8) -> Vec<u8> {
let mut img = spaceman_image(8, 4, 16, 2, 1, 0, 256, 1);
{
let cib = &mut img[BS..2 * BS];
cib[CIB_CHUNK_INFO_COUNT..CIB_CHUNK_INFO_COUNT + 4]
.copy_from_slice(&2u32.to_le_bytes());
let ci0 = CIB_CHUNK_INFO + CI_BITMAP_ADDR;
cib[ci0..ci0 + 8].copy_from_slice(&2u64.to_le_bytes());
let cks = crate::object::fletcher64_checksum(cib);
cib[0..8].copy_from_slice(&cks.to_le_bytes());
}
img[2 * BS] = chunk0_bitmap_byte;
img
}
#[test]
fn free_block_runs_merges_across_chunk_boundary() {
let mut r = Cursor::new(two_chunk_image(0x1E));
let runs = free_block_runs(&mut r, 0, BS).expect("free_block_runs");
assert_eq!(runs, vec![(0, 1), (5, 11)]);
}
#[test]
fn free_block_runs_all_allocated_chunk0_keeps_chunk1_free() {
let mut r = Cursor::new(two_chunk_image(0xFF));
let runs = free_block_runs(&mut r, 0, BS).expect("free_block_runs");
assert_eq!(runs, vec![(8, 8)]);
}
#[test]
fn free_block_runs_rejects_cab_tier_loud() {
let img = spaceman_image(8, 4, 16, 2, 1, 3, 256, 1);
let mut r = Cursor::new(img);
let got = free_block_runs(&mut r, 0, BS);
let Err(crate::ApfsError::UnsupportedSpacemanCab { count }) = got else {
unreachable!("CAB tier ⇒ UnsupportedSpacemanCab: {got:?}") };
assert_eq!(count, 3);
}
#[test]
fn free_block_runs_rejects_zero_geometry_loud() {
let img = spaceman_image(0, 0, 16, 2, 1, 0, 256, 1);
let mut r = Cursor::new(img);
assert!(matches!(
free_block_runs(&mut r, 0, BS),
Err(crate::ApfsError::FieldOutOfRange { .. })
));
}
fn overcounted_cib_image(block_count: u64, chunk_count: u64, chunk_info_count: u32) -> Vec<u8> {
let mut img = spaceman_image(8, 4, block_count, chunk_count, 1, 0, 256, 1);
{
let cib = &mut img[BS..2 * BS];
cib[CIB_CHUNK_INFO_COUNT..CIB_CHUNK_INFO_COUNT + 4]
.copy_from_slice(&chunk_info_count.to_le_bytes());
let ci0 = CIB_CHUNK_INFO + CI_BITMAP_ADDR;
cib[ci0..ci0 + 8].copy_from_slice(&2u64.to_le_bytes());
let cks = crate::object::fletcher64_checksum(cib);
cib[0..8].copy_from_slice(&cks.to_le_bytes());
}
img[2 * BS] = 0x00; img
}
#[test]
fn free_block_runs_stops_when_cib_overcounts_chunks() {
let mut r = Cursor::new(overcounted_cib_image(8, 1, 2));
let runs = free_block_runs(&mut r, 0, BS).expect("free_block_runs");
assert_eq!(runs, vec![(0, 8)]);
}
#[test]
fn free_block_runs_stops_when_chunk_base_exceeds_block_count() {
let mut r = Cursor::new(overcounted_cib_image(8, 2, 2));
let runs = free_block_runs(&mut r, 0, BS).expect("free_block_runs");
assert_eq!(runs, vec![(0, 8)]);
}
}