Skip to main content

apfs_core/
xattr.rs

1//! Extended attributes (`APFS_TYPE_XATTR 4`, value `j_xattr_val_t`) and symlink
2//! targets.
3//!
4//! An xattr key is `j_xattr_key_t { j_key; u16 name_len; u8 name[] }` (the
5//! `name_len` includes the trailing NUL). The value is `j_xattr_val_t { u16
6//! flags; u16 xdata_len; u8 xdata[] }` (Apple *APFS Reference*). The `flags`
7//! word selects where the data lives:
8//!
9//! - **`XATTR_DATA_EMBEDDED` (0x0002)** — `xdata` holds the attribute value
10//!   inline.
11//! - **`XATTR_DATA_STREAM` (0x0001)** — `xdata` holds a `j_xattr_dstream_t`
12//!   (`u64 xattr_obj_id` = the dstream id, then a `j_dstream_t { u64 size; … }`);
13//!   the value bytes are the extents keyed by that dstream id.
14//!
15//! Forensically important named xattrs:
16//! - `com.apple.decmpfs` — transparent-compression header ([`crate::compression`]).
17//! - `com.apple.ResourceFork` — resource fork (non-embedded compressed payload),
18//!   stored as a stream xattr.
19//! - `com.apple.fs.symlink` — a symlink's target path, stored **embedded** in the
20//!   xattr value (verified against the real fixture: flags `0x6` =
21//!   `EMBEDDED | 0x4`, value `"Dir1/Beth.txt\0"`).
22
23use std::io::{Read, Seek};
24
25use crate::dir::for_each_fs_record_for_oid;
26use crate::fsrecord::{decode_jkey, RecordType};
27use crate::volume::ApfsVolume;
28
29/// `XATTR_DATA_STREAM` flag (the value is in a referenced data stream).
30pub const XATTR_DATA_STREAM: u16 = 0x0001;
31/// `XATTR_DATA_EMBEDDED` flag (the value is inline in the record).
32pub const XATTR_DATA_EMBEDDED: u16 = 0x0002;
33
34/// The `com.apple.decmpfs` xattr name (transparent compression header).
35pub const XATTR_NAME_DECMPFS: &str = "com.apple.decmpfs";
36/// The `com.apple.ResourceFork` xattr name (resource fork / compressed payload).
37pub const XATTR_NAME_RESOURCE_FORK: &str = "com.apple.ResourceFork";
38/// The `com.apple.fs.symlink` xattr name (symlink target path).
39pub const XATTR_NAME_SYMLINK: &str = "com.apple.fs.symlink";
40
41// j_xattr_key_t: name_len u16 follows the 8-byte j_key; name follows at @10.
42const OFF_XATTR_KEY_NAME_LEN: usize = 8;
43const OFF_XATTR_KEY_NAME: usize = 10;
44// j_xattr_val_t: flags u16 @0, xdata_len u16 @2, xdata @4.
45const OFF_XATTR_VAL_FLAGS: usize = 0;
46const OFF_XATTR_VAL_XDATA_LEN: usize = 2;
47const OFF_XATTR_VAL_XDATA: usize = 4;
48// j_xattr_dstream_t (the embedded value of a XATTR_DATA_STREAM xattr):
49//   u64 xattr_obj_id; then j_dstream_t { u64 size; … }.
50const OFF_XDSTREAM_OBJ_ID: usize = 0;
51const OFF_XDSTREAM_SIZE: usize = 8;
52
53/// Where an xattr's value lives.
54#[derive(Debug, Clone)]
55#[non_exhaustive]
56pub enum XattrValue {
57    /// The value is stored inline in the record.
58    Embedded(Vec<u8>),
59    /// The value is stored in a data stream: `(dstream_oid, logical_size)`. The
60    /// bytes are the `FILE_EXTENT` records keyed by `dstream_oid`
61    /// ([`crate::extent::read_stream`]).
62    Stream { dstream_oid: u64, size: u64 },
63}
64
65/// A parsed extended attribute.
66#[derive(Debug, Clone)]
67#[non_exhaustive]
68pub struct Xattr {
69    /// The attribute name (e.g. `com.apple.decmpfs`).
70    pub name: String,
71    /// The raw `j_xattr_val_t.flags` word.
72    pub flags: u16,
73    /// The attribute value (embedded bytes or a stream reference).
74    pub value: XattrValue,
75}
76
77/// Decode a single `XATTR` (key, value) record, or `None` if the key has no
78/// decodable name (a malformed/hostile record is skipped, never panics).
79fn parse_xattr(key: &[u8], value: &[u8]) -> Option<Xattr> {
80    let name_len = crate::bytes::le_u16(key, OFF_XATTR_KEY_NAME_LEN) as usize;
81    if name_len == 0 {
82        return None;
83    }
84    let name_bytes = key.get(OFF_XATTR_KEY_NAME..OFF_XATTR_KEY_NAME + name_len)?;
85    let name = decode_cstr(name_bytes);
86
87    let flags = crate::bytes::le_u16(value, OFF_XATTR_VAL_FLAGS);
88    let xdata_len = crate::bytes::le_u16(value, OFF_XATTR_VAL_XDATA_LEN) as usize;
89    let xdata = value
90        .get(OFF_XATTR_VAL_XDATA..OFF_XATTR_VAL_XDATA + xdata_len)
91        .unwrap_or(&[]);
92
93    let xvalue = if flags & XATTR_DATA_STREAM != 0 {
94        XattrValue::Stream {
95            dstream_oid: crate::bytes::le_u64(xdata, OFF_XDSTREAM_OBJ_ID),
96            size: crate::bytes::le_u64(xdata, OFF_XDSTREAM_SIZE),
97        }
98    } else {
99        XattrValue::Embedded(xdata.to_vec())
100    };
101    Some(Xattr {
102        name,
103        flags,
104        value: xvalue,
105    })
106}
107
108/// List all extended attributes of the inode `inode_oid`, scanning the volume
109/// fs-tree for `XATTR` records whose key oid is the inode.
110///
111/// # Errors
112/// [`crate::ApfsError::OmapUnresolved`] / [`crate::ApfsError::ChecksumMismatch`]
113/// / [`crate::ApfsError::CycleGuard`] / [`crate::ApfsError::Io`] on a
114/// structurally invalid omap/fs-tree or a read failure.
115pub fn list_xattrs<R: Read + Seek>(
116    reader: &mut R,
117    volume: &ApfsVolume,
118    inode_oid: u64,
119    block_size: usize,
120) -> crate::Result<Vec<Xattr>> {
121    let mut out = Vec::new();
122    for_each_fs_record_for_oid(reader, volume, block_size, inode_oid, &mut |key, value| {
123        let (oid, ty) = decode_jkey(crate::bytes::le_u64(key, 0));
124        if ty != Some(RecordType::Xattr) || oid != inode_oid {
125            return;
126        }
127        if let Some(x) = parse_xattr(key, value) {
128            out.push(x);
129        }
130    })?;
131    Ok(out)
132}
133
134/// Fetch a named xattr's value for an inode, or `None` if absent.
135///
136/// # Errors
137/// As [`list_xattrs`].
138pub fn get_xattr<R: Read + Seek>(
139    reader: &mut R,
140    volume: &ApfsVolume,
141    inode_oid: u64,
142    name: &str,
143    block_size: usize,
144) -> crate::Result<Option<XattrValue>> {
145    let xattrs = list_xattrs(reader, volume, inode_oid, block_size)?;
146    Ok(xattrs.into_iter().find(|x| x.name == name).map(|x| x.value))
147}
148
149/// Return the raw `com.apple.decmpfs` header bytes for an inode, if it is a
150/// transparently-compressed file. The decmpfs header is always embedded in the
151/// xattr (the bulk payload, when large, lives in the resource fork — see
152/// [`resource_fork`]).
153///
154/// # Errors
155/// As [`list_xattrs`].
156pub fn decmpfs_header<R: Read + Seek>(
157    reader: &mut R,
158    volume: &ApfsVolume,
159    inode_oid: u64,
160    block_size: usize,
161) -> crate::Result<Option<Vec<u8>>> {
162    match get_xattr(reader, volume, inode_oid, XATTR_NAME_DECMPFS, block_size)? {
163        // The decmpfs xattr is embedded; an unexpected stream form yields its
164        // (empty) embedded bytes, which the decoder then rejects loudly.
165        Some(XattrValue::Embedded(bytes)) => Ok(Some(bytes)),
166        Some(XattrValue::Stream { .. }) | None => Ok(None),
167    }
168}
169
170/// Read the inode's `com.apple.ResourceFork` value as bytes, if present.
171///
172/// The resource fork holds a non-embedded decmpfs payload. It is normally a
173/// stream xattr (`XATTR_DATA_STREAM`) whose dstream is read through the extent
174/// machinery; a small fork may be embedded.
175///
176/// # Errors
177/// As [`list_xattrs`], plus the structural errors of
178/// [`crate::extent::read_stream`] when the fork is stream-backed.
179pub fn resource_fork<R: Read + Seek>(
180    reader: &mut R,
181    volume: &ApfsVolume,
182    inode_oid: u64,
183    block_size: usize,
184) -> crate::Result<Option<Vec<u8>>> {
185    match get_xattr(
186        reader,
187        volume,
188        inode_oid,
189        XATTR_NAME_RESOURCE_FORK,
190        block_size,
191    )? {
192        Some(XattrValue::Embedded(bytes)) => Ok(Some(bytes)),
193        Some(XattrValue::Stream { dstream_oid, size }) => {
194            let bytes = crate::extent::read_stream(reader, volume, dstream_oid, size, block_size)?;
195            Ok(Some(bytes))
196        }
197        None => Ok(None),
198    }
199}
200
201/// Resolve a symlink's target path. APFS stores it in the `com.apple.fs.symlink`
202/// xattr value (embedded), as a NUL-terminated UTF-8 path. `None` if the inode is
203/// not a symlink (no such xattr).
204///
205/// # Errors
206/// As [`list_xattrs`].
207pub fn symlink_target<R: Read + Seek>(
208    reader: &mut R,
209    volume: &ApfsVolume,
210    inode_oid: u64,
211    block_size: usize,
212) -> crate::Result<Option<String>> {
213    match get_xattr(reader, volume, inode_oid, XATTR_NAME_SYMLINK, block_size)? {
214        Some(XattrValue::Embedded(bytes)) => Ok(Some(decode_cstr(&bytes))),
215        // A stream-backed symlink target is not a form macOS produces; surface
216        // None rather than guess (the caller can still list the raw xattr).
217        Some(XattrValue::Stream { .. }) | None => Ok(None),
218    }
219}
220
221/// Decode a NUL-terminated UTF-8 byte string. Bytes after the first NUL are
222/// dropped; invalid UTF-8 is replaced (never panics).
223fn decode_cstr(data: &[u8]) -> String {
224    let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
225    String::from_utf8_lossy(&data[..end]).into_owned()
226}