1use std::collections::BTreeSet;
2use std::ops::Range;
3
4use crate::error::{Error, Result};
5use crate::model::addr::{FatFileOffset, ThinFileOffset};
6use crate::model::header::{ArchSpec, CpuSubtype, CpuType, FatHeader};
7use crate::model::macho_file::MachoFile;
8
9#[derive(Debug)]
11pub struct FatArch<'data> {
12 spec: ArchSpec,
13 fat_offset: FatFileOffset,
14 size: u64,
15 align: u32,
16 reserved: u32,
17 macho: MachoFile<'data>,
18}
19
20impl<'data> FatArch<'data> {
21 pub(crate) fn try_new(
23 spec: ArchSpec,
24 fat_offset: FatFileOffset,
25 size: u64,
26 align: u32,
27 reserved: u32,
28 macho: MachoFile<'data>,
29 container_len: usize,
30 ) -> Result<Self> {
31 if size == 0 {
32 return Err(Error::format("fat architecture slice has zero size"));
33 }
34 if align >= u64::BITS {
35 return Err(Error::format(format!(
36 "fat architecture alignment exponent {align} is invalid"
37 )));
38 }
39 let alignment = 1u64
40 .checked_shl(align)
41 .ok_or_else(|| Error::format(format!("fat alignment 2^{align} overflows")))?;
42 if fat_offset.0 % alignment != 0 {
43 return Err(Error::format(format!(
44 "fat slice offset {:#x} is not aligned to 2^{align}",
45 fat_offset.0
46 )));
47 }
48 let end = fat_offset
49 .0
50 .checked_add(size)
51 .ok_or_else(|| Error::format("fat slice offset plus size overflows"))?;
52 if end > container_len as u64 {
53 return Err(Error::bounds(fat_offset.0, size, container_len as u64));
54 }
55 if macho.file_size() as u64 != size {
56 return Err(Error::format(format!(
57 "fat entry size {size} differs from parsed slice size {}",
58 macho.file_size()
59 )));
60 }
61 Ok(Self {
62 spec,
63 fat_offset,
64 size,
65 align,
66 reserved,
67 macho,
68 })
69 }
70
71 pub fn spec(&self) -> ArchSpec {
73 self.spec
74 }
75
76 pub fn fat_offset(&self) -> FatFileOffset {
78 self.fat_offset
79 }
80
81 pub fn size(&self) -> u64 {
83 self.size
84 }
85
86 pub fn align(&self) -> u32 {
88 self.align
89 }
90
91 pub fn reserved(&self) -> u32 {
93 self.reserved
94 }
95
96 pub fn macho(&self) -> &MachoFile<'data> {
98 &self.macho
99 }
100
101 pub fn thin_to_fat_offset(&self, thin: ThinFileOffset) -> Result<FatFileOffset> {
103 if thin.0 >= self.size {
104 return Err(Error::address(format!(
105 "thin offset {thin} is outside slice size {:#x}",
106 self.size
107 )));
108 }
109 self.fat_offset
110 .0
111 .checked_add(thin.0)
112 .map(FatFileOffset)
113 .ok_or_else(|| Error::address("fat offset translation overflows"))
114 }
115
116 pub fn fat_to_thin_offset(&self, fat: FatFileOffset) -> Result<ThinFileOffset> {
118 let rel = fat
119 .0
120 .checked_sub(self.fat_offset.0)
121 .ok_or_else(|| Error::address(format!("fat offset {fat} precedes this slice")))?;
122 if rel >= self.size {
123 let end = self
124 .fat_offset
125 .0
126 .checked_add(self.size)
127 .ok_or_else(|| Error::address("fat slice end overflows"))?;
128 return Err(Error::address(format!(
129 "fat offset {fat} is outside arch slice at {:#x}..{end:#x}",
130 self.fat_offset.0,
131 )));
132 }
133 Ok(ThinFileOffset(rel))
134 }
135}
136
137pub struct FatBinary<'data> {
139 header: FatHeader,
140 arches: Vec<FatArch<'data>>,
141 bytes: &'data [u8],
142}
143
144impl<'data> FatBinary<'data> {
145 pub(crate) fn try_new(
147 header: FatHeader,
148 arches: Vec<FatArch<'data>>,
149 bytes: &'data [u8],
150 ) -> Result<Self> {
151 let container_len = bytes.len();
152 if arches.is_empty() {
153 return Err(Error::format("fat binary has zero architectures"));
154 }
155 if arches.len() != header.architecture_count() as usize {
156 return Err(Error::format(format!(
157 "fat header declares {} architectures but {} were parsed",
158 header.architecture_count(),
159 arches.len()
160 )));
161 }
162 let mut identities = BTreeSet::new();
163 let mut ranges = Vec::with_capacity(arches.len());
164 for arch in &arches {
165 if !identities.insert((arch.spec.cpu_type.0, arch.spec.cpu_subtype.0)) {
166 return Err(Error::format(format!(
167 "duplicate fat architecture {}",
168 arch.spec.name()
169 )));
170 }
171 let end = arch
172 .fat_offset
173 .0
174 .checked_add(arch.size)
175 .ok_or_else(|| Error::format("fat slice end overflows"))?;
176 if end > container_len as u64 {
177 return Err(Error::bounds(
178 arch.fat_offset.0,
179 arch.size,
180 container_len as u64,
181 ));
182 }
183 ranges.push((arch.fat_offset.0, end));
184 }
185 ranges.sort_unstable();
186 for pair in ranges.windows(2) {
187 if pair[1].0 < pair[0].1 {
188 return Err(Error::format(format!(
189 "fat slices overlap at {:#x}..{:#x} and {:#x}..{:#x}",
190 pair[0].0, pair[0].1, pair[1].0, pair[1].1
191 )));
192 }
193 }
194 Ok(Self {
195 header,
196 arches,
197 bytes,
198 })
199 }
200
201 pub fn header(&self) -> &FatHeader {
203 &self.header
204 }
205
206 pub fn arches(&self) -> &[FatArch<'data>] {
208 &self.arches
209 }
210
211 pub fn bytes(&self) -> &'data [u8] {
213 self.bytes
214 }
215
216 pub fn find_arch(&self, cpu_type: CpuType) -> Option<&FatArch<'data>> {
218 self.arches
219 .iter()
220 .find(|arch| arch.spec.cpu_type == cpu_type)
221 }
222
223 pub fn find_arch_spec(
225 &self,
226 cpu_type: CpuType,
227 cpu_subtype: CpuSubtype,
228 ) -> Option<&FatArch<'data>> {
229 self.arches.iter().find(|arch| {
230 arch.spec.cpu_type == cpu_type && arch.spec.cpu_subtype.masked() == cpu_subtype.masked()
231 })
232 }
233}
234
235pub enum MachoContainer<'data> {
237 Thin(MachoFile<'data>),
239 Fat(FatBinary<'data>),
241}
242
243#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub struct SelectionKey {
246 pub container_index: usize,
248 pub architecture: ArchSpec,
250}
251
252#[derive(Debug, Clone)]
254pub struct SelectedImage<'container, 'data> {
255 pub key: SelectionKey,
257 pub container_range: Range<u64>,
259 pub image: &'container MachoFile<'data>,
261}
262
263impl<'data> MachoContainer<'data> {
264 pub fn bytes(&self) -> &'data [u8] {
266 match self {
267 Self::Thin(macho) => macho.bytes(),
268 Self::Fat(fat) => fat.bytes(),
269 }
270 }
271
272 pub fn is_thin(&self) -> bool {
274 matches!(self, Self::Thin(_))
275 }
276
277 pub fn is_fat(&self) -> bool {
279 matches!(self, Self::Fat(_))
280 }
281
282 pub fn macho_files(&self) -> MachoFiles<'_, 'data> {
284 match self {
285 Self::Thin(macho) => MachoFiles {
286 inner: MachoFilesInner::Thin(Some(macho)),
287 },
288 Self::Fat(fat) => MachoFiles {
289 inner: MachoFilesInner::Fat(fat.arches.iter()),
290 },
291 }
292 }
293
294 pub fn first_macho(&self) -> Option<&MachoFile<'data>> {
296 match self {
297 Self::Thin(macho) => Some(macho),
298 Self::Fat(fat) => fat.arches.first().map(FatArch::macho),
299 }
300 }
301
302 pub fn find_arch(&self, cpu_type: CpuType) -> Option<&MachoFile<'data>> {
304 match self {
305 Self::Thin(macho) if macho.header().cpu_type == cpu_type => Some(macho),
306 Self::Thin(_) => None,
307 Self::Fat(fat) => fat.find_arch(cpu_type).map(FatArch::macho),
308 }
309 }
310
311 pub fn find_arch_spec(
313 &self,
314 cpu_type: CpuType,
315 cpu_subtype: CpuSubtype,
316 ) -> Option<&MachoFile<'data>> {
317 match self {
318 Self::Thin(macho)
319 if macho.header().cpu_type == cpu_type
320 && macho.header().cpu_subtype.masked() == cpu_subtype.masked() =>
321 {
322 Some(macho)
323 }
324 Self::Thin(_) => None,
325 Self::Fat(fat) => fat
326 .find_arch_spec(cpu_type, cpu_subtype)
327 .map(FatArch::macho),
328 }
329 }
330
331 pub fn select_exact(&self, key: SelectionKey) -> Result<SelectedImage<'_, 'data>> {
337 let (image, start, size) = match self {
338 Self::Thin(image) if key.container_index == 0 => (image, 0, image.file_size() as u64),
339 Self::Thin(_) => {
340 return Err(Error::address(format!(
341 "thin Mach-O has no member at index {}",
342 key.container_index
343 )));
344 }
345 Self::Fat(fat) => {
346 let arch = fat.arches.get(key.container_index).ok_or_else(|| {
347 Error::address(format!(
348 "fat Mach-O has no member at index {}",
349 key.container_index
350 ))
351 })?;
352 (arch.macho(), arch.fat_offset.0, arch.size)
353 }
354 };
355 let actual = ArchSpec {
356 cpu_type: image.header().cpu_type(),
357 cpu_subtype: image.header().cpu_subtype(),
358 };
359 if actual.cpu_type != key.architecture.cpu_type
360 || actual.cpu_subtype.masked() != key.architecture.cpu_subtype.masked()
361 {
362 return Err(Error::validation(format!(
363 "Mach-O member {} is {}, not {}",
364 key.container_index,
365 actual.name(),
366 key.architecture.name()
367 )));
368 }
369 let end = start
370 .checked_add(size)
371 .ok_or_else(|| Error::address("selected Mach-O range overflows"))?;
372 Ok(SelectedImage {
373 key,
374 container_range: start..end,
375 image,
376 })
377 }
378}
379
380pub struct MachoFiles<'container, 'data> {
382 inner: MachoFilesInner<'container, 'data>,
383}
384
385enum MachoFilesInner<'container, 'data> {
386 Thin(Option<&'container MachoFile<'data>>),
387 Fat(std::slice::Iter<'container, FatArch<'data>>),
388}
389
390impl<'container, 'data> Iterator for MachoFiles<'container, 'data> {
391 type Item = &'container MachoFile<'data>;
392
393 fn next(&mut self) -> Option<Self::Item> {
394 match &mut self.inner {
395 MachoFilesInner::Thin(macho) => macho.take(),
396 MachoFilesInner::Fat(arches) => arches.next().map(FatArch::macho),
397 }
398 }
399
400 fn size_hint(&self) -> (usize, Option<usize>) {
401 let len = match &self.inner {
402 MachoFilesInner::Thin(macho) => usize::from(macho.is_some()),
403 MachoFilesInner::Fat(arches) => arches.len(),
404 };
405 (len, Some(len))
406 }
407}
408
409impl ExactSizeIterator for MachoFiles<'_, '_> {}
410
411impl std::fmt::Debug for FatBinary<'_> {
412 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
413 formatter
414 .debug_struct("FatBinary")
415 .field("header", &self.header)
416 .field(
417 "arches",
418 &self
419 .arches
420 .iter()
421 .map(|arch| arch.spec.name())
422 .collect::<Vec<_>>(),
423 )
424 .finish()
425 }
426}
427
428impl std::fmt::Debug for MachoContainer<'_> {
429 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430 match self {
431 Self::Thin(macho) => formatter.debug_tuple("Thin").field(macho).finish(),
432 Self::Fat(fat) => formatter.debug_tuple("Fat").field(fat).finish(),
433 }
434 }
435}
436
437#[cfg(test)]
438mod selection_tests {
439 use super::*;
440 use crate::format::parse;
441
442 fn thin64() -> Vec<u8> {
443 let mut bytes = vec![0_u8; 32];
444 bytes[0..4].copy_from_slice(&0xfeed_facfu32.to_le_bytes());
445 bytes[4..8].copy_from_slice(&0x0100_0007i32.to_le_bytes());
446 bytes[8..12].copy_from_slice(&3_i32.to_le_bytes());
447 bytes[12..16].copy_from_slice(&2_u32.to_le_bytes());
448 bytes
449 }
450
451 #[test]
452 fn exact_selection_rejects_stale_identity() {
453 let bytes = thin64();
454 let container = parse(&bytes).expect("thin fixture");
455 let key = SelectionKey {
456 container_index: 0,
457 architecture: ArchSpec {
458 cpu_type: CpuType(0x0100_0007),
459 cpu_subtype: CpuSubtype(3),
460 },
461 };
462 assert_eq!(
463 container
464 .select_exact(key)
465 .expect("selection")
466 .container_range,
467 0..32
468 );
469 assert!(
470 container
471 .select_exact(SelectionKey {
472 architecture: ArchSpec {
473 cpu_type: CpuType(0x0100_000c),
474 cpu_subtype: CpuSubtype(0),
475 },
476 ..key
477 })
478 .is_err()
479 );
480 }
481}