apfs_core/extent.rs
1//! File extents (`APFS_TYPE_FILE_EXTENT 8`, value `j_file_extent_val_t`) and
2//! file byte assembly.
3//!
4//! A file's content is described by a data stream (the inode's `private_id`,
5//! also surfaced as the `INO_EXT_TYPE_DSTREAM` xfield giving the logical `size`)
6//! plus a series of `FILE_EXTENT` records keyed by `(private_id, FILE_EXTENT,
7//! logical_offset)`. Each `j_file_extent_val_t { u64 len_and_flags; u64
8//! phys_block_num; u64 crypto_id }` (Apple *APFS Reference*) carries the extent
9//! length (`len_and_flags & J_FILE_EXTENT_LEN_MASK 0x00ffffffffffffff`, a
10//! multiple of the block size) and the starting physical block. A
11//! `phys_block_num` of 0 is a **sparse hole** — it reads back as zeroes.
12//!
13//! [`read_data`] assembles extents in logical order into plaintext bytes,
14//! truncating to the inode's DSTREAM `size`; if the inode carries a
15//! `com.apple.decmpfs` xattr, [`crate::compression`] is applied transparently
16//! over the decmpfs payload (inline xattr or `com.apple.ResourceFork` stream)
17//! instead of the regular extent stream.
18
19use std::io::{Read, Seek};
20
21use crate::dir::for_each_fs_record_for_oid;
22use crate::fsrecord::{decode_jkey, RecordType};
23use crate::inode::Inode;
24use crate::volume::ApfsVolume;
25
26/// Mask for the extent length within `len_and_flags` (`J_FILE_EXTENT_LEN_MASK`).
27pub const J_FILE_EXTENT_LEN_MASK: u64 = 0x00ff_ffff_ffff_ffff;
28
29// j_file_extent_key_t: the logical offset is the second u64 of the key, after
30// the 8-byte j_key header.
31const OFF_FEXT_KEY_LOGICAL: usize = 8;
32// j_file_extent_val_t field offsets.
33const OFF_FEXT_LEN_AND_FLAGS: usize = 0;
34const OFF_FEXT_PHYS_BLOCK: usize = 8;
35// j_file_extent_val_t.crypto_id -- the AES-XTS tweak for this extent's data on
36// an encrypted volume. It is STORED rather than derived from position because
37// APFS may relocate an extent without re-encrypting it, at which point the
38// block address stops predicting the tweak.
39const OFF_FEXT_CRYPTO_ID: usize = 16;
40
41/// Hard cap on assembled file size (allocation-bomb defense). A single image we
42/// would read in memory cannot legitimately exceed this; a DSTREAM `size` or an
43/// extent run beyond it is rejected loudly rather than allocated.
44const MAX_FILE_BYTES: u64 = 1 << 34; // 16 GiB
45
46/// One file extent (a `j_file_extent_val_t` plus its logical offset).
47#[derive(Debug, Clone, Copy)]
48#[non_exhaustive]
49pub struct FileExtent {
50 /// Logical offset within the file (from the record key).
51 pub logical_offset: u64,
52 /// Length in bytes (`len_and_flags & J_FILE_EXTENT_LEN_MASK`).
53 pub len: u64,
54 /// Starting physical block (0 = sparse hole).
55 pub phys_block_num: u64,
56 /// `crypto_id` — the AES-XTS tweak of this extent's first block on an
57 /// encrypted volume. Zero means the data is not encrypted.
58 pub crypto_id: u64,
59}
60
61/// List the `FILE_EXTENT` records of a data stream (`stream_oid`, an inode's
62/// `private_id` for the main fork or a resource-fork dstream id), sorted by
63/// logical offset.
64///
65/// A `phys_block_num` of 0 is preserved as a sparse hole; callers zero-fill it.
66///
67/// # Errors
68/// [`crate::ApfsError::OmapUnresolved`] / [`crate::ApfsError::ChecksumMismatch`]
69/// / [`crate::ApfsError::CycleGuard`] / [`crate::ApfsError::Io`] on a
70/// structurally invalid omap/fs-tree or a read failure.
71pub fn list_extents<R: Read + Seek>(
72 reader: &mut R,
73 volume: &ApfsVolume,
74 stream_oid: u64,
75 block_size: usize,
76) -> crate::Result<Vec<FileExtent>> {
77 let mut out = Vec::new();
78 for_each_fs_record_for_oid(reader, volume, block_size, stream_oid, &mut |key, value| {
79 let (oid, ty) = decode_jkey(crate::bytes::le_u64(key, 0));
80 if ty != Some(RecordType::FileExtent) || oid != stream_oid {
81 return;
82 }
83 let logical_offset = crate::bytes::le_u64(key, OFF_FEXT_KEY_LOGICAL);
84 let len = crate::bytes::le_u64(value, OFF_FEXT_LEN_AND_FLAGS) & J_FILE_EXTENT_LEN_MASK;
85 let phys_block_num = crate::bytes::le_u64(value, OFF_FEXT_PHYS_BLOCK);
86 out.push(FileExtent {
87 logical_offset,
88 len,
89 phys_block_num,
90 crypto_id: crate::bytes::le_u64(value, OFF_FEXT_CRYPTO_ID),
91 });
92 })?;
93 out.sort_by_key(|e| e.logical_offset);
94 Ok(out)
95}
96
97/// Assemble the raw extent stream of `stream_oid` into a byte buffer, zero-filling
98/// sparse holes, then truncate/zero-extend to exactly `logical_size` bytes.
99///
100/// This is the *physical* read: it does not apply decmpfs. [`read_data`] uses it
101/// for an ordinary file and to fetch a decmpfs resource-fork stream.
102///
103/// # Errors
104/// [`crate::ApfsError::FieldOutOfRange`] if `logical_size` or an extent block
105/// number exceeds the sanity caps / the image, plus the structural errors of
106/// [`list_extents`].
107pub fn read_stream<R: Read + Seek>(
108 reader: &mut R,
109 volume: &ApfsVolume,
110 stream_oid: u64,
111 logical_size: u64,
112 block_size: usize,
113) -> crate::Result<Vec<u8>> {
114 if logical_size > MAX_FILE_BYTES {
115 return Err(crate::ApfsError::FieldOutOfRange {
116 structure: "j_dstream",
117 field: "size",
118 value: logical_size,
119 cap: MAX_FILE_BYTES,
120 });
121 }
122 let extents = list_extents(reader, volume, stream_oid, block_size)?;
123
124 let size = logical_size as usize;
125 let mut out = vec![0u8; size];
126 for ext in &extents {
127 // Only fill within the logical size; extents past EOF (alloced tail) are
128 // ignored, holes leave the pre-zeroed region untouched.
129 let start = ext.logical_offset as usize;
130 if start >= size || ext.phys_block_num == 0 {
131 continue;
132 }
133 let avail = size - start;
134 let copy_len = (ext.len as usize).min(avail);
135 // Range-check the physical span against the image before reading.
136 let byte_off = ext.phys_block_num.checked_mul(block_size as u64).ok_or(
137 crate::ApfsError::FieldOutOfRange {
138 structure: "j_file_extent",
139 field: "phys_block_num",
140 value: ext.phys_block_num,
141 cap: u64::MAX / block_size as u64,
142 },
143 )?;
144 reader.seek(std::io::SeekFrom::Start(byte_off))?;
145
146 match volume.vek() {
147 Some(vek) if ext.crypto_id != 0 => {
148 // AES-XTS is a SECTOR transform: block j within a sector is
149 // whitened by tweak * alpha^j, so a partial sector cannot be
150 // decrypted on its own. Reading only the logical bytes -- a
151 // sound optimisation for plaintext -- silently yields the first
152 // 16 bytes correct and garbage after, because the short tail
153 // takes the ciphertext-stealing path. Read WHOLE blocks, then
154 // slice.
155 let nblocks = copy_len.div_ceil(block_size);
156 let mut tmp = vec![0u8; nblocks * block_size];
157 reader.read_exact(&mut tmp)?;
158 // The tweak advances one per BLOCK across the extent, so each
159 // block is decrypted with crypto_id + its index.
160 for (i, blk) in tmp.chunks_mut(block_size).enumerate() {
161 crate::encryption::decrypt_volume_area(
162 blk,
163 vek,
164 ext.crypto_id.saturating_add(i as u64),
165 block_size,
166 );
167 }
168 out[start..start + copy_len].copy_from_slice(&tmp[..copy_len]);
169 }
170 _ => reader.read_exact(&mut out[start..start + copy_len])?,
171 }
172 }
173 Ok(out)
174}
175
176/// Assemble a file's full byte content, applying transparent decmpfs compression
177/// when the inode carries a `com.apple.decmpfs` xattr.
178///
179/// For an ordinary file, the data stream (`inode.private_id`) is read extent by
180/// extent (sparse holes zero-filled) and truncated to the inode's logical
181/// `size`. For a transparently-compressed file, the decmpfs payload (inline in
182/// the xattr or in the `com.apple.ResourceFork` stream) is decoded via
183/// [`crate::compression`] — never the raw extent bytes.
184///
185/// # Errors
186/// [`crate::ApfsError::Decmpfs`] if a decmpfs file's payload cannot be decoded
187/// (a named codec/format error — never fabricated bytes); the structural errors
188/// of [`read_stream`] otherwise.
189pub fn read_data<R: Read + Seek>(
190 reader: &mut R,
191 volume: &ApfsVolume,
192 inode: &Inode,
193 block_size: usize,
194) -> crate::Result<Vec<u8>> {
195 // A compressed file is identified by a `com.apple.decmpfs` xattr.
196 if let Some(header) = crate::xattr::decmpfs_header(reader, volume, inode.oid, block_size)? {
197 return crate::compression::read_compressed(reader, volume, inode, &header, block_size);
198 }
199
200 let size = inode.size.unwrap_or(0);
201 read_stream(reader, volume, inode.private_id, size, block_size)
202}