Skip to main content

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}