use crate::{Error, Result};
pub(crate) const MIN_BLOCK_SIZE: u32 = 4096;
pub(crate) const MAX_BLOCK_SIZE: u32 = 65_536;
pub(crate) const DEFAULT_BLOCK_SIZE: usize = MIN_BLOCK_SIZE as usize;
pub(crate) const NX_MAGIC: &[u8; 4] = b"NXSB";
pub(crate) const APFS_MAGIC: &[u8; 4] = b"APSB";
pub(crate) const OBJECT_TYPE_MASK: u32 = 0x0000_FFFF;
pub(crate) const OBJ_STORAGETYPE_MASK: u32 = 0xC000_0000;
pub(crate) const OBJECT_TYPE_NX_SUPERBLOCK: u32 = 0x0000_0001;
pub(crate) const OBJECT_TYPE_BTREE: u32 = 0x0000_0002;
pub(crate) const OBJECT_TYPE_BTREE_NODE: u32 = 0x0000_0003;
pub(crate) const OBJECT_TYPE_OMAP: u32 = 0x0000_000B;
pub(crate) const OBJECT_TYPE_CHECKPOINT_MAP: u32 = 0x0000_000C;
pub(crate) const OBJECT_TYPE_FS: u32 = 0x0000_000D;
pub(crate) const OBJECT_TYPE_FSTREE: u32 = 0x0000_000E;
pub(crate) const OBJECT_TYPE_BLOCKREFTREE: u32 = 0x0000_000F;
pub(crate) const OBJECT_TYPE_SNAP_META_TREE: u32 = 0x0000_0010;
pub(crate) const OBJECT_TYPE_NX_REAPER: u32 = 0x0000_0011;
pub(crate) const OBJECT_TYPE_OMAP_SNAPSHOT: u32 = 0x0000_0013;
pub(crate) const OBJECT_TYPE_EFI_JUMPSTART: u32 = 0x0000_0014;
pub(crate) const OBJECT_TYPE_FUSION_MIDDLE_TREE: u32 = 0x0000_0015;
pub(crate) const OBJECT_TYPE_NX_FUSION_WBC: u32 = 0x0000_0016;
pub(crate) const OBJECT_TYPE_NX_FUSION_WBC_LIST: u32 = 0x0000_0017;
pub(crate) const OBJECT_TYPE_ER_STATE: u32 = 0x0000_0018;
pub(crate) const OBJECT_TYPE_GBITMAP: u32 = 0x0000_0019;
pub(crate) const OBJECT_TYPE_GBITMAP_TREE: u32 = 0x0000_001A;
pub(crate) const OBJECT_TYPE_GBITMAP_BLOCK: u32 = 0x0000_001B;
pub(crate) const OBJECT_TYPE_ER_RECOVERY_BLOCK: u32 = 0x0000_001C;
pub(crate) const OBJECT_TYPE_SNAP_META_EXT: u32 = 0x0000_001D;
pub(crate) const OBJECT_TYPE_INTEGRITY_META: u32 = 0x0000_001E;
pub(crate) const OBJECT_TYPE_FEXT_TREE: u32 = 0x0000_001F;
pub(crate) const OBJECT_TYPE_RESERVED_20: u32 = 0x0000_0020;
pub(crate) const OBJECT_TYPE_TEST: u32 = 0x0000_00FF;
pub(crate) const APFS_OBJECT_TYPE_CONTAINER_KEYBAG: u32 = 0x6B65_7973;
pub(crate) const APFS_OBJECT_TYPE_VOLUME_KEYBAG: u32 = 0x7265_6373;
pub(crate) const APFS_OBJECT_TYPE_MEDIA_KEYBAG: u32 = 0x6D6B_6579;
pub(crate) const OBJ_VIRTUAL: u32 = 0x0000_0000;
pub(crate) const OBJ_PHYSICAL: u32 = 0x4000_0000;
pub(crate) const OBJ_EPHEMERAL: u32 = 0x8000_0000;
pub(crate) const OBJ_NOHEADER: u32 = 0x2000_0000;
pub(crate) const OBJ_ENCRYPTED: u32 = 0x1000_0000;
pub(crate) const OBJ_NONPERSISTENT: u32 = 0x0800_0000;
pub(crate) const BTREE_PHYSICAL: u32 = 0x0000_0010;
pub(crate) const BTREE_HASHED: u32 = 0x0000_0080;
pub(crate) const BTREE_NOHEADER: u32 = 0x0000_0100;
#[cfg(test)]
pub(crate) const BTNODE_ROOT: u16 = 0x0001;
pub(crate) const BTNODE_LEAF: u16 = 0x0002;
pub(crate) const BTNODE_FIXED_KV_SIZE: u16 = 0x0004;
pub(crate) const BTNODE_HASHED: u16 = 0x0008;
pub(crate) const BTNODE_NOHEADER: u16 = 0x0010;
pub(crate) const CHECKPOINT_AREA_BTREE_FLAG: u32 = 0x8000_0000;
pub(crate) const OMAP_VAL_DELETED: u32 = 0x0000_0001;
pub(crate) const CHECKPOINT_MAP_LAST: u32 = 0x0000_0001;
pub(crate) const OMAP_MANUALLY_MANAGED: u32 = 0x0000_0001;
pub(crate) const OMAP_ENCRYPTING: u32 = 0x0000_0002;
pub(crate) const OMAP_DECRYPTING: u32 = 0x0000_0004;
pub(crate) const OMAP_KEYROLLING: u32 = 0x0000_0008;
pub(crate) const OMAP_CRYPTO_GENERATION: u32 = 0x0000_0010;
pub(crate) const NX_FEATURE_DEFRAG: u64 = 0x0000_0001;
pub(crate) const NX_FEATURE_LCFD: u64 = 0x0000_0002;
pub(crate) const APFS_INCOMPAT_CASE_INSENSITIVE: u64 = 0x0000_0001;
pub(crate) const APFS_INCOMPAT_DATALESS_SNAPS: u64 = 0x0000_0002;
pub(crate) const APFS_INCOMPAT_ENC_ROLLED: u64 = 0x0000_0004;
pub(crate) const APFS_INCOMPAT_NORMALIZATION_INSENSITIVE: u64 = 0x0000_0008;
pub(crate) const APFS_INCOMPAT_INCOMPLETE_RESTORE: u64 = 0x0000_0010;
pub(crate) const APFS_INCOMPAT_SEALED_VOLUME: u64 = 0x0000_0020;
pub(crate) const APFS_INCOMPAT_PFK_VOL: u64 = 0x0000_0040;
pub(crate) const APFS_INCOMPAT_RESERVED_80: u64 = 0x0000_0080;
pub(crate) const APFS_INCOMPAT_SECONDARY_FSROOT: u64 = 0x0000_0100;
pub(crate) const APFS_FEATURE_DEFRAG_PRERELEASE: u64 = 0x0000_0001;
pub(crate) const APFS_FEATURE_HARDLINK_MAP_RECORDS: u64 = 0x0000_0002;
pub(crate) const APFS_FEATURE_DEFRAG: u64 = 0x0000_0004;
pub(crate) const APFS_FEATURE_STRICTATIME: u64 = 0x0000_0008;
pub(crate) const APFS_FEATURE_VOLGRP_SYSTEM_INO_SPACE: u64 = 0x0000_0010;
pub(crate) const NX_INCOMPAT_FUSION: u64 = 0x0000_0100;
pub(crate) const NX_CRYPTO_SW: u64 = 0x0000_0004;
pub(crate) const APFS_FS_UNENCRYPTED: u64 = 0x0000_0001;
pub(crate) const APFS_FS_ONEKEY: u64 = 0x0000_0008;
pub(crate) const APFS_FS_SPILLEDOVER: u64 = 0x0000_0010;
pub(crate) const APFS_FS_RUN_SPILLOVER_CLEANER: u64 = 0x0000_0020;
pub(crate) const APFS_FS_ALWAYS_CHECK_EXTENTREF: u64 = 0x0000_0040;
pub(crate) const APFS_FS_PREVIOUSLY_SEALED: u64 = 0x0000_0080;
pub(crate) const APFS_FS_PFK: u64 = 0x0000_0100;
pub(crate) const APFS_VOL_ROLE_SYSTEM: u16 = 0x0001;
pub(crate) const APFS_VOL_ROLE_USER: u16 = 0x0002;
pub(crate) const APFS_VOL_ROLE_RECOVERY: u16 = 0x0004;
pub(crate) const APFS_VOL_ROLE_VM: u16 = 0x0008;
pub(crate) const APFS_VOL_ROLE_PREBOOT: u16 = 0x0010;
pub(crate) const APFS_VOL_ROLE_INSTALLER: u16 = 0x0020;
pub(crate) const APFS_VOLUME_ENUM_SHIFT: u16 = 6;
pub(crate) const APFS_VOL_ROLE_DATA: u16 = 1 << APFS_VOLUME_ENUM_SHIFT;
pub(crate) const APFS_VOL_ROLE_BASEBAND: u16 = 2 << APFS_VOLUME_ENUM_SHIFT;
pub(crate) const APFS_VOL_ROLE_UPDATE: u16 = 3 << APFS_VOLUME_ENUM_SHIFT;
pub(crate) const APFS_VOL_ROLE_XART: u16 = 4 << APFS_VOLUME_ENUM_SHIFT;
pub(crate) const APFS_VOL_ROLE_HARDWARE: u16 = 5 << APFS_VOLUME_ENUM_SHIFT;
pub(crate) const APFS_VOL_ROLE_BACKUP: u16 = 6 << APFS_VOLUME_ENUM_SHIFT;
pub(crate) const APFS_VOL_ROLE_ENTERPRISE: u16 = 9 << APFS_VOLUME_ENUM_SHIFT;
pub(crate) const APFS_VOL_ROLE_PRELOGIN: u16 = 11 << APFS_VOLUME_ENUM_SHIFT;
pub(crate) const OBJECT_HEADER_SIZE: usize = 32;
pub(crate) const BTREE_NODE_HEADER_SIZE: usize = 24;
pub(crate) const BTREE_INFO_SIZE: usize = 40;
pub(crate) const APFS_SEAL_BROKEN: u32 = 1;
pub(crate) const APFS_HASH_SHA256: u32 = 0x1;
pub(crate) const APFS_HASH_SHA512_256: u32 = 0x2;
pub(crate) const APFS_HASH_SHA384: u32 = 0x3;
pub(crate) const APFS_HASH_SHA512: u32 = 0x4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ApfsObjectHeader {
pub checksum: u64,
pub oid: u64,
pub xid: u64,
pub object_type: u32,
pub subtype: u32,
}
impl ApfsObjectHeader {
pub(crate) fn parse(bytes: &[u8]) -> Result<Self> {
require_len(bytes, OBJECT_HEADER_SIZE, "apfs object header")?;
Ok(Self {
checksum: read_u64_le(bytes, 0)?,
oid: read_u64_le(bytes, 8)?,
xid: read_u64_le(bytes, 16)?,
object_type: read_u32_le(bytes, 24)?,
subtype: read_u32_le(bytes, 28)?,
})
}
pub(crate) fn type_code(&self) -> u32 {
self.object_type & OBJECT_TYPE_MASK
}
pub(crate) fn storage_type(&self) -> u32 {
self.object_type & OBJ_STORAGETYPE_MASK
}
pub(crate) fn is_physical(&self) -> bool {
self.storage_type() == OBJ_PHYSICAL
}
pub(crate) fn is_ephemeral(&self) -> bool {
self.storage_type() == OBJ_EPHEMERAL
}
pub(crate) fn validate_checksum(&self, block: &[u8]) -> bool {
self.checksum == fletcher64(&block[8..])
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ApfsContainerSuperblock {
pub header: ApfsObjectHeader,
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 checkpoint_descriptor_blocks: u32,
pub checkpoint_data_blocks: u32,
pub checkpoint_descriptor_base: u64,
pub checkpoint_data_base: u64,
pub checkpoint_descriptor_next: u32,
pub checkpoint_data_next: u32,
pub checkpoint_descriptor_index: u32,
pub checkpoint_descriptor_len: u32,
pub checkpoint_data_index: u32,
pub checkpoint_data_len: u32,
pub spaceman_oid: u64,
pub omap_oid: u64,
pub reaper_oid: u64,
pub test_type: u32,
pub counters: [u64; 32],
pub blocked_out_prange: Option<ApfsPrange>,
pub evict_mapping_tree_oid: u64,
pub flags: u64,
pub efi_jumpstart_oid: u64,
pub fusion_uuid: [u8; 16],
pub container_keybag_prange: Option<ApfsPrange>,
pub ephemeral_info: [u64; 4],
pub test_oid: u64,
pub fusion_middle_tree_oid: u64,
pub fusion_wbc_oid: u64,
pub fusion_wbc_prange: Option<ApfsPrange>,
pub newest_mounted_version: u64,
pub media_keybag_prange: Option<ApfsPrange>,
pub max_file_systems: u32,
pub file_system_oids: Vec<u64>,
}
impl ApfsContainerSuperblock {
pub(crate) fn parse(block: &[u8]) -> Result<Self> {
require_len(block, 1408, "apfs container superblock")?;
let header = ApfsObjectHeader::parse(block)?;
let magic = read_array::<4>(block, 32)?;
if &magic != NX_MAGIC {
return Err(Error::invalid_format(format!(
"invalid apfs container superblock magic: {:?}",
String::from_utf8_lossy(&magic)
)));
}
let block_size = read_u32_le(block, 36)?;
if !(MIN_BLOCK_SIZE..=MAX_BLOCK_SIZE).contains(&block_size) || !block_size.is_power_of_two() {
return Err(Error::invalid_format(format!(
"invalid apfs block size: {block_size}"
)));
}
let max_file_systems = read_u32_le(block, 180)?;
if max_file_systems > 100 {
return Err(Error::invalid_format(format!(
"invalid apfs max file system count: {max_file_systems}"
)));
}
let mut file_system_oids = Vec::new();
for index in 0..100usize {
let oid = read_u64_le(block, 184 + index * 8)?;
if oid != 0 {
file_system_oids.push(oid);
}
}
Ok(Self {
header,
block_size,
block_count: read_u64_le(block, 40)?,
features: read_u64_le(block, 48)?,
readonly_compatible_features: read_u64_le(block, 56)?,
incompatible_features: read_u64_le(block, 64)?,
uuid: read_array(block, 72)?,
next_oid: read_u64_le(block, 88)?,
next_xid: read_u64_le(block, 96)?,
checkpoint_descriptor_blocks: read_u32_le(block, 104)?,
checkpoint_data_blocks: read_u32_le(block, 108)?,
checkpoint_descriptor_base: read_u64_le(block, 112)?,
checkpoint_data_base: read_u64_le(block, 120)?,
checkpoint_descriptor_next: read_u32_le(block, 128)?,
checkpoint_data_next: read_u32_le(block, 132)?,
checkpoint_descriptor_index: read_u32_le(block, 136)?,
checkpoint_descriptor_len: read_u32_le(block, 140)?,
checkpoint_data_index: read_u32_le(block, 144)?,
checkpoint_data_len: read_u32_le(block, 148)?,
spaceman_oid: read_u64_le(block, 152)?,
omap_oid: read_u64_le(block, 160)?,
reaper_oid: read_u64_le(block, 168)?,
test_type: read_u32_le(block, 176)?,
counters: read_u64_array::<32>(block, 984)?,
blocked_out_prange: {
let prange = ApfsPrange::parse(read_slice(block, 1240, 16, "apfs blocked out prange")?)?;
(prange.start_paddr != 0 || prange.block_count != 0).then_some(prange)
},
evict_mapping_tree_oid: read_u64_le(block, 1256)?,
flags: read_u64_le(block, 1264)?,
efi_jumpstart_oid: read_u64_le(block, 1272)?,
fusion_uuid: read_array(block, 1280)?,
container_keybag_prange: {
let prange =
ApfsPrange::parse(read_slice(block, 1296, 16, "apfs container keybag prange")?)?;
(prange.start_paddr != 0 && prange.block_count != 0).then_some(prange)
},
ephemeral_info: [
read_u64_le(block, 1312)?,
read_u64_le(block, 1320)?,
read_u64_le(block, 1328)?,
read_u64_le(block, 1336)?,
],
test_oid: read_u64_le(block, 1344)?,
fusion_middle_tree_oid: read_u64_le(block, 1352)?,
fusion_wbc_oid: read_u64_le(block, 1360)?,
fusion_wbc_prange: {
let prange = ApfsPrange::parse(read_slice(block, 1368, 16, "apfs fusion wbc prange")?)?;
(prange.start_paddr != 0 || prange.block_count != 0).then_some(prange)
},
newest_mounted_version: read_u64_le(block, 1384)?,
media_keybag_prange: {
let prange = ApfsPrange::parse(read_slice(block, 1392, 16, "apfs media keybag prange")?)?;
(prange.start_paddr != 0 || prange.block_count != 0).then_some(prange)
},
max_file_systems,
file_system_oids,
})
}
pub(crate) fn validate(&self, block: &[u8], _address: u64) -> Result<()> {
if self.header.type_code() != OBJECT_TYPE_NX_SUPERBLOCK {
return Err(Error::invalid_format(format!(
"invalid apfs container object type: 0x{:08x}",
self.header.object_type
)));
}
if !self.header.is_ephemeral() {
return Err(Error::invalid_format(
"apfs container superblock must be ephemeral".to_string(),
));
}
if self.header.oid != 1 {
return Err(Error::invalid_format(format!(
"apfs container superblock oid {} must be 1",
self.header.oid
)));
}
if !self.header.validate_checksum(block) {
return Err(Error::invalid_format(
"invalid apfs container superblock checksum".to_string(),
));
}
Ok(())
}
pub(crate) fn compare_layout(&self, other: &Self) -> Result<()> {
for (name, left, right) in [("uuid", self.uuid.as_slice(), other.uuid.as_slice())] {
if left != right {
return Err(Error::invalid_format(format!(
"apfs container superblock {name} does not match"
)));
}
}
for (name, left, right) in [(
"fusion_uuid",
self.fusion_uuid.as_slice(),
other.fusion_uuid.as_slice(),
)] {
if left != right {
return Err(Error::invalid_format(format!(
"apfs container superblock {name} does not match"
)));
}
}
for (name, left, right) in [
(
"block_size",
u64::from(self.block_size),
u64::from(other.block_size),
),
("block_count", self.block_count, other.block_count),
(
"checkpoint_descriptor_blocks",
u64::from(self.checkpoint_descriptor_blocks),
u64::from(other.checkpoint_descriptor_blocks),
),
(
"checkpoint_data_blocks",
u64::from(self.checkpoint_data_blocks),
u64::from(other.checkpoint_data_blocks),
),
(
"checkpoint_descriptor_base",
self.checkpoint_descriptor_base,
other.checkpoint_descriptor_base,
),
(
"checkpoint_data_base",
self.checkpoint_data_base,
other.checkpoint_data_base,
),
(
"evict_mapping_tree_oid",
self.evict_mapping_tree_oid,
other.evict_mapping_tree_oid,
),
("flags", self.flags, other.flags),
(
"efi_jumpstart_oid",
self.efi_jumpstart_oid,
other.efi_jumpstart_oid,
),
] {
if left != right {
return Err(Error::invalid_format(format!(
"apfs container superblock {name} changed across checkpoints"
)));
}
}
Ok(())
}
pub(crate) fn descriptor_area_is_btree(&self) -> bool {
(self.checkpoint_descriptor_blocks & CHECKPOINT_AREA_BTREE_FLAG) != 0
}
pub(crate) fn descriptor_area_block_count(&self) -> u32 {
self.checkpoint_descriptor_blocks & !CHECKPOINT_AREA_BTREE_FLAG
}
pub(crate) fn is_fusion(&self) -> bool {
(self.incompatible_features & NX_INCOMPAT_FUSION) != 0
}
pub(crate) fn uses_software_crypto(&self) -> bool {
(self.flags & NX_CRYPTO_SW) != 0
}
}
pub(crate) fn nx_feature_names(flags: u64) -> Vec<&'static str> {
bit_names_u64(
flags,
&[(NX_FEATURE_DEFRAG, "defrag"), (NX_FEATURE_LCFD, "lcfd")],
)
}
pub(crate) fn nx_incompat_feature_names(flags: u64) -> Vec<&'static str> {
bit_names_u64(
flags,
&[
(1, "version1"),
(2, "version2"),
(NX_INCOMPAT_FUSION, "fusion"),
],
)
}
pub(crate) fn nx_flag_names(flags: u64) -> Vec<&'static str> {
bit_names_u64(
flags,
&[
(1, "reserved_1"),
(2, "reserved_2"),
(NX_CRYPTO_SW, "crypto_sw"),
],
)
}
pub(crate) fn apfs_feature_names(flags: u64) -> Vec<&'static str> {
bit_names_u64(
flags,
&[
(APFS_FEATURE_DEFRAG_PRERELEASE, "defrag_prerelease"),
(APFS_FEATURE_HARDLINK_MAP_RECORDS, "hardlink_map_records"),
(APFS_FEATURE_DEFRAG, "defrag"),
(APFS_FEATURE_STRICTATIME, "strictatime"),
(
APFS_FEATURE_VOLGRP_SYSTEM_INO_SPACE,
"volgrp_system_ino_space",
),
],
)
}
pub(crate) fn apfs_incompat_feature_names(flags: u64) -> Vec<&'static str> {
bit_names_u64(
flags,
&[
(APFS_INCOMPAT_CASE_INSENSITIVE, "case_insensitive"),
(APFS_INCOMPAT_DATALESS_SNAPS, "dataless_snaps"),
(APFS_INCOMPAT_ENC_ROLLED, "enc_rolled"),
(
APFS_INCOMPAT_NORMALIZATION_INSENSITIVE,
"normalization_insensitive",
),
(APFS_INCOMPAT_INCOMPLETE_RESTORE, "incomplete_restore"),
(APFS_INCOMPAT_SEALED_VOLUME, "sealed_volume"),
(APFS_INCOMPAT_PFK_VOL, "pfk"),
(APFS_INCOMPAT_RESERVED_80, "reserved_80"),
(APFS_INCOMPAT_SECONDARY_FSROOT, "secondary_fsroot"),
],
)
}
pub(crate) fn apfs_fs_flag_names(flags: u64) -> Vec<&'static str> {
bit_names_u64(
flags,
&[
(APFS_FS_UNENCRYPTED, "unencrypted"),
(APFS_FS_ONEKEY, "onekey"),
(APFS_FS_SPILLEDOVER, "spilledover"),
(APFS_FS_RUN_SPILLOVER_CLEANER, "run_spillover_cleaner"),
(APFS_FS_ALWAYS_CHECK_EXTENTREF, "always_check_extentref"),
(APFS_FS_PREVIOUSLY_SEALED, "previously_sealed"),
(APFS_FS_PFK, "pfk"),
],
)
}
pub(crate) fn apfs_omap_flag_names(flags: u32) -> Vec<&'static str> {
bit_names_u32(
flags,
&[
(OMAP_MANUALLY_MANAGED, "manually_managed"),
(OMAP_ENCRYPTING, "encrypting"),
(OMAP_DECRYPTING, "decrypting"),
(OMAP_KEYROLLING, "keyrolling"),
(OMAP_CRYPTO_GENERATION, "crypto_generation"),
],
)
}
pub fn apfs_object_type_name(object_type: u32) -> &'static str {
match object_type & OBJECT_TYPE_MASK {
0 => "invalid",
OBJECT_TYPE_NX_SUPERBLOCK => "nx_superblock",
OBJECT_TYPE_BTREE => "btree",
OBJECT_TYPE_BTREE_NODE => "btree_node",
0x0000_0005 => "spaceman",
0x0000_0006 => "spaceman_cab",
0x0000_0007 => "spaceman_cib",
0x0000_0008 => "spaceman_bitmap",
0x0000_0009 => "spaceman_free_queue",
0x0000_000A => "extent_list_tree",
OBJECT_TYPE_OMAP => "omap",
OBJECT_TYPE_CHECKPOINT_MAP => "checkpoint_map",
OBJECT_TYPE_FS => "fs",
OBJECT_TYPE_FSTREE => "fstree",
OBJECT_TYPE_BLOCKREFTREE => "blockreftree",
OBJECT_TYPE_SNAP_META_TREE => "snap_meta_tree",
OBJECT_TYPE_NX_REAPER => "nx_reaper",
0x0000_0012 => "nx_reap_list",
OBJECT_TYPE_OMAP_SNAPSHOT => "omap_snapshot",
OBJECT_TYPE_EFI_JUMPSTART => "efi_jumpstart",
OBJECT_TYPE_FUSION_MIDDLE_TREE => "fusion_middle_tree",
OBJECT_TYPE_NX_FUSION_WBC => "nx_fusion_wbc",
OBJECT_TYPE_NX_FUSION_WBC_LIST => "nx_fusion_wbc_list",
OBJECT_TYPE_ER_STATE => "er_state",
OBJECT_TYPE_GBITMAP => "gbitmap",
OBJECT_TYPE_GBITMAP_TREE => "gbitmap_tree",
OBJECT_TYPE_GBITMAP_BLOCK => "gbitmap_block",
OBJECT_TYPE_ER_RECOVERY_BLOCK => "er_recovery_block",
OBJECT_TYPE_SNAP_META_EXT => "snap_meta_ext",
OBJECT_TYPE_INTEGRITY_META => "integrity_meta",
OBJECT_TYPE_FEXT_TREE => "fext_tree",
OBJECT_TYPE_RESERVED_20 => "reserved_20",
OBJECT_TYPE_TEST => "test",
APFS_OBJECT_TYPE_CONTAINER_KEYBAG => "container_keybag",
APFS_OBJECT_TYPE_VOLUME_KEYBAG => "volume_keybag",
APFS_OBJECT_TYPE_MEDIA_KEYBAG => "media_keybag",
_ => "unknown",
}
}
pub fn apfs_object_storage_kind_name(object_type: u32) -> &'static str {
match object_type & OBJ_STORAGETYPE_MASK {
OBJ_VIRTUAL => "virtual",
OBJ_PHYSICAL => "physical",
OBJ_EPHEMERAL => "ephemeral",
_ => "unknown",
}
}
pub fn apfs_object_flag_names(object_type: u32) -> Vec<&'static str> {
bit_names_u32(
object_type,
&[
(OBJ_NONPERSISTENT, "nonpersistent"),
(OBJ_ENCRYPTED, "encrypted"),
(OBJ_NOHEADER, "noheader"),
],
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ApfsObjectMap {
pub header: ApfsObjectHeader,
pub flags: u32,
pub snapshot_count: u32,
pub tree_type: u32,
pub snapshot_tree_type: u32,
pub tree_oid: u64,
pub snapshot_tree_oid: u64,
pub most_recent_snapshot_xid: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApfsCheckpointMapping {
pub object_type: u32,
pub object_subtype: u32,
pub size: u32,
pub file_system_object_id: u64,
pub object_id: u64,
pub physical_address: u64,
}
impl ApfsCheckpointMapping {
pub fn parse(bytes: &[u8]) -> Result<Self> {
require_len(bytes, 40, "apfs checkpoint map entry")?;
Ok(Self {
object_type: read_u32_le(bytes, 0)?,
object_subtype: read_u32_le(bytes, 4)?,
size: read_u32_le(bytes, 8)?,
file_system_object_id: read_u64_le(bytes, 16)?,
object_id: read_u64_le(bytes, 24)?,
physical_address: read_u64_le(bytes, 32)?,
})
}
pub fn object_type_name(&self) -> &'static str {
apfs_object_type_name(self.object_type)
}
pub fn object_storage_kind_name(&self) -> &'static str {
apfs_object_storage_kind_name(self.object_type)
}
pub fn object_flag_names(&self) -> Vec<&'static str> {
apfs_object_flag_names(self.object_type)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApfsCheckpointMap {
header: ApfsObjectHeader,
pub flags: u32,
pub entry_count: u32,
pub entries: Vec<ApfsCheckpointMapping>,
}
impl ApfsCheckpointMap {
pub fn parse(block: &[u8]) -> Result<Self> {
require_len(block, 40, "apfs checkpoint map")?;
let header = ApfsObjectHeader::parse(block)?;
let entry_count = read_u32_le(block, 36)?;
let entries_offset = 40usize;
let entries_length = usize::try_from(entry_count)
.map_err(|_| Error::invalid_range("apfs checkpoint map entry count exceeds usize"))?
.checked_mul(40)
.ok_or_else(|| Error::invalid_range("apfs checkpoint map size overflow"))?;
let entries_bytes = read_slice(
block,
entries_offset,
entries_length,
"apfs checkpoint map entries",
)?;
let mut entries = Vec::with_capacity(entry_count as usize);
for chunk in entries_bytes.chunks_exact(40) {
entries.push(ApfsCheckpointMapping::parse(chunk)?);
}
Ok(Self {
header,
flags: read_u32_le(block, 32)?,
entry_count,
entries,
})
}
pub fn validate(&self, block: &[u8]) -> Result<()> {
if self.header.type_code() != OBJECT_TYPE_CHECKPOINT_MAP {
return Err(Error::invalid_format(format!(
"invalid apfs checkpoint map type: 0x{:08x}",
self.header.object_type
)));
}
if !self.header.is_physical() {
return Err(Error::invalid_format(
"apfs checkpoint map must be physical".to_string(),
));
}
if !self.header.validate_checksum(block) {
return Err(Error::invalid_format(
"invalid apfs checkpoint map checksum".to_string(),
));
}
Ok(())
}
pub fn is_last(&self) -> bool {
(self.flags & CHECKPOINT_MAP_LAST) != 0
}
pub fn object_id(&self) -> u64 {
self.header.oid
}
pub fn xid(&self) -> u64 {
self.header.xid
}
}
impl ApfsObjectMap {
pub(crate) fn parse(block: &[u8]) -> Result<Self> {
require_len(block, 88, "apfs object map")?;
let header = ApfsObjectHeader::parse(block)?;
Ok(Self {
header,
flags: read_u32_le(block, 32)?,
snapshot_count: read_u32_le(block, 36)?,
tree_type: read_u32_le(block, 40)?,
snapshot_tree_type: read_u32_le(block, 44)?,
tree_oid: read_u64_le(block, 48)?,
snapshot_tree_oid: read_u64_le(block, 56)?,
most_recent_snapshot_xid: read_u64_le(block, 64)?,
})
}
pub(crate) fn validate(&self, block: &[u8], address: u64) -> Result<()> {
if self.header.type_code() != OBJECT_TYPE_OMAP {
return Err(Error::invalid_format(format!(
"invalid apfs object map type: 0x{:08x}",
self.header.object_type
)));
}
if !self.header.is_physical() {
return Err(Error::invalid_format(
"apfs object map must be physical".to_string(),
));
}
if self.header.oid != address {
return Err(Error::invalid_format(format!(
"apfs object map oid {} does not match address {address}",
self.header.oid
)));
}
if !self.header.validate_checksum(block) {
return Err(Error::invalid_format(
"invalid apfs object map checksum".to_string(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ApfsVolumeSuperblock {
pub header: ApfsObjectHeader,
pub fs_index: u32,
pub features: u64,
pub readonly_compatible_features: u64,
pub incompatible_features: u64,
pub unmount_time: u64,
pub reserve_block_count: u64,
pub quota_block_count: u64,
pub alloc_block_count: u64,
pub meta_crypto: ApfsMetaCryptoState,
pub volume_uuid: [u8; 16],
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 revert_to_xid: u64,
pub revert_to_sblock_oid: u64,
pub next_object_id: u64,
pub number_of_files: u64,
pub number_of_directories: u64,
pub number_of_symlinks: u64,
pub number_of_other_fsobjects: u64,
pub number_of_snapshots: u64,
pub total_blocks_allocated: u64,
pub total_blocks_freed: u64,
pub last_modification_time: u64,
pub fs_flags: u64,
pub formatted_by: ApfsChangeInfo,
pub modified_by: Vec<ApfsChangeInfo>,
pub volume_name: String,
pub next_document_id: u32,
pub role: u16,
pub root_to_xid: u64,
pub encryption_rolling_state_oid: u64,
pub snap_meta_ext_oid: u64,
pub volume_group_id: [u8; 16],
pub integrity_meta_oid: u64,
pub fext_tree_oid: u64,
pub fext_tree_type: u32,
pub pfkur_tree_type: u32,
pub pfkur_tree_oid: u64,
pub doc_id_index_xid: u64,
pub doc_id_index_flags: u32,
pub doc_id_tree_type: u32,
pub doc_id_tree_oid: u64,
pub prev_doc_id_tree_oid: u64,
pub doc_id_fixup_cursor: u64,
pub secondary_root_tree_oid: u64,
pub secondary_root_tree_type: u32,
pub clone_group_tree_flags: u32,
}
impl ApfsVolumeSuperblock {
pub(crate) fn parse(block: &[u8]) -> Result<Self> {
require_len(block, 1112, "apfs volume superblock")?;
let header = ApfsObjectHeader::parse(block)?;
let magic = read_array::<4>(block, 32)?;
if &magic != APFS_MAGIC {
return Err(Error::invalid_format(format!(
"invalid apfs volume superblock magic: {:?}",
String::from_utf8_lossy(&magic)
)));
}
let volume_name_bytes = read_slice(block, 704, 256, "apfs volume name")?;
let volume_name = bytes_to_cstring(volume_name_bytes);
Ok(Self {
header,
fs_index: read_u32_le(block, 36)?,
features: read_u64_le(block, 40)?,
readonly_compatible_features: read_u64_le(block, 48)?,
incompatible_features: read_u64_le(block, 56)?,
unmount_time: read_u64_le(block, 60)?,
reserve_block_count: read_u64_le(block, 68)?,
quota_block_count: read_u64_le(block, 76)?,
alloc_block_count: read_u64_le(block, 84)?,
meta_crypto: ApfsMetaCryptoState::parse(read_slice(
block,
96,
20,
"apfs meta crypto state",
)?)?,
root_tree_type: read_u32_le(block, 116)?,
extentref_tree_type: read_u32_le(block, 120)?,
snap_meta_tree_type: read_u32_le(block, 124)?,
omap_oid: read_u64_le(block, 128)?,
root_tree_oid: read_u64_le(block, 136)?,
extentref_tree_oid: read_u64_le(block, 144)?,
snap_meta_tree_oid: read_u64_le(block, 152)?,
revert_to_xid: read_u64_le(block, 160)?,
revert_to_sblock_oid: read_u64_le(block, 168)?,
next_object_id: read_u64_le(block, 176)?,
number_of_files: read_u64_le(block, 184)?,
number_of_directories: read_u64_le(block, 192)?,
number_of_symlinks: read_u64_le(block, 200)?,
number_of_other_fsobjects: read_u64_le(block, 208)?,
number_of_snapshots: read_u64_le(block, 216)?,
total_blocks_allocated: read_u64_le(block, 224)?,
total_blocks_freed: read_u64_le(block, 232)?,
volume_uuid: read_array(block, 240)?,
last_modification_time: read_u64_le(block, 256)?,
fs_flags: read_u64_le(block, 264)?,
formatted_by: parse_change_info(block, 272)?,
modified_by: (0..8)
.map(|index| parse_change_info(block, 320 + index * 48))
.collect::<Result<Vec<_>>>()?,
volume_name,
next_document_id: read_u32_le(block, 960)?,
role: read_u16_le(block, 964)?,
root_to_xid: read_u64_le(block, 968)?,
encryption_rolling_state_oid: read_u64_le(block, 976)?,
snap_meta_ext_oid: read_u64_le(block, 1000)?,
volume_group_id: read_array(block, 1008)?,
integrity_meta_oid: read_u64_le(block, 1024)?,
fext_tree_oid: read_u64_le(block, 1032)?,
fext_tree_type: read_u32_le(block, 1040)?,
pfkur_tree_type: read_u32_le(block, 1044)?,
pfkur_tree_oid: read_u64_le(block, 1048)?,
doc_id_index_xid: read_u64_le(block, 1056)?,
doc_id_index_flags: read_u32_le(block, 1064)?,
doc_id_tree_type: read_u32_le(block, 1068)?,
doc_id_tree_oid: read_u64_le(block, 1072)?,
prev_doc_id_tree_oid: read_u64_le(block, 1080)?,
doc_id_fixup_cursor: read_u64_le(block, 1088)?,
secondary_root_tree_oid: read_u64_le(block, 1096)?,
secondary_root_tree_type: read_u32_le(block, 1104)?,
clone_group_tree_flags: read_u32_le(block, 1108)?,
})
}
pub(crate) fn validate(&self, block: &[u8]) -> Result<()> {
if self.header.type_code() != OBJECT_TYPE_FS {
return Err(Error::invalid_format(format!(
"invalid apfs volume superblock type: 0x{:08x}",
self.header.object_type
)));
}
if !self.header.validate_checksum(block) {
return Err(Error::invalid_format(
"invalid apfs volume superblock checksum".to_string(),
));
}
Ok(())
}
pub(crate) fn is_case_insensitive(&self) -> bool {
(self.incompatible_features & APFS_INCOMPAT_CASE_INSENSITIVE) != 0
}
pub(crate) fn is_normalization_insensitive(&self) -> bool {
(self.incompatible_features & APFS_INCOMPAT_NORMALIZATION_INSENSITIVE) != 0
}
pub(crate) fn is_sealed(&self) -> bool {
(self.incompatible_features & APFS_INCOMPAT_SEALED_VOLUME) != 0
}
pub(crate) fn has_dataless_snapshots(&self) -> bool {
(self.incompatible_features & APFS_INCOMPAT_DATALESS_SNAPS) != 0
}
pub(crate) fn has_secondary_fs_root(&self) -> bool {
(self.incompatible_features & APFS_INCOMPAT_SECONDARY_FSROOT) != 0
|| self.secondary_root_tree_oid != 0
}
pub(crate) fn uses_volume_group_system_inode_space(&self) -> bool {
(self.features & APFS_FEATURE_VOLGRP_SYSTEM_INO_SPACE) != 0
}
pub(crate) fn is_encrypted(&self) -> bool {
(self.fs_flags & APFS_FS_UNENCRYPTED) == 0 && (self.fs_flags & APFS_FS_PFK) == 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApfsIntegrityMetadata {
header: ApfsObjectHeader,
pub version: u32,
pub flags: u32,
pub hash_type: u32,
pub broken_xid: u64,
pub root_hash: Box<[u8]>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApfsChangeInfo {
pub application_id: String,
pub timestamp: u64,
pub last_xid: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ApfsMetaCryptoState {
pub major_version: u16,
pub minor_version: u16,
pub flags: u32,
pub persistent_class: u32,
pub key_os_version: u32,
pub key_revision: u16,
}
impl ApfsMetaCryptoState {
pub(crate) fn parse(bytes: &[u8]) -> Result<Self> {
require_len(bytes, 20, "apfs meta crypto state")?;
Ok(Self {
major_version: read_u16_le(bytes, 0)?,
minor_version: read_u16_le(bytes, 2)?,
flags: read_u32_le(bytes, 4)?,
persistent_class: read_u32_le(bytes, 8)?,
key_os_version: read_u32_le(bytes, 12)?,
key_revision: read_u16_le(bytes, 16)?,
})
}
}
impl ApfsChangeInfo {
pub fn is_empty(&self) -> bool {
self.application_id.is_empty() && self.timestamp == 0 && self.last_xid == 0
}
}
impl ApfsIntegrityMetadata {
pub(crate) fn parse(block: &[u8]) -> Result<Self> {
require_len(block, 112, "apfs integrity metadata")?;
let header = ApfsObjectHeader::parse(block)?;
if header.type_code() != OBJECT_TYPE_INTEGRITY_META {
return Err(Error::invalid_format(format!(
"invalid apfs integrity metadata type: 0x{:08x}",
header.object_type
)));
}
if !header.validate_checksum(block) {
return Err(Error::invalid_format(
"invalid apfs integrity metadata checksum".to_string(),
));
}
let hash_type = read_u32_le(block, 40)?;
let root_hash_offset = usize::try_from(read_u32_le(block, 44)?).map_err(|_| {
Error::invalid_range("apfs integrity metadata root hash offset exceeds usize")
})?;
let root_hash_length = apfs_hash_size(hash_type)?;
let root_hash = read_slice(
block,
root_hash_offset,
root_hash_length,
"apfs integrity metadata root hash",
)?;
Ok(Self {
header,
version: read_u32_le(block, 32)?,
flags: read_u32_le(block, 36)?,
hash_type,
broken_xid: read_u64_le(block, 48)?,
root_hash: root_hash.to_vec().into_boxed_slice(),
})
}
pub fn seal_broken(&self) -> bool {
(self.flags & APFS_SEAL_BROKEN) != 0
}
pub fn object_id(&self) -> u64 {
self.header.oid
}
pub fn xid(&self) -> u64 {
self.header.xid
}
}
pub(crate) fn apfs_hash_size(hash_type: u32) -> Result<usize> {
match hash_type {
APFS_HASH_SHA256 | APFS_HASH_SHA512_256 => Ok(32),
APFS_HASH_SHA384 => Ok(48),
APFS_HASH_SHA512 => Ok(64),
_ => Err(Error::unsupported(format!(
"unsupported apfs integrity hash type: {hash_type}"
))),
}
}
fn parse_change_info(block: &[u8], offset: usize) -> Result<ApfsChangeInfo> {
Ok(ApfsChangeInfo {
application_id: bytes_to_cstring(read_slice(block, offset, 32, "apfs change info id")?),
timestamp: read_u64_le(block, offset + 32)?,
last_xid: read_u64_le(block, offset + 40)?,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ApfsPrange {
pub start_paddr: u64,
pub block_count: u64,
}
impl ApfsPrange {
pub(crate) fn parse(bytes: &[u8]) -> Result<Self> {
require_len(bytes, 16, "apfs prange")?;
let start = read_i64_le(bytes, 0)?;
if start < 0 {
return Err(Error::invalid_format(
"apfs prange start address must be non-negative".to_string(),
));
}
Ok(Self {
start_paddr: start as u64,
block_count: read_u64_le(bytes, 8)?,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ApfsBtreeInfo {
pub flags: u32,
pub node_size: u32,
pub key_size: u32,
pub value_size: u32,
pub longest_key: u32,
pub longest_value: u32,
pub key_count: u64,
pub node_count: u64,
}
impl ApfsBtreeInfo {
pub(crate) fn parse(bytes: &[u8]) -> Result<Self> {
require_len(bytes, BTREE_INFO_SIZE, "apfs btree info")?;
Ok(Self {
flags: read_u32_le(bytes, 0)?,
node_size: read_u32_le(bytes, 4)?,
key_size: read_u32_le(bytes, 8)?,
value_size: read_u32_le(bytes, 12)?,
longest_key: read_u32_le(bytes, 16)?,
longest_value: read_u32_le(bytes, 20)?,
key_count: read_u64_le(bytes, 24)?,
node_count: read_u64_le(bytes, 32)?,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ApfsBtreeNodeHeader {
pub flags: u16,
pub level: u16,
pub key_count: u32,
pub table_space_offset: u16,
pub table_space_length: u16,
pub free_space_offset: u16,
pub free_space_length: u16,
pub key_free_list_offset: u16,
pub key_free_list_length: u16,
pub value_free_list_offset: u16,
pub value_free_list_length: u16,
}
impl ApfsBtreeNodeHeader {
pub(crate) fn parse(bytes: &[u8]) -> Result<Self> {
require_len(bytes, BTREE_NODE_HEADER_SIZE, "apfs btree node header")?;
Ok(Self {
flags: read_u16_le(bytes, 0)?,
level: read_u16_le(bytes, 2)?,
key_count: read_u32_le(bytes, 4)?,
table_space_offset: read_u16_le(bytes, 8)?,
table_space_length: read_u16_le(bytes, 10)?,
free_space_offset: read_u16_le(bytes, 12)?,
free_space_length: read_u16_le(bytes, 14)?,
key_free_list_offset: read_u16_le(bytes, 16)?,
key_free_list_length: read_u16_le(bytes, 18)?,
value_free_list_offset: read_u16_le(bytes, 20)?,
value_free_list_length: read_u16_le(bytes, 22)?,
})
}
}
pub(crate) fn bytes_to_cstring(bytes: &[u8]) -> String {
let end = bytes
.iter()
.position(|byte| *byte == 0)
.unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).to_string()
}
pub(crate) fn format_uuid_le(bytes: &[u8; 16]) -> String {
format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[3],
bytes[2],
bytes[1],
bytes[0],
bytes[5],
bytes[4],
bytes[7],
bytes[6],
bytes[8],
bytes[9],
bytes[10],
bytes[11],
bytes[12],
bytes[13],
bytes[14],
bytes[15],
)
}
pub(crate) fn apfs_role_names(role: u16) -> Vec<&'static str> {
let mut names = Vec::new();
for (mask, name) in [
(APFS_VOL_ROLE_SYSTEM, "system"),
(APFS_VOL_ROLE_USER, "user"),
(APFS_VOL_ROLE_RECOVERY, "recovery"),
(APFS_VOL_ROLE_VM, "vm"),
(APFS_VOL_ROLE_PREBOOT, "preboot"),
(APFS_VOL_ROLE_INSTALLER, "installer"),
(APFS_VOL_ROLE_DATA, "data"),
(APFS_VOL_ROLE_BASEBAND, "baseband"),
(APFS_VOL_ROLE_UPDATE, "update"),
(APFS_VOL_ROLE_XART, "xart"),
(APFS_VOL_ROLE_HARDWARE, "hardware"),
(APFS_VOL_ROLE_BACKUP, "backup"),
(APFS_VOL_ROLE_ENTERPRISE, "enterprise"),
(APFS_VOL_ROLE_PRELOGIN, "prelogin"),
] {
if (role & mask) != 0 {
names.push(name);
}
}
if names.is_empty() {
names.push("none");
}
names
}
pub(crate) fn fletcher64(data: &[u8]) -> u64 {
let mut sum1 = 0u64;
let mut sum2 = 0u64;
for chunk in data.chunks_exact(4) {
let word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]) as u64;
sum1 = sum1.wrapping_add(word);
sum2 = sum2.wrapping_add(sum1);
}
let checksum_low = 0xFFFF_FFFFu64 - ((sum1 + sum2) % 0xFFFF_FFFFu64);
let checksum_high = 0xFFFF_FFFFu64 - ((sum1 + checksum_low) % 0xFFFF_FFFFu64);
checksum_low | (checksum_high << 32)
}
pub(crate) fn read_slice<'a>(
bytes: &'a [u8], offset: usize, length: usize, what: &str,
) -> Result<&'a [u8]> {
let end = offset
.checked_add(length)
.ok_or_else(|| Error::invalid_range(format!("{what} offset overflow")))?;
bytes.get(offset..end).ok_or_else(|| {
Error::invalid_format(format!(
"{what} extends beyond the available APFS block data"
))
})
}
pub(crate) fn read_array<const N: usize>(bytes: &[u8], offset: usize) -> Result<[u8; N]> {
let slice = read_slice(bytes, offset, N, "apfs array")?;
let mut result = [0u8; N];
result.copy_from_slice(slice);
Ok(result)
}
pub(crate) fn read_u16_le(bytes: &[u8], offset: usize) -> Result<u16> {
Ok(u16::from_le_bytes(read_array(bytes, offset)?))
}
pub(crate) fn read_u32_le(bytes: &[u8], offset: usize) -> Result<u32> {
Ok(u32::from_le_bytes(read_array(bytes, offset)?))
}
pub(crate) fn read_u64_le(bytes: &[u8], offset: usize) -> Result<u64> {
Ok(u64::from_le_bytes(read_array(bytes, offset)?))
}
pub(crate) fn read_i64_le(bytes: &[u8], offset: usize) -> Result<i64> {
Ok(i64::from_le_bytes(read_array(bytes, offset)?))
}
fn read_u64_array<const N: usize>(bytes: &[u8], offset: usize) -> Result<[u64; N]> {
let mut values = [0u64; N];
for (index, slot) in values.iter_mut().enumerate() {
*slot = read_u64_le(bytes, offset + index * 8)?;
}
Ok(values)
}
fn bit_names_u64(flags: u64, mapping: &[(u64, &'static str)]) -> Vec<&'static str> {
mapping
.iter()
.filter_map(|(mask, name)| ((flags & *mask) != 0).then_some(*name))
.collect()
}
fn bit_names_u32(flags: u32, mapping: &[(u32, &'static str)]) -> Vec<&'static str> {
mapping
.iter()
.filter_map(|(mask, name)| ((flags & *mask) != 0).then_some(*name))
.collect()
}
fn require_len(bytes: &[u8], length: usize, what: &str) -> Result<()> {
if bytes.len() < length {
return Err(Error::invalid_format(format!(
"{what} requires at least {length} bytes"
)));
}
Ok(())
}
#[cfg(test)]
#[path = "ondisk_tests.rs"]
mod tests;