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