1use std::io::{Read, Seek};
27
28use crate::btree::{self, BTreeSubtype};
29use crate::fsrecord::{decode_jkey, RecordType};
30use crate::object::{fletcher64_checksum, fletcher64_stored, ObjPhys};
31use crate::omap::ObjectMap;
32use crate::volume::ApfsVolume;
33
34const OBJECT_SUBTYPE_SNAPMETATREE: u32 = 0x10;
36
37const OFF_SNAP_NAME_KEY_LEN: usize = 8;
39const OFF_SNAP_NAME_KEY_NAME: usize = 10;
40
41const OFF_SNAP_EXTENTREF_TREE_OID: usize = 0;
43const OFF_SNAP_SBLOCK_OID: usize = 8;
44const OFF_SNAP_CREATE_TIME: usize = 16;
45const OFF_SNAP_CHANGE_TIME: usize = 24;
46const OFF_SNAP_INUM: usize = 32;
47const OFF_SNAP_FLAGS: usize = 44;
48const OFF_SNAP_NAME_LEN: usize = 48;
49const OFF_SNAP_NAME: usize = 50;
50
51const MAX_SNAP_TREE_DEPTH: usize = 64;
53
54const MAX_SNAP_NAME_LEN: usize = 4096;
57
58#[derive(Debug, Clone)]
60#[non_exhaustive]
61pub struct Snapshot {
62 pub xid: u64,
64 pub name: String,
66 pub create_time: u64,
68 pub change_time: u64,
70 pub sblock_oid: u64,
73 pub extentref_tree_oid: u64,
75 pub inum: u64,
77 pub flags: u32,
79}
80
81fn parse_snap_metadata(xid: u64, value: &[u8]) -> Snapshot {
85 let name_len = (crate::bytes::le_u16(value, OFF_SNAP_NAME_LEN) as usize).min(MAX_SNAP_NAME_LEN);
86 let name = value
87 .get(OFF_SNAP_NAME..OFF_SNAP_NAME + name_len)
88 .map_or_else(String::new, decode_cstr);
89 Snapshot {
90 xid,
91 name,
92 create_time: crate::bytes::le_u64(value, OFF_SNAP_CREATE_TIME),
93 change_time: crate::bytes::le_u64(value, OFF_SNAP_CHANGE_TIME),
94 sblock_oid: crate::bytes::le_u64(value, OFF_SNAP_SBLOCK_OID),
95 extentref_tree_oid: crate::bytes::le_u64(value, OFF_SNAP_EXTENTREF_TREE_OID),
96 inum: crate::bytes::le_u64(value, OFF_SNAP_INUM),
97 flags: crate::bytes::le_u32(value, OFF_SNAP_FLAGS),
98 }
99}
100
101pub fn list_snapshots<R: Read + Seek>(
108 reader: &mut R,
109 volume: &ApfsVolume,
110 block_size: usize,
111) -> crate::Result<Vec<Snapshot>> {
112 let mut out = Vec::new();
113 for_each_snap_record(reader, volume, block_size, &mut |key, value| {
114 let (xid, ty) = decode_jkey(crate::bytes::le_u64(key, 0));
115 if ty == Some(RecordType::SnapMetadata) {
116 out.push(parse_snap_metadata(xid, value));
117 }
118 })?;
119 out.sort_by_key(|s| s.xid);
120 Ok(out)
121}
122
123pub fn resolve_snapshot_xid<R: Read + Seek>(
130 reader: &mut R,
131 volume: &ApfsVolume,
132 name: &str,
133 block_size: usize,
134) -> crate::Result<Option<u64>> {
135 let mut found = None;
136 for_each_snap_record(reader, volume, block_size, &mut |key, value| {
137 if found.is_some() {
138 return;
139 }
140 let (_oid, ty) = decode_jkey(crate::bytes::le_u64(key, 0));
141 if ty != Some(RecordType::SnapName) {
142 return;
143 }
144 if decode_snap_name_key(key).as_deref() == Some(name) {
145 found = Some(crate::bytes::le_u64(value, 0));
147 }
148 })?;
149 Ok(found)
150}
151
152pub fn mount_snapshot<R: Read + Seek>(
171 reader: &mut R,
172 live_volume: &ApfsVolume,
173 snapshot: &Snapshot,
174 block_size: usize,
175) -> crate::Result<ApfsVolume> {
176 let mut buf = vec![0u8; block_size];
177 let offset = snapshot.sblock_oid.saturating_mul(block_size as u64);
178 reader.seek(std::io::SeekFrom::Start(offset))?;
179 reader.read_exact(&mut buf)?;
180 Ok(ApfsVolume::parse(&buf)?.with_omap_oid(live_volume.omap_oid()))
181}
182
183fn for_each_snap_record<R, F>(
192 reader: &mut R,
193 volume: &ApfsVolume,
194 block_size: usize,
195 visit: &mut F,
196) -> crate::Result<()>
197where
198 R: Read + Seek,
199 F: FnMut(&[u8], &[u8]),
200{
201 let mut buf = vec![0u8; block_size];
204 let omap_off = volume.omap_oid().saturating_mul(block_size as u64);
205 reader.seek(std::io::SeekFrom::Start(omap_off))?;
206 reader.read_exact(&mut buf)?;
207 let omap = ObjectMap::parse(&buf)?;
208
209 let mut visited = std::collections::HashSet::new();
210 descend_snap(
211 reader,
212 &omap,
213 volume.snap_meta_tree_oid(),
214 true,
215 volume.xid(),
216 block_size,
217 0,
218 &mut visited,
219 visit,
220 )
221}
222
223#[allow(clippy::too_many_arguments)]
224fn descend_snap<R, F>(
225 reader: &mut R,
226 omap: &ObjectMap,
227 node_oid: u64,
228 is_root: bool,
229 xid: u64,
230 block_size: usize,
231 depth: usize,
232 visited: &mut std::collections::HashSet<u64>,
233 visit: &mut F,
234) -> crate::Result<()>
235where
236 R: Read + Seek,
237 F: FnMut(&[u8], &[u8]),
238{
239 let cycle = || crate::ApfsError::CycleGuard {
240 cap: MAX_SNAP_TREE_DEPTH,
241 };
242 if depth >= MAX_SNAP_TREE_DEPTH {
245 return Err(cycle()); }
247 if !visited.insert(node_oid) {
248 return Err(cycle());
249 }
250
251 let paddr = if is_root {
254 node_oid
255 } else {
256 omap.resolve(reader, node_oid, xid, block_size)?.paddr
257 };
258
259 let mut buf = vec![0u8; block_size];
260 let offset = paddr.saturating_mul(block_size as u64);
261 reader.seek(std::io::SeekFrom::Start(offset))?;
262 reader.read_exact(&mut buf)?;
263
264 let stored = fletcher64_stored(&buf);
266 let computed = fletcher64_checksum(&buf);
267 if stored != computed {
268 let block = ObjPhys::parse(&buf).map_or(paddr, |h| h.oid);
269 return Err(crate::ApfsError::ChecksumMismatch {
270 block,
271 stored,
272 computed,
273 });
274 }
275
276 let Some(hdr) = btree::parse_node_header(&buf) else {
277 return Ok(()); };
279
280 if hdr.is_leaf() {
284 for e in btree::node_entries(&buf, BTreeSubtype::FsTree) {
285 visit(e.key, e.value);
286 }
287 return Ok(());
288 }
289
290 let children: Vec<u64> = btree::node_entries(&buf, BTreeSubtype::FsTree)
292 .iter()
293 .map(|e| crate::bytes::le_u64(e.value, 0))
294 .collect();
295 for child in children {
296 descend_snap(
297 reader,
298 omap,
299 child,
300 false,
301 xid,
302 block_size,
303 depth + 1,
304 visited,
305 visit,
306 )?;
307 }
308 Ok(())
309}
310
311fn decode_snap_name_key(key: &[u8]) -> Option<String> {
315 let name_len =
316 (crate::bytes::le_u16(key, OFF_SNAP_NAME_KEY_LEN) as usize).min(MAX_SNAP_NAME_LEN);
317 if name_len == 0 {
318 return None;
319 }
320 key.get(OFF_SNAP_NAME_KEY_NAME..OFF_SNAP_NAME_KEY_NAME + name_len)
321 .map(decode_cstr)
322}
323
324fn decode_cstr(data: &[u8]) -> String {
326 let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
327 String::from_utf8_lossy(&data[..end]).into_owned()
328}
329
330#[must_use]
332pub fn snap_meta_tree_subtype() -> u32 {
333 OBJECT_SUBTYPE_SNAPMETATREE
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn parse_snap_metadata_value_decodes_all_fields() {
342 let mut v = Vec::new();
343 v.extend_from_slice(&0x11u64.to_le_bytes()); v.extend_from_slice(&0x22u64.to_le_bytes()); v.extend_from_slice(&1000u64.to_le_bytes()); v.extend_from_slice(&2000u64.to_le_bytes()); v.extend_from_slice(&99u64.to_le_bytes()); v.extend_from_slice(&0x4000_0002u32.to_le_bytes()); v.extend_from_slice(&0x1u32.to_le_bytes()); v.extend_from_slice(&6u16.to_le_bytes()); v.extend_from_slice(b"snap1\0"); let s = parse_snap_metadata(42, &v);
354 assert_eq!(s.xid, 42);
355 assert_eq!(s.extentref_tree_oid, 0x11);
356 assert_eq!(s.sblock_oid, 0x22);
357 assert_eq!(s.create_time, 1000);
358 assert_eq!(s.change_time, 2000);
359 assert_eq!(s.inum, 99);
360 assert_eq!(s.flags, 0x1);
361 assert_eq!(s.name, "snap1");
362 }
363
364 #[test]
365 fn parse_snap_metadata_clamps_overlong_name() {
366 let mut v = vec![0u8; OFF_SNAP_NAME];
367 v[OFF_SNAP_NAME_LEN..OFF_SNAP_NAME_LEN + 2].copy_from_slice(&50000u16.to_le_bytes());
368 let s = parse_snap_metadata(1, &v);
369 assert_eq!(s.name, "");
370 }
371
372 #[test]
373 fn parse_snap_metadata_truncated_value_reads_zero() {
374 let s = parse_snap_metadata(7, &[0u8; 4]);
375 assert_eq!(s.sblock_oid, 0);
376 assert_eq!(s.create_time, 0);
377 assert_eq!(s.name, "");
378 }
379
380 #[test]
381 fn snap_meta_tree_subtype_is_snapmetatree() {
382 assert_eq!(snap_meta_tree_subtype(), 0x10);
383 }
384
385 fn jkey(ty: u64, oid: u64) -> [u8; 8] {
387 ((ty << 60) | oid).to_le_bytes()
388 }
389
390 #[test]
391 fn decode_snap_name_key_reads_name() {
392 let mut key = Vec::new();
394 key.extend_from_slice(&jkey(11, 0)); key.extend_from_slice(&6u16.to_le_bytes()); key.extend_from_slice(b"snap1\0");
397 assert_eq!(decode_snap_name_key(&key).as_deref(), Some("snap1"));
398 }
399
400 #[test]
401 fn decode_snap_name_key_rejects_zero_and_overlong() {
402 let mut zero = Vec::new();
404 zero.extend_from_slice(&jkey(11, 0));
405 zero.extend_from_slice(&0u16.to_le_bytes());
406 assert_eq!(decode_snap_name_key(&zero), None);
407
408 let mut overlong = Vec::new();
409 overlong.extend_from_slice(&jkey(11, 0));
410 overlong.extend_from_slice(&200u16.to_le_bytes());
411 overlong.extend_from_slice(b"z");
412 assert_eq!(decode_snap_name_key(&overlong), None);
413 }
414
415 const P4_CONTENT: &[u8] = include_bytes!("../../tests/data/apfs_content.bin");
422 const P4_BLOCK_SIZE: usize = 4096;
423 const P4_APSB_BLOCK: u64 = 438;
424
425 fn snapshot_pointing_at(sblock_oid: u64) -> Snapshot {
426 Snapshot {
427 xid: 0,
428 name: "synthetic-pointer".to_string(),
429 create_time: 0,
430 change_time: 0,
431 sblock_oid,
432 extentref_tree_oid: 0,
433 inum: 0,
434 flags: 0,
435 }
436 }
437
438 #[test]
439 fn mount_snapshot_reads_sblock_as_volume() {
440 use std::io::Cursor;
441 let mut r = Cursor::new(P4_CONTENT);
442 let start = P4_APSB_BLOCK as usize * P4_BLOCK_SIZE;
445 let live =
446 ApfsVolume::parse(&P4_CONTENT[start..start + P4_BLOCK_SIZE]).expect("parse live APSB");
447 let snap = snapshot_pointing_at(P4_APSB_BLOCK);
448 let mounted = mount_snapshot(&mut r, &live, &snap, P4_BLOCK_SIZE).expect("mount snapshot");
449
450 assert_eq!(mounted.oid(), live.oid());
453 assert_eq!(mounted.xid(), live.xid());
454 assert_eq!(mounted.omap_oid(), live.omap_oid());
455 assert_eq!(mounted.root_tree_oid(), live.root_tree_oid());
456 assert_eq!(mounted.name(), live.name());
457 assert_eq!(mounted.name(), "APFSP4");
458 }
459
460 #[test]
461 fn mount_snapshot_grafts_live_omap_over_snapshot_zero() {
462 use std::io::Cursor;
463 let mut r = Cursor::new(P4_CONTENT);
468 let start = P4_APSB_BLOCK as usize * P4_BLOCK_SIZE;
469 let block438 =
470 ApfsVolume::parse(&P4_CONTENT[start..start + P4_BLOCK_SIZE]).expect("parse APSB");
471 let live = block438.clone().with_omap_oid(999_111);
472 let snap = snapshot_pointing_at(P4_APSB_BLOCK);
473 let mounted = mount_snapshot(&mut r, &live, &snap, P4_BLOCK_SIZE).expect("mount snapshot");
474 assert_eq!(
475 mounted.omap_oid(),
476 999_111,
477 "the live volume's omap must be grafted onto the snapshot view"
478 );
479 assert_eq!(mounted.xid(), block438.xid());
481 assert_eq!(mounted.root_tree_oid(), block438.root_tree_oid());
482 }
483
484 #[test]
485 fn mount_snapshot_rejects_non_apsb_block() {
486 use std::io::Cursor;
487 let mut r = Cursor::new(P4_CONTENT);
488 let start = P4_APSB_BLOCK as usize * P4_BLOCK_SIZE;
489 let live =
490 ApfsVolume::parse(&P4_CONTENT[start..start + P4_BLOCK_SIZE]).expect("parse live APSB");
491 let snap = snapshot_pointing_at(0);
494 let err = mount_snapshot(&mut r, &live, &snap, P4_BLOCK_SIZE).unwrap_err();
495 assert!(
496 matches!(err, crate::ApfsError::UnexpectedObjectType { .. }),
497 "mounting a non-APSB block must fail loudly, got {err:?}"
498 );
499 }
500
501 const BS: usize = 4096;
516
517 fn seal(block: &mut [u8]) {
519 let c = fletcher64_checksum(block);
520 block[0..8].copy_from_slice(&c.to_le_bytes());
521 }
522
523 fn obj_hdr(block: &mut [u8], oid: u64, xid: u64, o_type: u32, o_subtype: u32) {
525 block[8..16].copy_from_slice(&oid.to_le_bytes());
526 block[16..24].copy_from_slice(&xid.to_le_bytes());
527 block[24..28].copy_from_slice(&o_type.to_le_bytes());
528 block[28..32].copy_from_slice(&o_subtype.to_le_bytes());
529 }
530
531 fn btree_node(oid: u64, xid: u64, is_leaf: bool, records: &[(Vec<u8>, Vec<u8>)]) -> Vec<u8> {
537 const FOOTER: usize = 40;
538 let mut b = vec![0u8; BS];
539 let flags: u16 = 0x1 | if is_leaf { 0x2 } else { 0 };
541 b[32..34].copy_from_slice(&flags.to_le_bytes());
542 let level: u16 = u16::from(!is_leaf);
543 b[34..36].copy_from_slice(&level.to_le_bytes());
544 b[36..40].copy_from_slice(&(records.len() as u32).to_le_bytes());
545
546 let toc_len = records.len() * 8;
547 b[40..42].copy_from_slice(&0u16.to_le_bytes());
549 b[42..44].copy_from_slice(&(toc_len as u16).to_le_bytes());
550
551 let toc_start = 56; let key_area = toc_start + toc_len;
553 let val_base = BS - FOOTER; let mut key_off = 0usize; let mut val_off = 0usize; for (i, (k, v)) in records.iter().enumerate() {
558 let e = toc_start + i * 8;
559 b[e..e + 2].copy_from_slice(&(key_off as u16).to_le_bytes());
560 b[e + 2..e + 4].copy_from_slice(&(k.len() as u16).to_le_bytes());
561 let this_val = v.len();
563 let v_reversed = val_off + this_val;
564 b[e + 4..e + 6].copy_from_slice(&(v_reversed as u16).to_le_bytes());
565 b[e + 6..e + 8].copy_from_slice(&(this_val as u16).to_le_bytes());
566
567 let ks = key_area + key_off;
568 b[ks..ks + k.len()].copy_from_slice(k);
569 let vs = val_base - v_reversed;
570 b[vs..vs + this_val].copy_from_slice(v);
571
572 key_off += k.len();
573 val_off += this_val;
574 }
575 obj_hdr(&mut b, oid, xid, 0x4000_0002, 0x10); seal(&mut b);
577 b
578 }
579
580 fn snap_jkey(ty: u64, id: u64) -> Vec<u8> {
582 ((ty << 60) | id).to_le_bytes().to_vec()
583 }
584
585 fn snap_meta_val(sblock: u64, create: u64, name: &str) -> Vec<u8> {
587 let mut v = vec![0u8; OFF_SNAP_NAME];
588 v[OFF_SNAP_SBLOCK_OID..OFF_SNAP_SBLOCK_OID + 8].copy_from_slice(&sblock.to_le_bytes());
589 v[OFF_SNAP_CREATE_TIME..OFF_SNAP_CREATE_TIME + 8].copy_from_slice(&create.to_le_bytes());
590 let mut name_b = name.as_bytes().to_vec();
591 name_b.push(0);
592 v[OFF_SNAP_NAME_LEN..OFF_SNAP_NAME_LEN + 2]
593 .copy_from_slice(&(name_b.len() as u16).to_le_bytes());
594 v.extend_from_slice(&name_b);
595 v
596 }
597
598 fn snap_name_key(name: &str) -> Vec<u8> {
600 let mut k = snap_jkey(11, 0);
601 let mut name_b = name.as_bytes().to_vec();
602 name_b.push(0);
603 k.extend_from_slice(&(name_b.len() as u16).to_le_bytes());
604 k.extend_from_slice(&name_b);
605 k
606 }
607
608 fn apsb(oid: u64, xid: u64, omap_oid: u64, snap_meta_oid: u64) -> Vec<u8> {
611 let mut b = vec![0u8; BS];
612 b[32..36].copy_from_slice(&0x4253_5041u32.to_le_bytes()); b[128..136].copy_from_slice(&omap_oid.to_le_bytes()); b[152..160].copy_from_slice(&snap_meta_oid.to_le_bytes()); obj_hdr(&mut b, oid, xid, 0x0d, 0); seal(&mut b);
617 b
618 }
619
620 fn omap_block(oid: u64, tree_block: u64) -> Vec<u8> {
623 let mut b = vec![0u8; BS];
624 b[40..44].copy_from_slice(&0x4000_0002u32.to_le_bytes()); b[48..56].copy_from_slice(&tree_block.to_le_bytes()); obj_hdr(&mut b, oid, 0, 0x0b, 0); seal(&mut b);
628 b
629 }
630
631 fn omap_leaf(oid: u64, entries: &[(u64, u64, u64)]) -> Vec<u8> {
633 const FOOTER: usize = 40;
634 let mut b = vec![0u8; BS];
635 let flags: u16 = 0x1 | 0x2 | 0x4; b[32..34].copy_from_slice(&flags.to_le_bytes());
637 b[36..40].copy_from_slice(&(entries.len() as u32).to_le_bytes());
638 let toc_len = entries.len() * 4; b[40..42].copy_from_slice(&0u16.to_le_bytes());
640 b[42..44].copy_from_slice(&(toc_len as u16).to_le_bytes());
641 let toc_start = 56;
642 let key_area = toc_start + toc_len;
643 let val_base = BS - FOOTER;
644 let mut key_off = 0usize;
645 let mut val_off = 0usize;
646 for (i, (virt, xid, phys)) in entries.iter().enumerate() {
647 let e = toc_start + i * 4;
648 b[e..e + 2].copy_from_slice(&(key_off as u16).to_le_bytes());
649 let mut k = vec![0u8; 16];
651 k[0..8].copy_from_slice(&virt.to_le_bytes());
652 k[8..16].copy_from_slice(&xid.to_le_bytes());
653 let ks = key_area + key_off;
654 b[ks..ks + 16].copy_from_slice(&k);
655 let mut v = vec![0u8; 16];
657 v[8..16].copy_from_slice(&phys.to_le_bytes());
658 let v_reversed = val_off + 16;
659 b[e + 2..e + 4].copy_from_slice(&(v_reversed as u16).to_le_bytes());
660 let vs = val_base - v_reversed;
661 b[vs..vs + 16].copy_from_slice(&v);
662 key_off += 16;
663 val_off += 16;
664 }
665 obj_hdr(&mut b, oid, 0, 0x4000_0002, 0x0b); seal(&mut b);
667 b
668 }
669
670 fn image(blocks: &[(u64, Vec<u8>)]) -> Vec<u8> {
672 let max = blocks.iter().map(|(i, _)| *i).max().unwrap_or(0) as usize;
673 let mut buf = vec![0u8; (max + 1) * BS];
674 for (i, b) in blocks {
675 let off = *i as usize * BS;
676 buf[off..off + BS].copy_from_slice(b);
677 }
678 buf
679 }
680
681 fn single_leaf_volume(snap_records: &[(Vec<u8>, Vec<u8>)]) -> (Vec<u8>, ApfsVolume) {
684 let snap_leaf = btree_node(50, 7, true, snap_records);
685 let omap = omap_block(40, 41);
686 let omap_tree = omap_leaf(41, &[]); let apsb_b = apsb(1026, 7, 40, 50);
688 let buf = image(&[(40, omap), (41, omap_tree), (50, snap_leaf), (1026, apsb_b)]);
689 let vol = ApfsVolume::parse(&buf[1026 * BS..1027 * BS]).expect("parse synth APSB");
690 (buf, vol)
691 }
692
693 #[test]
694 fn lists_snapshots_from_populated_leaf() {
695 use std::io::Cursor;
696 let records = vec![
700 (snap_jkey(1, 5), snap_meta_val(0x200, 1000, "snapA")),
701 (snap_jkey(1, 9), snap_meta_val(0x300, 2000, "snapB")),
702 (snap_name_key("snapA"), 5u64.to_le_bytes().to_vec()),
703 ];
704 let (buf, vol) = single_leaf_volume(&records);
705 let mut r = Cursor::new(buf);
706 let snaps = list_snapshots(&mut r, &vol, BS).expect("list");
707 assert_eq!(snaps.len(), 2);
708 assert_eq!(snaps[0].xid, 5);
709 assert_eq!(snaps[0].name, "snapA");
710 assert_eq!(snaps[0].sblock_oid, 0x200);
711 assert_eq!(snaps[0].create_time, 1000);
712 assert_eq!(snaps[1].xid, 9);
713 assert_eq!(snaps[1].name, "snapB");
714 }
715
716 #[test]
717 fn resolves_snap_name_from_populated_leaf() {
718 use std::io::Cursor;
719 let records = vec![
720 (snap_jkey(1, 5), snap_meta_val(0x200, 1000, "snapA")),
721 (snap_name_key("snapA"), 5u64.to_le_bytes().to_vec()),
722 (snap_name_key("snapB"), 9u64.to_le_bytes().to_vec()),
723 ];
724 let (buf, vol) = single_leaf_volume(&records);
725 let mut r = Cursor::new(buf);
726 assert_eq!(
727 resolve_snapshot_xid(&mut r, &vol, "snapB", BS).expect("resolve"),
728 Some(9)
729 );
730 assert_eq!(
731 resolve_snapshot_xid(&mut r, &vol, "snapA", BS).expect("resolve"),
732 Some(5)
733 );
734 assert_eq!(
735 resolve_snapshot_xid(&mut r, &vol, "absent", BS).expect("resolve"),
736 None
737 );
738 }
739
740 #[test]
741 fn walks_index_node_resolving_child_virtually() {
742 use std::io::Cursor;
743 let leaf = btree_node(
747 60,
748 7,
749 true,
750 &[(snap_jkey(1, 3), snap_meta_val(0x99, 500, "s"))],
751 );
752 let index = btree_node(
754 50,
755 7,
756 false,
757 &[(snap_jkey(1, 3), 1500u64.to_le_bytes().to_vec())],
758 );
759 let omap = omap_block(40, 41);
760 let omap_tree = omap_leaf(41, &[(1500, 7, 60)]);
762 let apsb_b = apsb(1026, 7, 40, 50);
763 let buf = image(&[
764 (40, omap),
765 (41, omap_tree),
766 (50, index),
767 (60, leaf),
768 (1026, apsb_b),
769 ]);
770 let vol = ApfsVolume::parse(&buf[1026 * BS..1027 * BS]).expect("parse APSB");
771 let mut r = Cursor::new(buf);
772 let snaps = list_snapshots(&mut r, &vol, BS).expect("list via index");
773 assert_eq!(snaps.len(), 1);
774 assert_eq!(snaps[0].xid, 3);
775 assert_eq!(snaps[0].sblock_oid, 0x99);
776 }
777
778 #[test]
779 fn snap_tree_checksum_mismatch_is_loud() {
780 use std::io::Cursor;
781 let records = vec![(snap_jkey(1, 5), snap_meta_val(0x200, 1000, "snapA"))];
782 let (mut buf, vol) = single_leaf_volume(&records);
783 buf[50 * BS + 100] ^= 0xff;
785 let mut r = Cursor::new(buf);
786 let err = list_snapshots(&mut r, &vol, BS).unwrap_err();
787 assert!(
788 matches!(err, crate::ApfsError::ChecksumMismatch { .. }),
789 "a corrupted snap-meta node must fail loudly, got {err:?}"
790 );
791 }
792
793 #[test]
794 fn snap_tree_cycle_is_rejected() {
795 use std::io::Cursor;
796 let index = btree_node(
799 50,
800 7,
801 false,
802 &[(snap_jkey(1, 3), 1500u64.to_le_bytes().to_vec())],
803 );
804 let omap = omap_block(40, 41);
805 let omap_tree = omap_leaf(41, &[(1500, 7, 50)]);
807 let apsb_b = apsb(1026, 7, 40, 50);
808 let buf = image(&[(40, omap), (41, omap_tree), (50, index), (1026, apsb_b)]);
809 let vol = ApfsVolume::parse(&buf[1026 * BS..1027 * BS]).expect("parse APSB");
810 let mut r = Cursor::new(buf);
811 let err = list_snapshots(&mut r, &vol, BS).unwrap_err();
812 assert!(
813 matches!(err, crate::ApfsError::CycleGuard { .. }),
814 "a cyclic snap-meta tree must be rejected, got {err:?}"
815 );
816 }
817}