use crate::Result;
use super::checksum::fletcher64;
use super::obj::{OBJECT_TYPE_BTREE, OBJECT_TYPE_BTREE_NODE};
pub(super) const OBJECT_TYPE_SPACEMAN: u32 = 0x0000_0005;
pub(super) const OBJECT_TYPE_SPACEMAN_CIB: u32 = 0x0000_0007;
pub(super) const OBJECT_TYPE_SPACEMAN_FREE_QUEUE: u32 = 0x0000_0009;
const OBJ_PHYSICAL: u32 = 0x4000_0000;
const OBJ_EPHEMERAL: u32 = 0x8000_0000;
const SM_FLAG_VERSIONED: u32 = 0x0000_0001;
const SPACEMAN_IP_BM_TX_MULTIPLIER: u32 = 16;
const SPACEMAN_IP_BM_INDEX_INVALID: u16 = 0xffff;
const SM_ALLOCZONE_NUM_PREVIOUS_BOUNDARIES: usize = 7;
const SM_ALLOCZONE_INVALID_END_BOUNDARY: u64 = 0;
const SM_DATAZONE_ALLOCZONE_COUNT: usize = 8;
const SD_COUNT: usize = 2;
const SD_MAIN: usize = 0;
const ALLOC_ZONE_INFO_SIZE: usize = 16 + SM_ALLOCZONE_NUM_PREVIOUS_BOUNDARIES * 16 + 8;
const DATAZONE_SIZE: usize = SD_COUNT * SM_DATAZONE_ALLOCZONE_COUNT * ALLOC_ZONE_INFO_SIZE;
const SM_DATAZONE_OFFSET: usize = 344;
const SM_CIB_ADDR_OFFSET: u32 = (SM_DATAZONE_OFFSET + DATAZONE_SIZE).next_multiple_of(8) as u32;
pub(super) const DEFAULT_IP_BLOCK_COUNT: u64 = 16;
pub(super) fn blocks_per_chunk(block_size: usize) -> u64 {
(block_size as u64) * 8
}
pub(super) fn chunks_per_cib(block_size: usize) -> u32 {
((block_size - 40) / 32) as u32
}
pub(super) struct SpacemanLayout {
pub block_size: usize,
pub total_blocks: u64,
pub xid: u64,
pub spaceman_oid: u64,
pub cib_paddr: u64,
pub bitmap_paddrs: Vec<u64>,
pub used_ranges: Vec<(u64, u64)>,
pub ip_base: u64,
pub ip_block_count: u64,
pub ip_bm_base: u64,
pub ip_bm_size_in_blocks: u32,
pub free_queue_paddrs: [u64; 3],
}
pub(super) struct EmittedSpaceman {
pub spaceman_block: Vec<u8>,
pub cib_block: Vec<u8>,
pub bitmap_blocks: Vec<Vec<u8>>,
pub ip_bm_block: Vec<u8>,
pub free_queue_blocks: [Vec<u8>; 3],
}
pub(super) fn build_spaceman(layout: &SpacemanLayout) -> Result<EmittedSpaceman> {
let bs = layout.block_size;
if bs < 512 || !bs.is_power_of_two() {
return Err(crate::Error::InvalidArgument(format!(
"apfs spaceman: implausible block size {bs}"
)));
}
let bpc = blocks_per_chunk(bs);
let chunks: u64 = layout.total_blocks.div_ceil(bpc);
let cpc = chunks_per_cib(bs);
if chunks as u32 > cpc {
return Err(crate::Error::Unsupported(format!(
"apfs spaceman: {chunks} chunks would overflow a single CIB \
(max {cpc}); CAB layer not implemented"
)));
}
if layout.bitmap_paddrs.len() != chunks as usize {
return Err(crate::Error::InvalidArgument(format!(
"apfs spaceman: caller supplied {} bitmap paddrs for {} chunks",
layout.bitmap_paddrs.len(),
chunks,
)));
}
if layout.ip_block_count == 0
|| layout.ip_block_count % SPACEMAN_IP_BM_TX_MULTIPLIER as u64 != 0
{
return Err(crate::Error::InvalidArgument(format!(
"apfs spaceman: ip_block_count {} must be a non-zero multiple of \
SPACEMAN_IP_BM_TX_MULTIPLIER ({})",
layout.ip_block_count, SPACEMAN_IP_BM_TX_MULTIPLIER
)));
}
if layout.ip_bm_size_in_blocks == 0 {
return Err(crate::Error::InvalidArgument(
"apfs spaceman: ip_bm_size_in_blocks must be ≥ 1".into(),
));
}
let mut bitmap_blocks: Vec<Vec<u8>> = Vec::with_capacity(chunks as usize);
let mut free_total: u64 = 0;
let mut free_per_chunk: Vec<u32> = Vec::with_capacity(chunks as usize);
for chunk_idx in 0..chunks {
let mut bmap = vec![0u8; bs];
let chunk_start = chunk_idx * bpc;
let chunk_end = ((chunk_idx + 1) * bpc).min(layout.total_blocks);
let chunk_blocks = (chunk_end - chunk_start) as u32;
for &(rs, re) in &layout.used_ranges {
let lo = rs.max(chunk_start);
let hi = re.min(chunk_end);
for b in lo..hi {
let bit = (b - chunk_start) as usize;
bmap[bit / 8] |= 1 << (bit % 8);
}
}
for bit in chunk_blocks..(bpc as u32) {
let bit = bit as usize;
bmap[bit / 8] |= 1 << (bit % 8);
}
let mut used: u32 = 0;
for &(rs, re) in &layout.used_ranges {
let lo = rs.max(chunk_start);
let hi = re.min(chunk_end);
if hi > lo {
used += (hi - lo) as u32;
}
}
let free = chunk_blocks - used;
free_per_chunk.push(free);
free_total += free as u64;
bitmap_blocks.push(bmap);
}
let mut cib = vec![0u8; bs];
cib[8..16].copy_from_slice(&layout.cib_paddr.to_le_bytes());
cib[16..24].copy_from_slice(&layout.xid.to_le_bytes());
cib[24..28].copy_from_slice(&(OBJECT_TYPE_SPACEMAN_CIB | OBJ_PHYSICAL).to_le_bytes());
cib[32..36].copy_from_slice(&0u32.to_le_bytes());
cib[36..40].copy_from_slice(&(chunks as u32).to_le_bytes());
let chunk_info_base = 40usize;
for (chunk_idx, (&free_in_chunk, &bmap_paddr)) in free_per_chunk
.iter()
.zip(layout.bitmap_paddrs.iter())
.enumerate()
{
let off = chunk_info_base + chunk_idx * 32;
let chunk_start = (chunk_idx as u64) * bpc;
let chunk_end = (chunk_start + bpc).min(layout.total_blocks);
let chunk_blocks = (chunk_end - chunk_start) as u32;
cib[off..off + 8].copy_from_slice(&layout.xid.to_le_bytes());
cib[off + 8..off + 16].copy_from_slice(&chunk_start.to_le_bytes());
cib[off + 16..off + 20].copy_from_slice(&chunk_blocks.to_le_bytes());
cib[off + 20..off + 24].copy_from_slice(&free_in_chunk.to_le_bytes());
cib[off + 24..off + 32].copy_from_slice(&bmap_paddr.to_le_bytes());
}
sign_block(&mut cib);
let mut sm = vec![0u8; bs];
sm[8..16].copy_from_slice(&layout.spaceman_oid.to_le_bytes());
sm[16..24].copy_from_slice(&layout.xid.to_le_bytes());
sm[24..28].copy_from_slice(&(OBJECT_TYPE_SPACEMAN | OBJ_EPHEMERAL).to_le_bytes());
sm[32..36].copy_from_slice(&(bs as u32).to_le_bytes());
sm[36..40].copy_from_slice(&(bpc as u32).to_le_bytes());
sm[40..44].copy_from_slice(&cpc.to_le_bytes());
let cibs_per_cab = ((bs - 40) / 8) as u32;
sm[44..48].copy_from_slice(&cibs_per_cab.to_le_bytes());
sm[48..56].copy_from_slice(&layout.total_blocks.to_le_bytes()); sm[56..64].copy_from_slice(&chunks.to_le_bytes()); sm[64..68].copy_from_slice(&1u32.to_le_bytes()); sm[68..72].copy_from_slice(&0u32.to_le_bytes()); sm[72..80].copy_from_slice(&free_total.to_le_bytes()); let cib_addr_off: u32 = SM_CIB_ADDR_OFFSET;
sm[80..84].copy_from_slice(&cib_addr_off.to_le_bytes());
sm[144..148].copy_from_slice(&SM_FLAG_VERSIONED.to_le_bytes());
sm[148..152].copy_from_slice(&SPACEMAN_IP_BM_TX_MULTIPLIER.to_le_bytes());
sm[152..160].copy_from_slice(&layout.ip_block_count.to_le_bytes());
sm[160..164].copy_from_slice(&layout.ip_bm_size_in_blocks.to_le_bytes());
sm[164..168].copy_from_slice(&SPACEMAN_IP_BM_TX_MULTIPLIER.to_le_bytes());
sm[168..176].copy_from_slice(&layout.ip_bm_base.to_le_bytes());
sm[176..184].copy_from_slice(&layout.ip_base.to_le_bytes());
let sfq_base = 200usize;
for i in 0..3 {
let off = sfq_base + i * 40;
sm[off + 8..off + 16].copy_from_slice(&layout.free_queue_paddrs[i].to_le_bytes());
}
sm[320..322].copy_from_slice(&0u16.to_le_bytes()); sm[322..324].copy_from_slice(&0u16.to_le_bytes()); let xid_off: u32 = 0; let bm_off: u32 = 0; let next_off: u32 = 0; sm[324..328].copy_from_slice(&xid_off.to_le_bytes());
sm[328..332].copy_from_slice(&bm_off.to_le_bytes());
sm[332..336].copy_from_slice(&next_off.to_le_bytes());
sm[336..340].copy_from_slice(&1u32.to_le_bytes());
let main_zone0_off =
SM_DATAZONE_OFFSET + (SD_MAIN * SM_DATAZONE_ALLOCZONE_COUNT) * ALLOC_ZONE_INFO_SIZE;
sm[main_zone0_off..main_zone0_off + 8].copy_from_slice(&0u64.to_le_bytes()); sm[main_zone0_off + 8..main_zone0_off + 16].copy_from_slice(&layout.total_blocks.to_le_bytes()); sm[main_zone0_off + 130..main_zone0_off + 132]
.copy_from_slice(&(SM_ALLOCZONE_NUM_PREVIOUS_BOUNDARIES as u16).to_le_bytes());
let _ = SM_ALLOCZONE_INVALID_END_BOUNDARY;
let struct_size = (cib_addr_off as usize) + 8 * (chunks as usize);
sm[340..344].copy_from_slice(&(struct_size as u32).to_le_bytes());
let off = cib_addr_off as usize;
if off + 8 > bs {
return Err(crate::Error::Unsupported(format!(
"apfs spaceman: block size {bs} too small for inline CIB addr array \
(cib_addr_off={off})"
)));
}
sm[off..off + 8].copy_from_slice(&layout.cib_paddr.to_le_bytes());
sign_block(&mut sm);
let mut free_queue_blocks: [Vec<u8>; 3] = [Vec::new(), Vec::new(), Vec::new()];
for (i, &paddr) in layout.free_queue_paddrs.iter().enumerate() {
free_queue_blocks[i] = build_empty_free_queue_root(bs, paddr, layout.xid)?;
}
let ip_bm_block = vec![0u8; bs];
Ok(EmittedSpaceman {
spaceman_block: sm,
cib_block: cib,
bitmap_blocks,
ip_bm_block,
free_queue_blocks,
})
}
fn build_empty_free_queue_root(bs: usize, paddr: u64, xid: u64) -> Result<Vec<u8>> {
if bs < 56 + 40 {
return Err(crate::Error::Unsupported(format!(
"apfs spaceman: block size {bs} too small for empty free-queue root"
)));
}
const BTNODE_FLAGS: u16 = 0x0001 | 0x0002 | 0x0004;
const BTREE_INFO_FLAGS: u32 = 0x0000_0010 | 0x0000_0004;
let mut block = vec![0u8; bs];
block[8..16].copy_from_slice(&paddr.to_le_bytes());
block[16..24].copy_from_slice(&xid.to_le_bytes());
block[24..28].copy_from_slice(&(OBJECT_TYPE_BTREE | OBJ_PHYSICAL).to_le_bytes());
block[28..32].copy_from_slice(&OBJECT_TYPE_SPACEMAN_FREE_QUEUE.to_le_bytes());
block[32..34].copy_from_slice(&BTNODE_FLAGS.to_le_bytes());
block[34..36].copy_from_slice(&0u16.to_le_bytes()); block[36..40].copy_from_slice(&0u32.to_le_bytes());
block[40..42].copy_from_slice(&0u16.to_le_bytes()); block[42..44].copy_from_slice(&0u16.to_le_bytes()); let free_len = (bs - 40 - 56) as u16;
block[44..46].copy_from_slice(&0u16.to_le_bytes()); block[46..48].copy_from_slice(&free_len.to_le_bytes()); block[48..50].copy_from_slice(&0xffffu16.to_le_bytes());
block[50..52].copy_from_slice(&0u16.to_le_bytes());
block[52..54].copy_from_slice(&0xffffu16.to_le_bytes());
block[54..56].copy_from_slice(&0u16.to_le_bytes());
let info_off = bs - 40;
block[info_off..info_off + 4].copy_from_slice(&BTREE_INFO_FLAGS.to_le_bytes());
block[info_off + 4..info_off + 8].copy_from_slice(&(bs as u32).to_le_bytes()); block[info_off + 8..info_off + 12].copy_from_slice(&16u32.to_le_bytes()); block[info_off + 12..info_off + 16].copy_from_slice(&8u32.to_le_bytes()); block[info_off + 32..info_off + 40].copy_from_slice(&1u64.to_le_bytes());
let _ = (OBJECT_TYPE_BTREE_NODE, SPACEMAN_IP_BM_INDEX_INVALID);
sign_block(&mut block);
Ok(block)
}
fn sign_block(buf: &mut [u8]) {
let cksum = fletcher64(buf);
buf[0..8].copy_from_slice(&cksum.to_le_bytes());
}
#[cfg(test)]
#[derive(Debug)]
pub(super) struct DecodedSpaceman {
pub block_size: u32,
pub blocks_per_chunk: u32,
pub main_block_count: u64,
pub main_chunk_count: u64,
pub main_cib_count: u32,
pub main_cab_count: u32,
pub main_free_count: u64,
pub cib_paddr: u64,
pub flags: u32,
pub ip_bm_tx_multiplier: u32,
pub ip_block_count: u64,
pub ip_bm_size_in_blocks: u32,
pub ip_bm_block_count: u32,
pub ip_bm_base: u64,
pub ip_base: u64,
pub sfq_tree_oids: [u64; 3],
pub datazone_main_zone0_start: u64,
pub datazone_main_zone0_end: u64,
}
#[cfg(test)]
pub(super) fn decode_spaceman(buf: &[u8]) -> Result<DecodedSpaceman> {
if buf.len() < SM_DATAZONE_OFFSET + DATAZONE_SIZE {
return Err(crate::Error::InvalidImage(
"apfs spaceman: block too short".into(),
));
}
let otype = u32::from_le_bytes(buf[24..28].try_into().unwrap());
if otype & 0x0000_ffff != OBJECT_TYPE_SPACEMAN {
return Err(crate::Error::InvalidImage(format!(
"apfs spaceman: unexpected object type {otype:#x}"
)));
}
let block_size = u32::from_le_bytes(buf[32..36].try_into().unwrap());
let blocks_per_chunk = u32::from_le_bytes(buf[36..40].try_into().unwrap());
let main_block_count = u64::from_le_bytes(buf[48..56].try_into().unwrap());
let main_chunk_count = u64::from_le_bytes(buf[56..64].try_into().unwrap());
let main_cib_count = u32::from_le_bytes(buf[64..68].try_into().unwrap());
let main_cab_count = u32::from_le_bytes(buf[68..72].try_into().unwrap());
let main_free_count = u64::from_le_bytes(buf[72..80].try_into().unwrap());
let cib_addr_off = u32::from_le_bytes(buf[80..84].try_into().unwrap()) as usize;
if cib_addr_off + 8 > buf.len() {
return Err(crate::Error::InvalidImage(format!(
"apfs spaceman: sm_addr_offset {cib_addr_off} past block end"
)));
}
let cib_paddr = u64::from_le_bytes(buf[cib_addr_off..cib_addr_off + 8].try_into().unwrap());
let flags = u32::from_le_bytes(buf[144..148].try_into().unwrap());
let ip_bm_tx_multiplier = u32::from_le_bytes(buf[148..152].try_into().unwrap());
let ip_block_count = u64::from_le_bytes(buf[152..160].try_into().unwrap());
let ip_bm_size_in_blocks = u32::from_le_bytes(buf[160..164].try_into().unwrap());
let ip_bm_block_count = u32::from_le_bytes(buf[164..168].try_into().unwrap());
let ip_bm_base = u64::from_le_bytes(buf[168..176].try_into().unwrap());
let ip_base = u64::from_le_bytes(buf[176..184].try_into().unwrap());
let mut sfq_tree_oids = [0u64; 3];
for (i, slot) in sfq_tree_oids.iter_mut().enumerate() {
let off = 200 + i * 40 + 8;
*slot = u64::from_le_bytes(buf[off..off + 8].try_into().unwrap());
}
let dz_main_zone0 =
SM_DATAZONE_OFFSET + (SD_MAIN * SM_DATAZONE_ALLOCZONE_COUNT) * ALLOC_ZONE_INFO_SIZE;
let datazone_main_zone0_start =
u64::from_le_bytes(buf[dz_main_zone0..dz_main_zone0 + 8].try_into().unwrap());
let datazone_main_zone0_end = u64::from_le_bytes(
buf[dz_main_zone0 + 8..dz_main_zone0 + 16]
.try_into()
.unwrap(),
);
Ok(DecodedSpaceman {
block_size,
blocks_per_chunk,
main_block_count,
main_chunk_count,
main_cib_count,
main_cab_count,
main_free_count,
cib_paddr,
flags,
ip_bm_tx_multiplier,
ip_block_count,
ip_bm_size_in_blocks,
ip_bm_block_count,
ip_bm_base,
ip_base,
sfq_tree_oids,
datazone_main_zone0_start,
datazone_main_zone0_end,
})
}
#[cfg(test)]
pub(super) fn decode_cib_entries(buf: &[u8]) -> Result<Vec<DecodedChunkInfo>> {
if buf.len() < 40 {
return Err(crate::Error::InvalidImage(
"apfs CIB: block too short".into(),
));
}
let otype = u32::from_le_bytes(buf[24..28].try_into().unwrap());
if otype & 0x0000_ffff != OBJECT_TYPE_SPACEMAN_CIB {
return Err(crate::Error::InvalidImage(format!(
"apfs CIB: unexpected object type {otype:#x}"
)));
}
let count = u32::from_le_bytes(buf[36..40].try_into().unwrap()) as usize;
let mut out = Vec::with_capacity(count);
for i in 0..count {
let off = 40 + i * 32;
if off + 32 > buf.len() {
return Err(crate::Error::InvalidImage(
"apfs CIB: chunk_info_t past block end".into(),
));
}
out.push(DecodedChunkInfo {
addr: u64::from_le_bytes(buf[off + 8..off + 16].try_into().unwrap()),
block_count: u32::from_le_bytes(buf[off + 16..off + 20].try_into().unwrap()),
free_count: u32::from_le_bytes(buf[off + 20..off + 24].try_into().unwrap()),
bitmap_addr: u64::from_le_bytes(buf[off + 24..off + 32].try_into().unwrap()),
});
}
Ok(out)
}
#[cfg(test)]
#[derive(Debug)]
pub(super) struct DecodedChunkInfo {
pub addr: u64,
pub block_count: u32,
pub free_count: u32,
pub bitmap_addr: u64,
}
#[cfg(test)]
pub(super) fn count_used_bits(bmap: &[u8], chunk_blocks: u32) -> u32 {
let mut n: u32 = 0;
for bit in 0..chunk_blocks as usize {
if bmap[bit / 8] & (1 << (bit % 8)) != 0 {
n += 1;
}
}
n
}