use super::obj::{OBJECT_TYPE_NX_SUPERBLOCK, ObjPhys};
pub const NX_MAGIC: u32 = 0x4253_584e;
pub const APFS_MAGIC: u32 = 0x4253_5041;
pub const NX_MAX_FILE_SYSTEMS: usize = 100;
#[derive(Debug, Clone)]
pub struct NxSuperblock {
pub obj: ObjPhys,
pub magic: u32,
pub block_size: u32,
pub block_count: u64,
pub features: u64,
pub readonly_compatible_features: u64,
pub incompatible_features: u64,
pub uuid: [u8; 16],
pub next_oid: u64,
pub next_xid: u64,
pub xp_desc_blocks: u32,
pub xp_desc_base: u64,
pub xp_desc_index: u32,
pub xp_desc_len: u32,
pub omap_oid: u64,
pub max_file_systems: u32,
pub fs_oid: [u64; NX_MAX_FILE_SYSTEMS],
}
impl NxSuperblock {
pub const MIN_SIZE: usize = 984;
pub fn decode(buf: &[u8]) -> crate::Result<Self> {
if buf.len() < Self::MIN_SIZE {
return Err(crate::Error::InvalidImage(format!(
"apfs: nx_superblock buffer too short ({} < {})",
buf.len(),
Self::MIN_SIZE
)));
}
let obj = ObjPhys::decode(buf)?;
if obj.obj_type() != OBJECT_TYPE_NX_SUPERBLOCK {
return Err(crate::Error::InvalidImage(format!(
"apfs: o_type {:#x} is not NX_SUPERBLOCK",
obj.obj_type()
)));
}
let magic = u32::from_le_bytes(buf[32..36].try_into().unwrap());
if magic != NX_MAGIC {
return Err(crate::Error::InvalidImage(format!(
"apfs: nx_magic {magic:#010x} != 'NXSB'"
)));
}
let block_size = u32::from_le_bytes(buf[36..40].try_into().unwrap());
let block_count = u64::from_le_bytes(buf[40..48].try_into().unwrap());
let features = u64::from_le_bytes(buf[48..56].try_into().unwrap());
let ro_compat = u64::from_le_bytes(buf[56..64].try_into().unwrap());
let incompat = u64::from_le_bytes(buf[64..72].try_into().unwrap());
let mut uuid = [0u8; 16];
uuid.copy_from_slice(&buf[72..88]);
let next_oid = u64::from_le_bytes(buf[88..96].try_into().unwrap());
let next_xid = u64::from_le_bytes(buf[96..104].try_into().unwrap());
let xp_desc_blocks = u32::from_le_bytes(buf[104..108].try_into().unwrap());
let xp_desc_base = u64::from_le_bytes(buf[112..120].try_into().unwrap());
let xp_desc_index = u32::from_le_bytes(buf[136..140].try_into().unwrap());
let xp_desc_len = u32::from_le_bytes(buf[140..144].try_into().unwrap());
let omap_oid = u64::from_le_bytes(buf[160..168].try_into().unwrap());
let max_file_systems = u32::from_le_bytes(buf[180..184].try_into().unwrap());
let mut fs_oid = [0u64; NX_MAX_FILE_SYSTEMS];
for (i, slot) in fs_oid.iter_mut().enumerate() {
let off = 184 + i * 8;
*slot = u64::from_le_bytes(buf[off..off + 8].try_into().unwrap());
}
Ok(Self {
obj,
magic,
block_size,
block_count,
features,
readonly_compatible_features: ro_compat,
incompatible_features: incompat,
uuid,
next_oid,
next_xid,
xp_desc_blocks,
xp_desc_base,
xp_desc_index,
xp_desc_len,
omap_oid,
max_file_systems,
fs_oid,
})
}
}
#[derive(Debug, Clone)]
pub struct ApfsSuperblock {
pub obj: ObjPhys,
pub magic: u32,
pub fs_index: u32,
pub features: u64,
pub readonly_compatible_features: u64,
pub incompatible_features: u64,
pub root_tree_type: u32,
pub extentref_tree_type: u32,
pub snap_meta_tree_type: u32,
pub omap_oid: u64,
pub root_tree_oid: u64,
pub extentref_tree_oid: u64,
pub snap_meta_tree_oid: u64,
pub num_files: u64,
pub num_directories: u64,
pub num_symlinks: u64,
pub vol_uuid: [u8; 16],
pub fs_flags: u64,
pub volname: String,
}
impl ApfsSuperblock {
pub const MIN_SIZE: usize = 960;
pub fn decode(buf: &[u8]) -> crate::Result<Self> {
if buf.len() < Self::MIN_SIZE {
return Err(crate::Error::InvalidImage(format!(
"apfs: apfs_superblock buffer too short ({} < {})",
buf.len(),
Self::MIN_SIZE
)));
}
let obj = ObjPhys::decode(buf)?;
let magic = u32::from_le_bytes(buf[32..36].try_into().unwrap());
if magic != APFS_MAGIC {
return Err(crate::Error::InvalidImage(format!(
"apfs: apfs_magic {magic:#010x} != 'APSB'"
)));
}
let fs_index = u32::from_le_bytes(buf[36..40].try_into().unwrap());
let features = u64::from_le_bytes(buf[40..48].try_into().unwrap());
let ro_compat = u64::from_le_bytes(buf[48..56].try_into().unwrap());
let incompat = u64::from_le_bytes(buf[56..64].try_into().unwrap());
let root_tree_type = u32::from_le_bytes(buf[116..120].try_into().unwrap());
let extentref_tree_type = u32::from_le_bytes(buf[120..124].try_into().unwrap());
let snap_meta_tree_type = u32::from_le_bytes(buf[124..128].try_into().unwrap());
let omap_oid = u64::from_le_bytes(buf[128..136].try_into().unwrap());
let root_tree_oid = u64::from_le_bytes(buf[136..144].try_into().unwrap());
let extentref_tree_oid = u64::from_le_bytes(buf[144..152].try_into().unwrap());
let snap_meta_tree_oid = u64::from_le_bytes(buf[152..160].try_into().unwrap());
let num_files = u64::from_le_bytes(buf[184..192].try_into().unwrap());
let num_directories = u64::from_le_bytes(buf[192..200].try_into().unwrap());
let num_symlinks = u64::from_le_bytes(buf[200..208].try_into().unwrap());
let mut vol_uuid = [0u8; 16];
vol_uuid.copy_from_slice(&buf[240..256]);
let fs_flags = u64::from_le_bytes(buf[264..272].try_into().unwrap());
let raw = &buf[704..704 + 256.min(buf.len() - 704)];
let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
let volname = String::from_utf8_lossy(&raw[..end]).into_owned();
Ok(Self {
obj,
magic,
fs_index,
features,
readonly_compatible_features: ro_compat,
incompatible_features: incompat,
root_tree_type,
extentref_tree_type,
snap_meta_tree_type,
omap_oid,
root_tree_oid,
extentref_tree_oid,
snap_meta_tree_oid,
num_files,
num_directories,
num_symlinks,
vol_uuid,
fs_flags,
volname,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_nxsb_minimal() {
let mut buf = vec![0u8; 4096];
buf[24..28].copy_from_slice(&OBJECT_TYPE_NX_SUPERBLOCK.to_le_bytes());
buf[32..36].copy_from_slice(&NX_MAGIC.to_le_bytes());
buf[36..40].copy_from_slice(&4096u32.to_le_bytes()); buf[40..48].copy_from_slice(&1234u64.to_le_bytes()); buf[104..108].copy_from_slice(&8u32.to_le_bytes()); buf[112..120].copy_from_slice(&1u64.to_le_bytes()); buf[136..140].copy_from_slice(&2u32.to_le_bytes()); buf[140..144].copy_from_slice(&3u32.to_le_bytes()); buf[160..168].copy_from_slice(&0xfeu64.to_le_bytes()); buf[180..184].copy_from_slice(&1u32.to_le_bytes()); buf[184..192].copy_from_slice(&0x4242u64.to_le_bytes());
let sb = NxSuperblock::decode(&buf).unwrap();
assert_eq!(sb.magic, NX_MAGIC);
assert_eq!(sb.block_size, 4096);
assert_eq!(sb.block_count, 1234);
assert_eq!(sb.xp_desc_blocks, 8);
assert_eq!(sb.xp_desc_base, 1);
assert_eq!(sb.xp_desc_index, 2);
assert_eq!(sb.xp_desc_len, 3);
assert_eq!(sb.omap_oid, 0xfe);
assert_eq!(sb.max_file_systems, 1);
assert_eq!(sb.fs_oid[0], 0x4242);
assert_eq!(sb.fs_oid[1], 0);
}
#[test]
fn decode_nxsb_rejects_bad_magic() {
let mut buf = vec![0u8; 4096];
buf[24..28].copy_from_slice(&OBJECT_TYPE_NX_SUPERBLOCK.to_le_bytes());
buf[32..36].copy_from_slice(&0xdead_beefu32.to_le_bytes());
assert!(NxSuperblock::decode(&buf).is_err());
}
#[test]
fn decode_apsb_minimal() {
let mut buf = vec![0u8; 4096];
buf[32..36].copy_from_slice(&APFS_MAGIC.to_le_bytes());
buf[128..136].copy_from_slice(&0x10u64.to_le_bytes()); buf[136..144].copy_from_slice(&0x20u64.to_le_bytes()); let name = b"Macintosh HD";
buf[704..704 + name.len()].copy_from_slice(name);
let sb = ApfsSuperblock::decode(&buf).unwrap();
assert_eq!(sb.magic, APFS_MAGIC);
assert_eq!(sb.omap_oid, 0x10);
assert_eq!(sb.root_tree_oid, 0x20);
assert_eq!(sb.volname, "Macintosh HD");
}
}