apfs_core/volume.rs
1//! Volume superblock (`apfs_superblock_t`, magic `APFS_MAGIC = 'BSPA'` →
2//! "APSB" → LE `0x42535041`) and per-volume navigation roots.
3//!
4//! Each volume's APSB (Apple *APFS Reference*, `apfs_superblock_t`) carries the
5//! volume's own object map (`apfs_omap_oid`), the file-system tree root
6//! (`apfs_root_tree_oid`, a virtual oid resolved through the volume omap), the
7//! extent-reference tree (`apfs_extentref_tree_oid`), the snapshot-metadata tree
8//! (`apfs_snap_meta_tree_oid`), the volume role/flags, `apfs_volname`, and
9//! `apfs_fs_index`.
10//!
11//! Field offsets (little-endian on disk), after the 32-byte `obj_phys_t` header
12//! — verified verbatim against libfsapfs `fsapfs_volume_superblock` and the raw
13//! self-minted fixture:
14//!
15//! | off | size | field |
16//! |-----|------|--------------------------------------|
17//! | 32 | 4 | `apfs_magic` ("APSB") |
18//! | 36 | 4 | `apfs_fs_index` |
19//! | 40 | 8 | `apfs_features` (compatible) |
20//! | 48 | 8 | `apfs_readonly_compatible_features` |
21//! | 56 | 8 | `apfs_incompatible_features` |
22//! | 116 | 4 | `apfs_root_tree_type` |
23//! | 128 | 8 | `apfs_omap_oid` (volume omap block) |
24//! | 136 | 8 | `apfs_root_tree_oid` (virtual) |
25//! | 144 | 8 | `apfs_extentref_tree_oid` |
26//! | 152 | 8 | `apfs_snap_meta_tree_oid` |
27//! | 240 | 16 | `apfs_vol_uuid` |
28//! | 256 | 8 | `apfs_fs_flags` |
29//! | 704 | 256 | `apfs_volname[APFS_VOLNAME_LEN]` |
30
31use crate::object::{fletcher64_checksum, fletcher64_stored, ObjPhys};
32
33/// Volume superblock magic `APFS_MAGIC` ('BSPA', "APSB" in a hex dump).
34pub const APFS_MAGIC: u32 = 0x4253_5041;
35
36/// Object type code `OBJECT_TYPE_FS 0xd` — the APSB object type (low 16 bits of
37/// `o_type`).
38const OBJECT_TYPE_FS: u16 = 0xd;
39
40// `apfs_superblock_t` field offsets after the 32-byte `obj_phys_t` header.
41const OFF_MAGIC: usize = 32;
42const OFF_FS_INDEX: usize = 36;
43const OFF_FEATURES: usize = 40;
44const OFF_RO_COMPAT_FEATURES: usize = 48;
45const OFF_INCOMPAT_FEATURES: usize = 56;
46const OFF_ROOT_TREE_TYPE: usize = 116;
47const OFF_OMAP_OID: usize = 128;
48const OFF_ROOT_TREE_OID: usize = 136;
49const OFF_EXTENTREF_TREE_OID: usize = 144;
50const OFF_SNAP_META_TREE_OID: usize = 152;
51const OFF_VOL_UUID: usize = 240;
52const OFF_FS_FLAGS: usize = 256;
53const OFF_VOLNAME: usize = 704;
54
55/// `APFS_VOLNAME_LEN` (Apple) — the fixed `apfs_volname` field size in bytes.
56const VOLNAME_LEN: usize = 256;
57
58/// Minimum readable APSB length: header through the volume name field.
59const APSB_MIN_LEN: usize = OFF_VOLNAME + VOLNAME_LEN;
60
61/// Build the `apfs_superblock` [`crate::ApfsError::UnexpectedObjectType`] error
62/// (`expected`/`found` are the offending values) — shared by the short-block,
63/// wrong-type, and wrong-magic guards so the error shape lives in one place.
64fn apsb_bad_type(expected: u32, found: u32) -> crate::ApfsError {
65 crate::ApfsError::UnexpectedObjectType {
66 structure: "apfs_superblock",
67 expected,
68 found,
69 }
70}
71
72/// A parsed volume superblock (subset; `#[non_exhaustive]` for additive growth).
73///
74/// The fs-tree navigation entry points ([`Self::root_tree_oid`] /
75/// [`Self::omap_oid`]) are consumed by [`crate::dir`] for name→inode path
76/// resolution.
77#[derive(Debug, Clone)]
78#[non_exhaustive]
79pub struct ApfsVolume {
80 oid: u64,
81 xid: u64,
82 fs_index: u32,
83 features: u64,
84 readonly_compatible_features: u64,
85 incompatible_features: u64,
86 root_tree_type: u32,
87 omap_oid: u64,
88 root_tree_oid: u64,
89 extentref_tree_oid: u64,
90 snap_meta_tree_oid: u64,
91 uuid: [u8; 16],
92 fs_flags: u64,
93 name: String,
94 /// Volume encryption key, once a password has unlocked it.
95 ///
96 /// Lives on the volume because every read path already receives one, so an
97 /// encrypted volume becomes readable without changing a single public
98 /// signature -- callers that never touch encryption are unaffected.
99 vek: Option<[u8; 32]>,
100}
101
102impl ApfsVolume {
103 /// Parse and validate an APSB block (magic-by-type + signature + Fletcher-64
104 /// checksum) before trusting any field.
105 ///
106 /// # Errors
107 /// [`crate::ApfsError::UnexpectedObjectType`] on a short block, a non-FS
108 /// object type, or a wrong `apfs_magic` signature (carrying the offending
109 /// value); [`crate::ApfsError::ChecksumMismatch`] on a Fletcher-64 failure.
110 pub fn parse(block: &[u8]) -> crate::Result<Self> {
111 if block.len() < APSB_MIN_LEN {
112 return Err(apsb_bad_type(APFS_MAGIC, 0));
113 }
114 // Object-type gate: the block must be an FS (volume superblock) object.
115 let Some(hdr) = ObjPhys::parse(block) else {
116 // cov:unreachable: len checked >= APSB_MIN_LEN > OBJ_PHYS_LEN, so
117 // ObjPhys::parse (None only on len < OBJ_PHYS_LEN) is always Some.
118 return Err(apsb_bad_type(APFS_MAGIC, 0)); // cov:unreachable
119 };
120 if hdr.obj_type() != OBJECT_TYPE_FS {
121 return Err(apsb_bad_type(u32::from(OBJECT_TYPE_FS), hdr.obj_type_raw));
122 }
123
124 // Signature gate: apfs_magic == "APSB".
125 let magic = crate::bytes::le_u32(block, OFF_MAGIC);
126 if magic != APFS_MAGIC {
127 return Err(apsb_bad_type(APFS_MAGIC, magic));
128 }
129
130 // Checksum gate before trusting the tree oids.
131 let stored = fletcher64_stored(block);
132 let computed = fletcher64_checksum(block);
133 if stored != computed {
134 return Err(crate::ApfsError::ChecksumMismatch {
135 block: hdr.oid,
136 stored,
137 computed,
138 });
139 }
140
141 // Volume name: a NUL-terminated UTF-8 string within the 256-byte field.
142 let name = decode_volname(block, OFF_VOLNAME, VOLNAME_LEN);
143
144 Ok(Self {
145 oid: hdr.oid,
146 xid: hdr.xid,
147 fs_index: crate::bytes::le_u32(block, OFF_FS_INDEX),
148 features: crate::bytes::le_u64(block, OFF_FEATURES),
149 readonly_compatible_features: crate::bytes::le_u64(block, OFF_RO_COMPAT_FEATURES),
150 incompatible_features: crate::bytes::le_u64(block, OFF_INCOMPAT_FEATURES),
151 root_tree_type: crate::bytes::le_u32(block, OFF_ROOT_TREE_TYPE),
152 omap_oid: crate::bytes::le_u64(block, OFF_OMAP_OID),
153 root_tree_oid: crate::bytes::le_u64(block, OFF_ROOT_TREE_OID),
154 extentref_tree_oid: crate::bytes::le_u64(block, OFF_EXTENTREF_TREE_OID),
155 snap_meta_tree_oid: crate::bytes::le_u64(block, OFF_SNAP_META_TREE_OID),
156 uuid: crate::bytes::arr::<16>(block, OFF_VOL_UUID),
157 fs_flags: crate::bytes::le_u64(block, OFF_FS_FLAGS),
158 name,
159 vek: None,
160 })
161 }
162
163 /// The volume superblock object id (`nx_o.o_oid`).
164 #[must_use]
165 pub fn oid(&self) -> u64 {
166 self.oid
167 }
168
169 /// The transaction id of this volume superblock (`nx_o.o_xid`). Used as the
170 /// xid for resolving virtual fs-tree oids through the volume omap.
171 #[must_use]
172 pub fn xid(&self) -> u64 {
173 self.xid
174 }
175
176 /// `apfs_fs_index` — this volume's index within the container's `nx_fs_oid[]`.
177 #[must_use]
178 pub fn fs_index(&self) -> u32 {
179 self.fs_index
180 }
181
182 /// `apfs_features` — compatible feature flags.
183 #[must_use]
184 pub fn features(&self) -> u64 {
185 self.features
186 }
187
188 /// `apfs_readonly_compatible_features` — read-only-compatible feature flags.
189 #[must_use]
190 pub fn readonly_compatible_features(&self) -> u64 {
191 self.readonly_compatible_features
192 }
193
194 /// `apfs_incompatible_features` — incompatible feature flags (the
195 /// case-insensitivity / normalization bits affect directory-name matching).
196 #[must_use]
197 pub fn incompatible_features(&self) -> u64 {
198 self.incompatible_features
199 }
200
201 /// `apfs_root_tree_type` — the storage-flag + object-type word of the
202 /// file-system root tree (the root tree is virtual, so its high bits carry
203 /// the virtual storage flag).
204 #[must_use]
205 pub fn root_tree_type(&self) -> u32 {
206 self.root_tree_type
207 }
208
209 /// Supply the volume encryption key recovered from a password.
210 ///
211 /// With it set, B-tree nodes flagged `OMAP_VAL_ENCRYPTED` and file extents
212 /// are decrypted transparently on read; without it an encrypted volume
213 /// fails its checksum rather than returning plausible garbage.
214 pub fn set_vek(&mut self, vek: [u8; 32]) {
215 self.vek = Some(vek);
216 }
217
218 /// The volume encryption key, if one has been supplied.
219 #[must_use]
220 pub fn vek(&self) -> Option<&[u8; 32]> {
221 self.vek.as_ref()
222 }
223
224 /// `apfs_omap_oid` — the block address of this volume's object map
225 /// (`omap_phys_t`, a physical object). The fs-tree's virtual oids resolve
226 /// through this omap.
227 #[must_use]
228 pub fn omap_oid(&self) -> u64 {
229 self.omap_oid
230 }
231
232 /// Return this volume superblock with its `omap_oid` replaced.
233 ///
234 /// A *snapshot's* frozen `apfs_superblock_t` carries `apfs_omap_oid == 0`:
235 /// snapshots have no object map of their own and are read through the
236 /// **live** volume's omap, resolved at the snapshot's `xid` (the omap B-tree
237 /// retains historical `(oid, xid) → paddr` mappings that snapshots pin). A
238 /// point-in-time view therefore keeps the frozen superblock's `root_tree_oid`
239 /// and `xid` but borrows the live volume's omap. See
240 /// [`crate::snapshot::mount_snapshot`].
241 #[must_use]
242 pub fn with_omap_oid(mut self, omap_oid: u64) -> Self {
243 self.omap_oid = omap_oid;
244 self
245 }
246
247 /// `apfs_root_tree_oid` — the **virtual** oid of the file-system tree root
248 /// (`FSTREE`). Resolve it through the volume omap ([`Self::omap_oid`]) at
249 /// [`Self::xid`] to get the root node's physical block address.
250 #[must_use]
251 pub fn root_tree_oid(&self) -> u64 {
252 self.root_tree_oid
253 }
254
255 /// `apfs_extentref_tree_oid` — the extent-reference tree oid.
256 #[must_use]
257 pub fn extentref_tree_oid(&self) -> u64 {
258 self.extentref_tree_oid
259 }
260
261 /// `apfs_snap_meta_tree_oid` — the snapshot-metadata tree oid.
262 #[must_use]
263 pub fn snap_meta_tree_oid(&self) -> u64 {
264 self.snap_meta_tree_oid
265 }
266
267 /// `apfs_vol_uuid` — the volume UUID.
268 #[must_use]
269 pub fn uuid(&self) -> [u8; 16] {
270 self.uuid
271 }
272
273 /// `apfs_fs_flags` — volume flags (e.g. encryption state bits).
274 #[must_use]
275 pub fn fs_flags(&self) -> u64 {
276 self.fs_flags
277 }
278
279 /// The volume name (`apfs_volname`).
280 #[must_use]
281 pub fn name(&self) -> &str {
282 &self.name
283 }
284}
285
286/// Decode a fixed-width, NUL-terminated UTF-8 volume name field. Bytes after the
287/// first NUL (or the whole field if there is none) are dropped; invalid UTF-8 is
288/// replaced (never panics).
289fn decode_volname(block: &[u8], offset: usize, len: usize) -> String {
290 let Some(field) = block.get(offset..offset + len) else {
291 return String::new(); // cov:unreachable: caller checks block.len() >= APSB_MIN_LEN
292 };
293 let end = field.iter().position(|&b| b == 0).unwrap_or(field.len());
294 String::from_utf8_lossy(&field[..end]).into_owned()
295}