Skip to main content

apfs_core/
fsrecord.rs

1//! File-system record keys (`j_key_t`) and record-type dispatch.
2//!
3//! Every file-system record's key begins with `j_key_t { u64 obj_id_and_type }`
4//! (Apple *APFS Reference*). The top 4 bits are the record type
5//! (`OBJ_TYPE_SHIFT 60`, `OBJ_TYPE_MASK 0xf000000000000000`); the low 60 bits
6//! are the object id (`OBJ_ID_MASK 0x0fffffffffffffff`).
7//!
8//! Record types (`j_obj_types`, verbatim numeric values from Apple):
9//! `SNAP_METADATA 1`, `EXTENT 2`, `INODE 3`, `XATTR 4`, `SIBLING_LINK 5`,
10//! `DSTREAM_ID 6`, `CRYPTO_STATE 7`, `FILE_EXTENT 8`, `DIR_REC 9`,
11//! `DIR_STATS 10`, `SNAP_NAME 11`, `SIBLING_MAP 12`, `FILE_INFO 13`.
12//!
13//! Variable records carry extended fields (xfields) after the fixed value: an
14//! `xf_blob { u16 xf_num_exts; u16 xf_used_data; u8 xf_data[] }`. `xf_data`
15//! begins with `xf_num_exts` 4-byte descriptors (`x_field_t { u8 x_type;
16//! u8 x_flags; u16 x_size }`), followed by the field value data — each value is
17//! **8-byte aligned** (Apple). Common fields: `INO_EXT_TYPE_NAME 4` (filename),
18//! `INO_EXT_TYPE_DSTREAM 8` (data-stream attribute → file size),
19//! `INO_EXT_TYPE_DOCUMENT_ID 3`. Layout verified verbatim against the Apple
20//! reference + libfsapfs format spec.
21
22/// APFS file-system record types (`j_obj_types`).
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum RecordType {
26    SnapMetadata = 1,
27    Extent = 2,
28    Inode = 3,
29    Xattr = 4,
30    SiblingLink = 5,
31    DstreamId = 6,
32    CryptoState = 7,
33    FileExtent = 8,
34    DirRec = 9,
35    DirStats = 10,
36    SnapName = 11,
37    SiblingMap = 12,
38    FileInfo = 13,
39}
40
41impl RecordType {
42    /// Map a 4-bit `j_obj_types` value to a [`RecordType`]; `None` for an
43    /// undefined type code (0, 14, 15).
44    #[must_use]
45    pub fn from_u4(value: u8) -> Option<Self> {
46        Some(match value {
47            1 => RecordType::SnapMetadata,
48            2 => RecordType::Extent,
49            3 => RecordType::Inode,
50            4 => RecordType::Xattr,
51            5 => RecordType::SiblingLink,
52            6 => RecordType::DstreamId,
53            7 => RecordType::CryptoState,
54            8 => RecordType::FileExtent,
55            9 => RecordType::DirRec,
56            10 => RecordType::DirStats,
57            11 => RecordType::SnapName,
58            12 => RecordType::SiblingMap,
59            13 => RecordType::FileInfo,
60            _ => return None,
61        })
62    }
63}
64
65/// Top-4-bit type shift in `j_key.obj_id_and_type`.
66pub const OBJ_TYPE_SHIFT: u64 = 60;
67/// Low-60-bit object-id mask.
68pub const OBJ_ID_MASK: u64 = 0x0fff_ffff_ffff_ffff;
69
70/// Decode a `j_key_t.obj_id_and_type` into `(object_id, record_type)`.
71///
72/// The object id is always returned (even for an undefined type) so a caller
73/// reporting an unrecognized record still has the offending oid (fleet "show the
74/// value" rule).
75#[must_use]
76pub fn decode_jkey(obj_id_and_type: u64) -> (u64, Option<RecordType>) {
77    let oid = obj_id_and_type & OBJ_ID_MASK;
78    #[allow(clippy::cast_possible_truncation)]
79    let ty = ((obj_id_and_type >> OBJ_TYPE_SHIFT) & 0xf) as u8;
80    (oid, RecordType::from_u4(ty))
81}
82
83// xf_blob header field offsets.
84const OFF_XF_NUM_EXTS: usize = 0;
85const OFF_XF_USED_DATA: usize = 2;
86/// First `x_field_t` descriptor begins after the 4-byte `xf_blob` header.
87const XF_DESC_OFF: usize = 4;
88/// `x_field_t` descriptor length (`x_type` u8, `x_flags` u8, `x_size` u16).
89const XF_DESC_LEN: usize = 4;
90
91/// Hard cap on `xf_num_exts` — an inode/drec value cannot hold more than a few
92/// hundred extended fields; cap well above any legal value to reject an
93/// allocation-bomb count without rejecting a legal blob.
94const MAX_XF_NUM_EXTS: usize = 4096;
95
96/// Walk an `xf_blob` extended-field area, returning `(x_type, value_bytes)` per
97/// field (bounds-checked, never panics).
98///
99/// Each value slice is 8-byte aligned within `xf_data` per the Apple spec; an
100/// out-of-bounds descriptor or value (a hostile/short blob) ends the walk early,
101/// yielding only the fields that fully fit.
102#[must_use]
103pub fn parse_xfields(data: &[u8]) -> Vec<(u8, &[u8])> {
104    if data.len() < XF_DESC_OFF {
105        return Vec::new();
106    }
107    let num = crate::bytes::le_u16(data, OFF_XF_NUM_EXTS) as usize;
108    let _used = crate::bytes::le_u16(data, OFF_XF_USED_DATA);
109    let num = num.min(MAX_XF_NUM_EXTS);
110
111    // The descriptor table is `num` 4-byte entries; value data follows it.
112    let Some(desc_end) = num
113        .checked_mul(XF_DESC_LEN)
114        .and_then(|t| t.checked_add(XF_DESC_OFF))
115    else {
116        return Vec::new(); // cov:unreachable: num capped at MAX_XF_NUM_EXTS
117    };
118    // If the descriptor table itself does not fit, there are no decodable fields.
119    if desc_end > data.len() {
120        return Vec::new();
121    }
122
123    let mut out = Vec::with_capacity(num);
124    let mut value_off = desc_end;
125    for i in 0..num {
126        let d = XF_DESC_OFF + i * XF_DESC_LEN;
127        let x_type = data[d]; // d < desc_end <= data.len(), in bounds
128        let x_size = crate::bytes::le_u16(data, d + 2) as usize;
129
130        // The value slice [value_off, value_off + x_size) must lie within the
131        // blob; a hostile size ends the walk rather than over-reading.
132        let Some(end) = value_off.checked_add(x_size) else {
133            break; // cov:unreachable: u16 size + usize off cannot overflow usize
134        };
135        let Some(value) = data.get(value_off..end) else {
136            break;
137        };
138        out.push((x_type, value));
139
140        // Advance to the next 8-byte-aligned value boundary.
141        let aligned = (x_size + 7) & !7;
142        let Some(next) = value_off.checked_add(aligned) else {
143            break; // cov:unreachable: aligned size cannot overflow a valid blob
144        };
145        value_off = next;
146    }
147    out
148}