Skip to main content

macho_core/model/
container.rs

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/// One validated Mach-O slice in a fat container.
10#[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    /// Construct and validate one fat architecture entry.
22    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    /// Architecture tuple declared by the fat table.
72    pub fn spec(&self) -> ArchSpec {
73        self.spec
74    }
75
76    /// Fat-container-relative start offset.
77    pub fn fat_offset(&self) -> FatFileOffset {
78        self.fat_offset
79    }
80
81    /// Slice length in bytes.
82    pub fn size(&self) -> u64 {
83        self.size
84    }
85
86    /// Base-two alignment exponent.
87    pub fn align(&self) -> u32 {
88        self.align
89    }
90
91    /// Reserved fat64 table value.
92    pub fn reserved(&self) -> u32 {
93        self.reserved
94    }
95
96    /// Parsed Mach-O image for this slice.
97    pub fn macho(&self) -> &MachoFile<'data> {
98        &self.macho
99    }
100
101    /// Translate a thin-image-relative offset to a fat-container-relative offset.
102    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    /// Translate a fat-container-relative offset to a thin-image-relative offset.
117    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
137/// Validated non-empty fat Mach-O container.
138pub struct FatBinary<'data> {
139    header: FatHeader,
140    arches: Vec<FatArch<'data>>,
141    bytes: &'data [u8],
142}
143
144impl<'data> FatBinary<'data> {
145    /// Validate a complete fat table and its parsed slices.
146    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    /// Fat header.
202    pub fn header(&self) -> &FatHeader {
203        &self.header
204    }
205
206    /// Validated architecture slices in table order.
207    pub fn arches(&self) -> &[FatArch<'data>] {
208        &self.arches
209    }
210
211    /// Complete bytes of the enclosing universal Mach-O input.
212    pub fn bytes(&self) -> &'data [u8] {
213        self.bytes
214    }
215
216    /// Find an architecture by CPU type.
217    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    /// Find an architecture by CPU type and masked subtype.
224    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
235/// Parsed thin or fat Mach-O input.
236pub enum MachoContainer<'data> {
237    /// One standalone Mach-O image.
238    Thin(MachoFile<'data>),
239    /// A validated non-empty fat container.
240    Fat(FatBinary<'data>),
241}
242
243/// Stable identity for exactly one image in a thin or universal container.
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
245pub struct SelectionKey {
246    /// Zero-based member index. Thin images always use index zero.
247    pub container_index: usize,
248    /// Exact CPU type and masked subtype expected at that index.
249    pub architecture: ArchSpec,
250}
251
252/// One image selected from a validated container together with its byte range.
253#[derive(Debug, Clone)]
254pub struct SelectedImage<'container, 'data> {
255    /// Exact identity used for selection.
256    pub key: SelectionKey,
257    /// Container-relative byte range occupied by the selected image.
258    pub container_range: Range<u64>,
259    /// Parsed image borrowed from the validated container.
260    pub image: &'container MachoFile<'data>,
261}
262
263impl<'data> MachoContainer<'data> {
264    /// Complete bytes of the original thin or universal Mach-O input.
265    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    /// Whether the container is thin.
273    pub fn is_thin(&self) -> bool {
274        matches!(self, Self::Thin(_))
275    }
276
277    /// Whether the container is fat.
278    pub fn is_fat(&self) -> bool {
279        matches!(self, Self::Fat(_))
280    }
281
282    /// Iterate parsed images without allocating a temporary collection.
283    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    /// Return the first image when one exists.
295    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    /// Find an image by CPU type.
303    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    /// Find an image by CPU type and masked subtype.
312    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    /// Select exactly one image by table index and architecture.
332    ///
333    /// This is the canonical selection boundary for consumers that retain an
334    /// image identity across parse, inspection, and mutation. Both fields are
335    /// checked so a stale index cannot silently retarget after container drift.
336    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
380/// Zero-allocation iterator over images in a [`MachoContainer`].
381pub 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}