1#![forbid(unsafe_code)]
2
3use crate::{
18 not_supported, ForensicFs, FsDirEntry, FsError, FsFileType, FsMetadata, FsResult, FsTimestamp,
19};
20use apfs_core::volume::ApfsVolume;
21use apfs_core::ApfsContainer;
22use std::io::{Read, Seek, SeekFrom};
23
24const ROOT_INO: u64 = 2;
26
27pub struct ApfsForensicFs<R: Read + Seek> {
29 reader: R,
30 volume: ApfsVolume,
31 block_size: usize,
32}
33
34fn drec_file_type(flags: u16) -> FsFileType {
36 match flags & 0x0F {
37 4 => FsFileType::Directory,
38 10 => FsFileType::Symlink,
39 1 => FsFileType::Fifo,
40 2 => FsFileType::CharDevice,
41 6 => FsFileType::BlockDevice,
42 12 => FsFileType::Socket,
43 _ => FsFileType::RegularFile,
44 }
45}
46
47fn mode_file_type(mode: u16) -> FsFileType {
49 match mode & 0o170_000 {
50 0o040_000 => FsFileType::Directory,
51 0o120_000 => FsFileType::Symlink,
52 0o010_000 => FsFileType::Fifo,
53 0o020_000 => FsFileType::CharDevice,
54 0o060_000 => FsFileType::BlockDevice,
55 0o140_000 => FsFileType::Socket,
56 _ => FsFileType::RegularFile,
57 }
58}
59
60fn ns_ts(ns: u64) -> FsTimestamp {
62 FsTimestamp {
63 seconds: (ns / 1_000_000_000) as i64,
64 nanoseconds: (ns % 1_000_000_000) as u32,
65 }
66}
67
68impl<R: Read + Seek> ApfsForensicFs<R> {
69 pub fn new(reader: R) -> Result<Self, FsError> {
76 let mut container =
77 ApfsContainer::open(reader).map_err(|e| FsError::Corrupt(format!("not APFS: {e}")))?;
78 let block_size = container.superblock().block_size as usize;
79 let addrs = container
80 .volume_superblock_addrs()
81 .map_err(|e| FsError::Corrupt(format!("APFS volume resolution failed: {e}")))?;
82 let first = *addrs
83 .first()
84 .ok_or_else(|| FsError::Corrupt("APFS container exposes no volumes".to_string()))?;
85
86 let mut reader = container.into_reader();
88 let byte_off = first
89 .checked_mul(block_size as u64)
90 .ok_or_else(|| FsError::Corrupt("APFS volume offset overflow".to_string()))?;
91 reader
92 .seek(SeekFrom::Start(byte_off))
93 .map_err(FsError::Io)?;
94 let mut block = vec![0u8; block_size];
95 reader.read_exact(&mut block).map_err(FsError::Io)?;
96 let volume = ApfsVolume::parse(&block)
97 .map_err(|e| FsError::Corrupt(format!("APFS volume superblock: {e}")))?;
98
99 Ok(Self {
100 reader,
101 volume,
102 block_size,
103 })
104 }
105}
106
107impl<R: Read + Seek> ForensicFs for ApfsForensicFs<R> {
108 fn root_ino(&self) -> u64 {
109 ROOT_INO
110 }
111
112 fn read_dir(&mut self, ino: u64) -> FsResult<Vec<FsDirEntry>> {
113 let entries =
114 apfs_core::dir::list_dir(&mut self.reader, &self.volume, ino, self.block_size)
115 .map_err(|e| FsError::Io(std::io::Error::other(e.to_string())))?;
116 Ok(entries
117 .into_iter()
118 .map(|e| FsDirEntry {
119 inode: e.file_id,
120 name: e.name.into_bytes(),
121 file_type: drec_file_type(e.flags),
122 })
123 .collect())
124 }
125
126 fn lookup(&mut self, parent_ino: u64, name: &[u8]) -> FsResult<Option<u64>> {
127 let name = std::str::from_utf8(name)
128 .map_err(|_| FsError::NotFound("non-UTF-8 APFS name".to_string()))?;
129 apfs_core::dir::lookup_child(
130 &mut self.reader,
131 &self.volume,
132 parent_ino,
133 name,
134 self.block_size,
135 )
136 .map_err(|e| FsError::Io(std::io::Error::other(e.to_string())))
137 }
138
139 fn metadata(&mut self, ino: u64) -> FsResult<FsMetadata> {
140 let inode =
141 apfs_core::dir::load_inode(&mut self.reader, &self.volume, ino, self.block_size)
142 .map_err(|e| FsError::NotFound(format!("inode {ino}: {e}")))?;
143 let file_type = mode_file_type(inode.mode);
144 Ok(FsMetadata {
145 ino,
146 file_type,
147 mode: inode.mode & 0o7777,
148 uid: inode.uid,
149 gid: inode.gid,
150 size: inode.size.unwrap_or(0),
151 links_count: inode.nlink_or_nchildren.max(0).min(i32::from(u16::MAX)) as u16,
152 atime: ns_ts(inode.access_time),
153 mtime: ns_ts(inode.mod_time),
154 ctime: ns_ts(inode.change_time),
155 crtime: ns_ts(inode.create_time),
156 allocated: true,
157 })
158 }
159
160 fn read_file(&mut self, ino: u64) -> FsResult<Vec<u8>> {
161 let inode =
162 apfs_core::dir::load_inode(&mut self.reader, &self.volume, ino, self.block_size)
163 .map_err(|e| FsError::NotFound(format!("inode {ino}: {e}")))?;
164 if mode_file_type(inode.mode) == FsFileType::Directory {
165 return Err(not_supported("read_file on a directory"));
166 }
167 apfs_core::extent::read_data(&mut self.reader, &self.volume, &inode, self.block_size)
168 .map_err(|e| FsError::Io(std::io::Error::other(e.to_string())))
169 }
170
171 fn read_file_range(&mut self, ino: u64, offset: u64, len: u64) -> FsResult<Vec<u8>> {
172 let data = self.read_file(ino)?;
173 let start = (offset as usize).min(data.len());
174 let end = start.saturating_add(len as usize).min(data.len());
175 Ok(data[start..end].to_vec())
176 }
177
178 fn read_link(&mut self, _ino: u64) -> FsResult<Vec<u8>> {
179 Err(not_supported("APFS symlink targets not yet resolved"))
180 }
181
182 fn fs_info(&self) -> FsResult<serde_json::Value> {
183 Ok(serde_json::json!({
184 "type": "apfs",
185 "block_size": self.block_size,
186 }))
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use std::io::Cursor;
194
195 const IMG: &str = "/Users/4n6h4x0r/src/apfs-forensic/tests/data/apfs_fstree.bin";
200
201 fn open() -> Option<ApfsForensicFs<Cursor<Vec<u8>>>> {
202 let data = std::fs::read(IMG).ok()?;
203 ApfsForensicFs::new(Cursor::new(data)).ok()
204 }
205
206 #[test]
207 fn root_ino_is_2() {
208 let Some(fs) = open() else {
209 eprintln!("skip: apfs_fstree.bin unavailable");
210 return;
211 };
212 assert_eq!(fs.root_ino(), 2);
213 }
214
215 #[test]
216 fn root_lists_top_and_dir1() {
217 let Some(mut fs) = open() else {
218 eprintln!("skip");
219 return;
220 };
221 let names: Vec<String> = fs
222 .read_dir(2)
223 .unwrap()
224 .iter()
225 .map(FsDirEntry::name_str)
226 .collect();
227 assert!(names.contains(&"top.txt".to_string()), "got {names:?}");
228 assert!(names.contains(&"Dir1".to_string()), "got {names:?}");
229 }
230
231 #[test]
232 fn top_txt_resolves_to_inode_22() {
233 let Some(mut fs) = open() else {
234 eprintln!("skip");
235 return;
236 };
237 assert_eq!(fs.lookup(2, b"top.txt").unwrap(), Some(22));
238 }
239
240 #[test]
241 fn read_top_txt_matches_oracle() {
242 let Some(mut fs) = open() else {
243 eprintln!("skip");
244 return;
245 };
246 let ino = fs.lookup(2, b"top.txt").unwrap().unwrap();
247 assert_eq!(fs.read_file(ino).unwrap(), b"top level file\n");
248 }
249
250 #[test]
251 fn nested_secret_bin_reachable() {
252 let Some(mut fs) = open() else {
253 eprintln!("skip");
254 return;
255 };
256 let dir1 = fs.lookup(2, b"Dir1").unwrap().unwrap();
257 let sub = fs.lookup(dir1, b"Sub").unwrap().unwrap();
258 let secret = fs.lookup(sub, b"secret.bin").unwrap().unwrap();
259 assert_eq!(secret, 21);
260 assert_eq!(fs.metadata(secret).unwrap().size, 26);
261 }
262
263 #[test]
264 fn top_is_regular_dir1_is_dir() {
265 let Some(mut fs) = open() else {
266 eprintln!("skip");
267 return;
268 };
269 let top = fs.lookup(2, b"top.txt").unwrap().unwrap();
270 assert_eq!(fs.metadata(top).unwrap().file_type, FsFileType::RegularFile);
271 assert_eq!(fs.metadata(top).unwrap().size, 15);
272 let dir1 = fs.lookup(2, b"Dir1").unwrap().unwrap();
273 assert_eq!(fs.metadata(dir1).unwrap().file_type, FsFileType::Directory);
274 }
275
276 #[test]
277 fn fs_info_reports_apfs() {
278 let Some(fs) = open() else {
279 eprintln!("skip");
280 return;
281 };
282 assert_eq!(fs.fs_info().unwrap()["type"], "apfs");
283 }
284}