use std::io::{Read, Seek};
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)
}
#[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");
}
}