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
36/// Hard cap on assembled file size (allocation-bomb defense). A single image we
37/// would read in memory cannot legitimately exceed this; a DSTREAM `size` or an
38/// extent run beyond it is rejected loudly rather than allocated.
39const MAX_FILE_BYTES: u64 = 1 << 34; // 16 GiB
40
41/// One file extent (a `j_file_extent_val_t` plus its logical offset).
42#[derive(Debug, Clone, Copy)]
43#[non_exhaustive]
44pub struct FileExtent {
45 /// Logical offset within the file (from the record key).
46 pub logical_offset: u64,
47 /// Length in bytes (`len_and_flags & J_FILE_EXTENT_LEN_MASK`).
48 pub len: u64,
49 /// Starting physical block (0 = sparse hole).
50 pub phys_block_num: u64,
51}
52
53/// List the `FILE_EXTENT` records of a data stream (`stream_oid`, an inode's
54/// `private_id` for the main fork or a resource-fork dstream id), sorted by
55/// logical offset.
56///
57/// A `phys_block_num` of 0 is preserved as a sparse hole; callers zero-fill it.
58///
59/// # Errors
60/// [`crate::ApfsError::OmapUnresolved`] / [`crate::ApfsError::ChecksumMismatch`]
61/// / [`crate::ApfsError::CycleGuard`] / [`crate::ApfsError::Io`] on a
62/// structurally invalid omap/fs-tree or a read failure.
63pub fn list_extents<R: Read + Seek>(
64 reader: &mut R,
65 volume: &ApfsVolume,
66 stream_oid: u64,
67 block_size: usize,
68) -> crate::Result<Vec<FileExtent>> {
69 let mut out = Vec::new();
70 for_each_fs_record_for_oid(reader, volume, block_size, stream_oid, &mut |key, value| {
71 let (oid, ty) = decode_jkey(crate::bytes::le_u64(key, 0));
72 if ty != Some(RecordType::FileExtent) || oid != stream_oid {
73 return;
74 }
75 let logical_offset = crate::bytes::le_u64(key, OFF_FEXT_KEY_LOGICAL);
76 let len = crate::bytes::le_u64(value, OFF_FEXT_LEN_AND_FLAGS) & J_FILE_EXTENT_LEN_MASK;
77 let phys_block_num = crate::bytes::le_u64(value, OFF_FEXT_PHYS_BLOCK);
78 out.push(FileExtent {
79 logical_offset,
80 len,
81 phys_block_num,
82 });
83 })?;
84 out.sort_by_key(|e| e.logical_offset);
85 Ok(out)
86}
87
88/// Assemble the raw extent stream of `stream_oid` into a byte buffer, zero-filling
89/// sparse holes, then truncate/zero-extend to exactly `logical_size` bytes.
90///
91/// This is the *physical* read: it does not apply decmpfs. [`read_data`] uses it
92/// for an ordinary file and to fetch a decmpfs resource-fork stream.
93///
94/// # Errors
95/// [`crate::ApfsError::FieldOutOfRange`] if `logical_size` or an extent block
96/// number exceeds the sanity caps / the image, plus the structural errors of
97/// [`list_extents`].
98pub fn read_stream<R: Read + Seek>(
99 reader: &mut R,
100 volume: &ApfsVolume,
101 stream_oid: u64,
102 logical_size: u64,
103 block_size: usize,
104) -> crate::Result<Vec<u8>> {
105 if logical_size > MAX_FILE_BYTES {
106 return Err(crate::ApfsError::FieldOutOfRange {
107 structure: "j_dstream",
108 field: "size",
109 value: logical_size,
110 cap: MAX_FILE_BYTES,
111 });
112 }
113 let extents = list_extents(reader, volume, stream_oid, block_size)?;
114
115 let size = logical_size as usize;
116 let mut out = vec![0u8; size];
117 for ext in &extents {
118 // Only fill within the logical size; extents past EOF (alloced tail) are
119 // ignored, holes leave the pre-zeroed region untouched.
120 let start = ext.logical_offset as usize;
121 if start >= size || ext.phys_block_num == 0 {
122 continue;
123 }
124 let avail = size - start;
125 let copy_len = (ext.len as usize).min(avail);
126 // Range-check the physical span against the image before reading.
127 let byte_off = ext.phys_block_num.checked_mul(block_size as u64).ok_or(
128 crate::ApfsError::FieldOutOfRange {
129 structure: "j_file_extent",
130 field: "phys_block_num",
131 value: ext.phys_block_num,
132 cap: u64::MAX / block_size as u64,
133 },
134 )?;
135 reader.seek(std::io::SeekFrom::Start(byte_off))?;
136 reader.read_exact(&mut out[start..start + copy_len])?;
137 }
138 Ok(out)
139}
140
141/// Assemble a file's full byte content, applying transparent decmpfs compression
142/// when the inode carries a `com.apple.decmpfs` xattr.
143///
144/// For an ordinary file, the data stream (`inode.private_id`) is read extent by
145/// extent (sparse holes zero-filled) and truncated to the inode's logical
146/// `size`. For a transparently-compressed file, the decmpfs payload (inline in
147/// the xattr or in the `com.apple.ResourceFork` stream) is decoded via
148/// [`crate::compression`] — never the raw extent bytes.
149///
150/// # Errors
151/// [`crate::ApfsError::Decmpfs`] if a decmpfs file's payload cannot be decoded
152/// (a named codec/format error — never fabricated bytes); the structural errors
153/// of [`read_stream`] otherwise.
154pub fn read_data<R: Read + Seek>(
155 reader: &mut R,
156 volume: &ApfsVolume,
157 inode: &Inode,
158 block_size: usize,
159) -> crate::Result<Vec<u8>> {
160 // A compressed file is identified by a `com.apple.decmpfs` xattr.
161 if let Some(header) = crate::xattr::decmpfs_header(reader, volume, inode.oid, block_size)? {
162 return crate::compression::read_compressed(reader, volume, inode, &header, block_size);
163 }
164
165 let size = inode.size.unwrap_or(0);
166 read_stream(reader, volume, inode.private_id, size, block_size)
167}