apm/lib.rs
1//! Apple Partition Map (APM) reader.
2//!
3//! Apple hybrid optical discs carry an Apple Partition Map so a Mac sees the
4//! disc's partitions (typically an `Apple_HFS` slice alongside the ISO 9660
5//! filesystem). The layout (Inside Macintosh: Devices) is big-endian, in
6//! fixed-size device blocks: block 0 is the Driver Descriptor Map (signature
7//! `ER`, carrying the block size), and blocks 1.. are partition entries
8//! (signature `PM`), the first of which reports how many entries the map holds.
9//!
10//! This crate reads the map for *detection and partition geometry* (name,
11//! type, start block, block count). Validated against a real `hdiutil` APM.
12//!
13//! For forensic anomaly detection (overlaps, out-of-bounds, map-count
14//! inconsistency, residual/hidden entries) see the `apm-partition-forensic` crate.
15
16// Production code is panic-free (no unwrap/expect, enforced by the workspace
17// lints); tests legitimately use them.
18#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
19
20/// Crate-level error type. (Manual impl — no `thiserror` dependency.)
21#[derive(Debug)]
22pub enum Error {
23 /// The buffer did not begin with the Driver Descriptor Map `ER` signature,
24 /// or the first partition entry lacked the `PM` signature.
25 NotApm,
26 /// The buffer was shorter than the structure it was asked to hold.
27 TooShort { need: usize, got: usize },
28 /// I/O failure while reading the disk image.
29 Io(std::io::Error),
30}
31
32impl core::fmt::Display for Error {
33 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
34 match self {
35 Error::NotApm => f.write_str("not an Apple Partition Map (missing ER/PM signature)"),
36 Error::TooShort { need, got } => {
37 write!(f, "buffer too short: need {need} bytes, got {got}")
38 }
39 Error::Io(e) => write!(f, "I/O error: {e}"),
40 }
41 }
42}
43
44impl std::error::Error for Error {
45 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
46 match self {
47 Error::Io(e) => Some(e),
48 _ => None,
49 }
50 }
51}
52
53impl From<std::io::Error> for Error {
54 fn from(e: std::io::Error) -> Self {
55 Error::Io(e)
56 }
57}
58
59/// Driver Descriptor Map signature (`ER`).
60const SIG_DDM: &[u8; 2] = b"ER";
61/// Partition map entry signature (`PM`).
62const SIG_PM: &[u8; 2] = b"PM";
63/// Cap on partition entries, guarding against a corrupt map.
64const MAX_PARTITIONS: u32 = 256;
65
66/// One Apple Partition Map entry.
67#[derive(Debug, Clone, PartialEq, Eq)]
68#[cfg_attr(feature = "serde", derive(serde::Serialize))]
69pub struct ApmPartition {
70 /// Partition name (`pmPartName`), e.g. `"disk image"`.
71 pub name: String,
72 /// Partition type (`pmPartType`), e.g. `"Apple_HFS"`.
73 pub type_name: String,
74 /// Physical start block of the partition (`pmPyPartStart`).
75 pub start_block: u32,
76 /// Partition length in blocks (`pmPartBlkCnt`).
77 pub block_count: u32,
78 /// Number of blocks in the partition map, as recorded by *this* entry
79 /// (`pmMapBlkCnt`). Every entry should report the same value.
80 pub map_count: u32,
81 /// Partition status bits (`pmPartStatus`).
82 pub status: u32,
83}
84
85impl ApmPartition {
86 /// Inclusive last block of this partition, saturating on overflow.
87 #[must_use]
88 pub fn end_block(&self) -> u32 {
89 self.start_block
90 .saturating_add(self.block_count)
91 .saturating_sub(1)
92 }
93}
94
95/// A parsed Apple Partition Map.
96#[derive(Debug, Clone, PartialEq, Eq)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize))]
98pub struct ApplePartitionMap {
99 /// Device block size in bytes (from the Driver Descriptor Map).
100 pub block_size: u32,
101 /// Number of blocks on the device (`sbBlkCount` in the Driver Descriptor Map).
102 pub device_block_count: u32,
103 /// Partition entries in map order.
104 pub partitions: Vec<ApmPartition>,
105}
106
107impl ApplePartitionMap {
108 /// The first `Apple_HFS` (or HFS+) partition, if any.
109 #[must_use]
110 pub fn hfs_partition(&self) -> Option<&ApmPartition> {
111 self.partitions
112 .iter()
113 .find(|p| p.type_name.starts_with("Apple_HFS"))
114 }
115}
116
117/// Parse an Apple Partition Map from a buffer beginning at the device start
118/// (block 0 = Driver Descriptor Map). Returns `None` without the `ER`/`PM`
119/// signatures or if the buffer is too short.
120#[must_use]
121pub fn parse(data: &[u8]) -> Option<ApplePartitionMap> {
122 if data.len() < 512 || data.get(0..2)? != SIG_DDM {
123 return None;
124 }
125 let block_size = u32::from(be16(data.get(2..4)?));
126 let device_block_count = be32(data.get(4..8)?);
127 let bs = block_size as usize;
128 if bs == 0 {
129 return None;
130 }
131 // First partition entry sits at block 1 and reports the map's entry count.
132 let first = bs;
133 if data.get(first..first + 2)? != SIG_PM {
134 return None;
135 }
136 let map_count = be32(data.get(first + 4..first + 8)?).min(MAX_PARTITIONS);
137
138 let mut partitions = Vec::new();
139 for i in 0..map_count {
140 let off = bs * (1 + i as usize);
141 let entry = match data.get(off..off + 92) {
142 Some(e) if &e[0..2] == SIG_PM => e,
143 _ => break,
144 };
145 partitions.push(ApmPartition {
146 map_count: be32(&entry[4..8]),
147 start_block: be32(&entry[8..12]),
148 block_count: be32(&entry[12..16]),
149 name: cstr(&entry[16..48]),
150 type_name: cstr(&entry[48..80]),
151 status: be32(&entry[88..92]),
152 });
153 }
154 Some(ApplePartitionMap {
155 block_size,
156 device_block_count,
157 partitions,
158 })
159}
160
161/// Decode a fixed-width NUL-terminated ASCII field.
162fn cstr(bytes: &[u8]) -> String {
163 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
164 bytes[..end].iter().map(|&b| b as char).collect()
165}
166
167/// Read a big-endian `u16` from the first two bytes of `b`; missing bytes read
168/// as zero, so this never panics on a short slice.
169fn be16(b: &[u8]) -> u16 {
170 u16::from_be_bytes([
171 b.first().copied().unwrap_or(0),
172 b.get(1).copied().unwrap_or(0),
173 ])
174}
175
176/// Read a big-endian `u32` from the first four bytes of `b`; missing bytes read
177/// as zero, so this never panics on a short slice.
178fn be32(b: &[u8]) -> u32 {
179 u32::from_be_bytes([
180 b.first().copied().unwrap_or(0),
181 b.get(1).copied().unwrap_or(0),
182 b.get(2).copied().unwrap_or(0),
183 b.get(3).copied().unwrap_or(0),
184 ])
185}