1pub mod findings;
18
19mod analyse;
20pub use analyse::{analyse, analyse_reader};
21pub use findings::{Anomaly, AnomalyKind, ApmAnalysis, Severity};
22
23#[derive(Debug)]
25pub enum Error {
26 NotApm,
29 TooShort { need: usize, got: usize },
31 Io(std::io::Error),
33}
34
35impl core::fmt::Display for Error {
36 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
37 match self {
38 Error::NotApm => f.write_str("not an Apple Partition Map (missing ER/PM signature)"),
39 Error::TooShort { need, got } => {
40 write!(f, "buffer too short: need {need} bytes, got {got}")
41 }
42 Error::Io(e) => write!(f, "I/O error: {e}"),
43 }
44 }
45}
46
47impl std::error::Error for Error {
48 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
49 match self {
50 Error::Io(e) => Some(e),
51 _ => None,
52 }
53 }
54}
55
56impl From<std::io::Error> for Error {
57 fn from(e: std::io::Error) -> Self {
58 Error::Io(e)
59 }
60}
61
62const SIG_DDM: &[u8; 2] = b"ER";
64const SIG_PM: &[u8; 2] = b"PM";
66const MAX_PARTITIONS: u32 = 256;
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71#[cfg_attr(feature = "serde", derive(serde::Serialize))]
72pub struct ApmPartition {
73 pub name: String,
75 pub type_name: String,
77 pub start_block: u32,
79 pub block_count: u32,
81 pub map_count: u32,
84 pub status: u32,
86}
87
88impl ApmPartition {
89 #[must_use]
91 pub fn end_block(&self) -> u32 {
92 self.start_block
93 .saturating_add(self.block_count)
94 .saturating_sub(1)
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize))]
101pub struct ApplePartitionMap {
102 pub block_size: u32,
104 pub device_block_count: u32,
106 pub partitions: Vec<ApmPartition>,
108}
109
110impl ApplePartitionMap {
111 #[must_use]
113 pub fn hfs_partition(&self) -> Option<&ApmPartition> {
114 self.partitions
115 .iter()
116 .find(|p| p.type_name.starts_with("Apple_HFS"))
117 }
118}
119
120#[must_use]
124pub fn parse(data: &[u8]) -> Option<ApplePartitionMap> {
125 if data.len() < 512 || &data[0..2] != SIG_DDM {
126 return None;
127 }
128 let block_size = u32::from(be16(&data[2..4]));
129 let device_block_count = be32(&data[4..8]);
130 let bs = block_size as usize;
131 if bs == 0 {
132 return None;
133 }
134 let first = bs;
136 if data.len() < first + 8 || &data[first..first + 2] != SIG_PM {
137 return None;
138 }
139 let map_count = be32(&data[first + 4..first + 8]).min(MAX_PARTITIONS);
140
141 let mut partitions = Vec::new();
142 for i in 0..map_count {
143 let off = bs * (1 + i as usize);
144 if data.len() < off + 92 || &data[off..off + 2] != SIG_PM {
145 break;
146 }
147 partitions.push(ApmPartition {
148 map_count: be32(&data[off + 4..off + 8]),
149 start_block: be32(&data[off + 8..off + 12]),
150 block_count: be32(&data[off + 12..off + 16]),
151 name: cstr(&data[off + 16..off + 48]),
152 type_name: cstr(&data[off + 48..off + 80]),
153 status: be32(&data[off + 88..off + 92]),
154 });
155 }
156 Some(ApplePartitionMap {
157 block_size,
158 device_block_count,
159 partitions,
160 })
161}
162
163fn cstr(bytes: &[u8]) -> String {
165 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
166 bytes[..end].iter().map(|&b| b as char).collect()
167}
168
169fn be16(b: &[u8]) -> u16 {
170 u16::from_be_bytes([b[0], b[1]])
171}
172fn be32(b: &[u8]) -> u32 {
173 u32::from_be_bytes([b[0], b[1], b[2], b[3]])
174}