1pub mod btree;
2pub mod catalog;
3pub mod error;
4pub mod extents;
5pub mod fletcher;
6pub mod object;
7pub mod omap;
8pub mod superblock;
9
10pub use error::{ApfsError, Result};
11
12use std::io::{Read, Seek, Write};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum EntryKind {
17 File,
18 Directory,
19 Symlink,
20}
21
22#[derive(Debug, Clone)]
24pub struct DirEntry {
25 pub name: String,
26 pub oid: u64,
27 pub kind: EntryKind,
28 pub size: u64,
29 pub create_time: i64,
30 pub modify_time: i64,
31}
32
33#[derive(Debug, Clone)]
35pub struct FileStat {
36 pub oid: u64,
37 pub kind: EntryKind,
38 pub size: u64,
39 pub create_time: i64,
40 pub modify_time: i64,
41 pub uid: u32,
42 pub gid: u32,
43 pub mode: u16,
44 pub nlink: u32,
45}
46
47#[derive(Debug, Clone)]
49pub struct WalkEntry {
50 pub path: String,
51 pub entry: DirEntry,
52}
53
54#[derive(Debug, Clone)]
56pub struct VolumeInfo {
57 pub name: String,
58 pub block_size: u32,
59 pub num_files: u64,
60 pub num_directories: u64,
61 pub num_symlinks: u64,
62}
63
64pub struct ApfsVolume<R: Read + Seek> {
66 reader: R,
67 block_size: u32,
68 vol_omap_root_block: u64,
69 catalog_root_block: u64,
70 info: VolumeInfo,
71}
72
73impl<R: Read + Seek> ApfsVolume<R> {
74 pub fn open(mut reader: R) -> Result<Self> {
86 let nxsb = superblock::read_nxsb(&mut reader)?;
88 let nxsb = superblock::find_latest_nxsb(&mut reader, &nxsb)?;
89 let block_size = nxsb.block_size;
90
91 let container_omap_root =
93 omap::read_omap_tree_root(&mut reader, nxsb.omap_oid, block_size)?;
94
95 let vol_oid = nxsb
97 .fs_oids
98 .iter()
99 .find(|&&o| o != 0)
100 .copied()
101 .ok_or(ApfsError::NoVolume)?;
102
103 let vol_block = omap::omap_lookup(&mut reader, container_omap_root, block_size, vol_oid)?;
105
106 let vol_data = object::read_block(&mut reader, vol_block, block_size)?;
108 let vol_sb = superblock::ApfsSuperblock::parse(&vol_data)?;
109
110 let vol_omap_root_block =
112 omap::read_omap_tree_root(&mut reader, vol_sb.omap_oid, block_size)?;
113
114 let catalog_root_block = omap::omap_lookup(
116 &mut reader,
117 vol_omap_root_block,
118 block_size,
119 vol_sb.root_tree_oid,
120 )?;
121
122 let info = VolumeInfo {
124 name: vol_sb.volume_name.clone(),
125 block_size,
126 num_files: vol_sb.num_files,
127 num_directories: vol_sb.num_directories,
128 num_symlinks: vol_sb.num_symlinks,
129 };
130
131 Ok(ApfsVolume {
132 reader,
133 block_size,
134 vol_omap_root_block,
135 catalog_root_block,
136 info,
137 })
138 }
139
140 pub fn volume_info(&self) -> &VolumeInfo {
142 &self.info
143 }
144
145 pub fn list_directory(&mut self, path: &str) -> Result<Vec<DirEntry>> {
147 let (oid, _inode) = if path == "/" || path.is_empty() {
148 (catalog::ROOT_DIR_PARENT, catalog::ROOT_DIR_RECORD)
150 } else {
151 let (oid, inode) = catalog::resolve_path(
152 &mut self.reader,
153 self.catalog_root_block,
154 self.vol_omap_root_block,
155 self.block_size,
156 path,
157 )?;
158 if inode.kind() != catalog::INODE_DIR_TYPE {
159 return Err(ApfsError::NotADirectory(path.to_string()));
160 }
161 (oid, oid)
162 };
163
164 let parent = if path == "/" || path.is_empty() {
165 catalog::ROOT_DIR_RECORD
166 } else {
167 oid
168 };
169
170 catalog::list_directory(
171 &mut self.reader,
172 self.catalog_root_block,
173 self.vol_omap_root_block,
174 self.block_size,
175 parent,
176 )
177 }
178
179 pub fn read_file(&mut self, path: &str) -> Result<Vec<u8>> {
181 let mut buf = Vec::new();
182 self.read_file_to(path, &mut buf)?;
183 Ok(buf)
184 }
185
186 pub fn read_file_to<W: Write>(&mut self, path: &str, writer: &mut W) -> Result<u64> {
188 let (_oid, inode) = catalog::resolve_path(
189 &mut self.reader,
190 self.catalog_root_block,
191 self.vol_omap_root_block,
192 self.block_size,
193 path,
194 )?;
195
196 let file_extents = catalog::lookup_extents(
198 &mut self.reader,
199 self.catalog_root_block,
200 self.vol_omap_root_block,
201 self.block_size,
202 inode.private_id,
203 )?;
204
205 extents::read_file_data(
206 &mut self.reader,
207 self.block_size,
208 &file_extents,
209 inode.size(),
210 writer,
211 )
212 }
213
214 pub fn open_file(&mut self, path: &str) -> Result<extents::ApfsForkReader<'_, R>> {
216 let (_oid, inode) = catalog::resolve_path(
217 &mut self.reader,
218 self.catalog_root_block,
219 self.vol_omap_root_block,
220 self.block_size,
221 path,
222 )?;
223
224 let file_extents = catalog::lookup_extents(
226 &mut self.reader,
227 self.catalog_root_block,
228 self.vol_omap_root_block,
229 self.block_size,
230 inode.private_id,
231 )?;
232
233 Ok(extents::ApfsForkReader::new(
234 &mut self.reader,
235 self.block_size,
236 file_extents,
237 inode.size(),
238 ))
239 }
240
241 pub fn stat(&mut self, path: &str) -> Result<FileStat> {
243 let (oid, inode) = catalog::resolve_path(
244 &mut self.reader,
245 self.catalog_root_block,
246 self.vol_omap_root_block,
247 self.block_size,
248 path,
249 )?;
250
251 Ok(FileStat {
252 oid,
253 kind: match inode.kind() {
254 catalog::INODE_DIR_TYPE => EntryKind::Directory,
255 catalog::INODE_SYMLINK_TYPE => EntryKind::Symlink,
256 _ => EntryKind::File,
257 },
258 size: inode.size(),
259 create_time: inode.create_time,
260 modify_time: inode.modify_time,
261 uid: inode.uid,
262 gid: inode.gid,
263 mode: inode.mode,
264 nlink: inode.nlink(),
265 })
266 }
267
268 pub fn walk(&mut self) -> Result<Vec<WalkEntry>> {
270 let mut entries = Vec::new();
271 self.walk_recursive(catalog::ROOT_DIR_RECORD, "", &mut entries)?;
272 Ok(entries)
273 }
274
275 pub fn exists(&mut self, path: &str) -> Result<bool> {
277 match catalog::resolve_path(
278 &mut self.reader,
279 self.catalog_root_block,
280 self.vol_omap_root_block,
281 self.block_size,
282 path,
283 ) {
284 Ok(_) => Ok(true),
285 Err(ApfsError::FileNotFound(_)) => Ok(false),
286 Err(e) => Err(e),
287 }
288 }
289
290 fn walk_recursive(
291 &mut self,
292 parent_oid: u64,
293 parent_path: &str,
294 entries: &mut Vec<WalkEntry>,
295 ) -> Result<()> {
296 let dir_entries = catalog::list_directory(
297 &mut self.reader,
298 self.catalog_root_block,
299 self.vol_omap_root_block,
300 self.block_size,
301 parent_oid,
302 )?;
303
304 for entry in dir_entries {
305 let full_path = if parent_path.is_empty() {
306 format!("/{}", entry.name)
307 } else {
308 format!("{}/{}", parent_path, entry.name)
309 };
310
311 let is_dir = entry.kind == EntryKind::Directory;
312 let oid = entry.oid;
313
314 entries.push(WalkEntry {
315 path: full_path.clone(),
316 entry,
317 });
318
319 if is_dir {
320 self.walk_recursive(oid, &full_path, entries)?;
321 }
322 }
323
324 Ok(())
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use std::io::BufReader;
332
333 #[test]
335 #[ignore]
336 fn test_volume_open() {
337 let file = std::fs::File::open("../tests/appfs.raw").unwrap();
338 let reader = BufReader::new(file);
339
340 let mut vol = ApfsVolume::open(reader).unwrap();
341 let info = vol.volume_info();
342
343 assert!(!info.name.is_empty(), "Volume name should not be empty");
344 assert_eq!(info.block_size, 4096);
345
346 let entries = vol.list_directory("/").unwrap();
347 assert!(!entries.is_empty(), "Root directory should have entries");
348
349 let walk_entries = vol.walk().unwrap();
350 assert!(!walk_entries.is_empty());
351 }
352
353 #[test]
355 #[ignore]
356 fn test_read_file_data() {
357 let file = std::fs::File::open("../tests/appfs.raw").unwrap();
358 let reader = BufReader::new(file);
359
360 let mut vol = ApfsVolume::open(reader).unwrap();
361
362 let walk = vol.walk().unwrap();
363 let small_file = walk.iter().find(|e| {
364 e.entry.kind == EntryKind::File && e.entry.size > 0 && e.entry.size < 1_000_000
365 });
366
367 let entry = small_file.expect("Should find a small file in the test image");
368 let data = vol.read_file(&entry.path).unwrap();
369 assert_eq!(
370 data.len() as u64,
371 entry.entry.size,
372 "Read size should match stat size"
373 );
374
375 let stat = vol.stat(&entry.path).unwrap();
376 assert_eq!(stat.size, entry.entry.size);
377 }
378}