Skip to main content

ud_format/
macho.rs

1//! Mach-O reader and writer with byte-identical round-trip.
2//!
3//! v1 covers thin (non-fat) 64-bit little-endian Mach-O images
4//! for both x86-64 and arm64. The parsed representation captures
5//! the structured header and each load command's `(cmd, cmdsize)`
6//! prefix; load command bodies stay as opaque bytes so the suite
7//! of cmd kinds (`LC_SEGMENT_64`, `LC_SYMTAB`, `LC_CODE_SIGNATURE`,
8//! `LC_DYLD_CHAINED_FIXUPS`, …) round-trip without needing per-cmd
9//! decoders.
10//!
11//! Contract: for any supported input `bytes`,
12//! `MachoFile::parse(bytes)?.write_to_vec() == bytes`.
13//!
14//! Fat (universal) wrappers and 32-bit Mach-O are out of scope
15//! for v1. Section contents (the bytes inside `__text`, `__data`,
16//! etc.) are never interpreted here — that belongs to the arch
17//! backends and analysis crates.
18
19#![allow(clippy::cast_possible_truncation)]
20
21use std::ops::Range;
22
23/// 64-bit little-endian Mach-O magic.
24pub const MH_MAGIC_64: u32 = 0xfeed_facf;
25
26/// 32-bit little-endian Mach-O magic (detected; v1 parse refuses).
27pub const MH_MAGIC: u32 = 0xfeed_face;
28
29/// Fat-arch wrapper magic (big-endian). Detected so callers can
30/// route appropriately; v1 parse refuses.
31pub const FAT_MAGIC: u32 = 0xcafe_babe;
32pub const FAT_MAGIC_64: u32 = 0xcafe_babf;
33
34/// `cputype` values for the architectures v1 supports.
35pub const CPU_TYPE_X86_64: u32 = 0x0100_0007;
36pub const CPU_TYPE_ARM64: u32 = 0x0100_000c;
37
38/// `LC_SEGMENT_64` load-command kind. Carries a segment descriptor
39/// plus that segment's sections.
40pub const LC_SEGMENT_64: u32 = 0x19;
41
42/// Bit OR'd into `cmd` to mark a command as required for correct
43/// dynamic linker behaviour. Not a kind by itself; ORed with the
44/// concrete `LC_*` value.
45pub const LC_REQ_DYLD: u32 = 0x8000_0000;
46
47/// `LC_SYMTAB`: classical symbol-table command. Body is four u32s:
48/// `symoff`, `nsyms`, `stroff`, `strsize`.
49pub const LC_SYMTAB: u32 = 0x2;
50
51/// `LC_DYSYMTAB`: extended dynamic-link symbol info. Body is 18
52/// u32s describing partitions of the LC_SYMTAB table plus auxiliary
53/// indirect-symbol / module / reference tables.
54pub const LC_DYSYMTAB: u32 = 0xb;
55
56/// `LC_LOAD_DYLINKER`: file path of the dynamic linker. Body is a
57/// `lc_str` offset (u32) followed by the NUL-padded name.
58pub const LC_LOAD_DYLINKER: u32 = 0xe;
59
60/// `LC_UUID`: 16-byte randomly-generated identifier baked into the
61/// binary at link time.
62pub const LC_UUID: u32 = 0x1b;
63
64/// `LC_LOAD_DYLIB`: dependent dynamic library. Body is a
65/// `dylib_command` after the cmd/cmdsize prefix.
66pub const LC_LOAD_DYLIB: u32 = 0xc;
67
68/// `LC_LOAD_WEAK_DYLIB`: dependent library, weak link.
69pub const LC_LOAD_WEAK_DYLIB: u32 = 0x18 | LC_REQ_DYLD;
70
71/// `LC_REEXPORT_DYLIB`: dependent library to re-export.
72pub const LC_REEXPORT_DYLIB: u32 = 0x1f | LC_REQ_DYLD;
73
74/// `LC_ID_DYLIB`: identifies this image when it is itself a dylib.
75pub const LC_ID_DYLIB: u32 = 0xd;
76
77/// `LC_BUILD_VERSION`: platform / minimum-OS / SDK / toolchain.
78pub const LC_BUILD_VERSION: u32 = 0x32;
79
80/// `LC_SOURCE_VERSION`: 64-bit packed source-control version.
81pub const LC_SOURCE_VERSION: u32 = 0x2a;
82
83/// `LC_MAIN`: entry-point load command. Body is `entryoff` (u64)
84/// + `stacksize` (u64).
85pub const LC_MAIN: u32 = 0x28 | LC_REQ_DYLD;
86
87/// `linkedit_data_command` kinds. All share the same 4 + 4 + 4 + 4
88/// body shape: cmd + cmdsize prefix already eaten, then body =
89/// `dataoff` (u32) + `datasize` (u32).
90pub const LC_CODE_SIGNATURE: u32 = 0x1d;
91pub const LC_FUNCTION_STARTS: u32 = 0x26;
92pub const LC_DATA_IN_CODE: u32 = 0x29;
93pub const LC_DYLIB_CODE_SIGN_DRS: u32 = 0x2b;
94pub const LC_LINKER_OPTIMIZATION_HINT: u32 = 0x2e;
95pub const LC_DYLD_EXPORTS_TRIE: u32 = 0x33 | LC_REQ_DYLD;
96pub const LC_DYLD_CHAINED_FIXUPS: u32 = 0x34 | LC_REQ_DYLD;
97
98/// On-disk size of `mach_header_64`.
99const MACH_HEADER_64_SIZE: u64 = 32;
100
101/// On-disk size of a `LC_SEGMENT_64` command's fixed prefix
102/// (the part before any embedded `section_64` entries):
103/// 8 bytes (cmd + cmdsize) + 16 (segname) + 8 (vmaddr) + 8 (vmsize)
104/// + 8 (fileoff) + 8 (filesize) + 4 (maxprot) + 4 (initprot)
105/// + 4 (nsects) + 4 (flags) = 72.
106const SEGMENT_64_PREFIX_SIZE: usize = 72;
107
108/// Architecture flavour the parsed Mach-O targets.
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum MachoCpu {
111    X86_64,
112    Arm64,
113}
114
115/// Errors surfaced when parsing or writing a Mach-O file.
116#[derive(Debug, thiserror::Error)]
117pub enum Error {
118    #[error("file too short: needed {needed} bytes at offset {offset}, have {have}")]
119    Truncated { offset: u64, needed: u64, have: u64 },
120
121    #[error("not a Mach-O file: bad magic {0:#x}")]
122    BadMagic(u32),
123
124    #[error(
125        "fat (universal) Mach-O wrappers are not supported in v1; demux into thin slices first"
126    )]
127    FatNotSupported,
128
129    #[error("32-bit Mach-O is not supported in v1 (magic {0:#x}); thin 64-bit only")]
130    Macho32NotSupported(u32),
131
132    #[error("unsupported cputype {0:#x}: v1 covers x86-64 (0x01000007) and arm64 (0x0100000c)")]
133    UnsupportedCpu(u32),
134
135    #[error(
136        "load command at offset {offset}: declared cmdsize {cmdsize} is too small (minimum 8)"
137    )]
138    BadLoadCmdSize { offset: u64, cmdsize: u32 },
139
140    #[error("load-command table runs past sizeofcmds: cursor {cursor}, end {end}")]
141    LoadCmdOverrun { cursor: u64, end: u64 },
142
143    #[error(
144        "structured regions overlap: {a_label} at {a_start}..{a_end} vs {b_label} at {b_start}..{b_end}"
145    )]
146    OverlappingRegions {
147        a_label: String,
148        a_start: u64,
149        a_end: u64,
150        b_label: String,
151        b_start: u64,
152        b_end: u64,
153    },
154
155    #[error("integer overflow computing region end for {label} at offset {offset} size {size}")]
156    RegionOverflow {
157        label: String,
158        offset: u64,
159        size: u64,
160    },
161}
162
163pub type Result<T, E = Error> = std::result::Result<T, E>;
164
165/// Parsed 64-bit Mach-O header.
166///
167/// Field names mirror Apple's `mach_header_64` verbatim. The
168/// struct is public so analysis crates can read its fields;
169/// invariants (`magic == MH_MAGIC_64`, `cputype` recognised)
170/// are enforced only at parse time.
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct MachHeader64 {
173    pub magic: u32,
174    pub cputype: u32,
175    pub cpusubtype: u32,
176    pub filetype: u32,
177    pub ncmds: u32,
178    pub sizeofcmds: u32,
179    pub flags: u32,
180    pub reserved: u32,
181}
182
183impl MachHeader64 {
184    fn parse(bytes: &[u8]) -> Result<Self> {
185        ensure_len(bytes, 0, MACH_HEADER_64_SIZE)?;
186        Ok(Self {
187            magic: read_u32(bytes, 0),
188            cputype: read_u32(bytes, 4),
189            cpusubtype: read_u32(bytes, 8),
190            filetype: read_u32(bytes, 12),
191            ncmds: read_u32(bytes, 16),
192            sizeofcmds: read_u32(bytes, 20),
193            flags: read_u32(bytes, 24),
194            reserved: read_u32(bytes, 28),
195        })
196    }
197
198    fn write(&self, out: &mut [u8]) {
199        write_u32(out, 0, self.magic);
200        write_u32(out, 4, self.cputype);
201        write_u32(out, 8, self.cpusubtype);
202        write_u32(out, 12, self.filetype);
203        write_u32(out, 16, self.ncmds);
204        write_u32(out, 20, self.sizeofcmds);
205        write_u32(out, 24, self.flags);
206        write_u32(out, 28, self.reserved);
207    }
208}
209
210/// One load command from the table that follows the file header.
211///
212/// `body` excludes the 8-byte `(cmd, cmdsize)` prefix — the prefix
213/// is rebuilt on write from the struct fields, and the body bytes
214/// round-trip verbatim. v1 keeps every command kind opaque; richer
215/// per-cmd decoding (e.g. structured `LC_SEGMENT_64`, structured
216/// `LC_SYMTAB`) is left for later passes.
217#[derive(Debug, Clone, PartialEq, Eq)]
218pub struct LoadCommand {
219    pub cmd: u32,
220    pub cmdsize: u32,
221    pub body: Vec<u8>,
222}
223
224/// A `LC_SEGMENT_64` descriptor, structurally decoded enough to
225/// drive segment-data extraction and the decompile path's
226/// section iteration. The raw bytes still round-trip through
227/// the matching [`LoadCommand::body`].
228#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct Segment64 {
230    /// Index of this segment's `LC_SEGMENT_64` entry in
231    /// [`MachoFile::commands`].
232    pub cmd_index: usize,
233    /// Null-padded segment name (`__TEXT`, `__DATA_CONST`,
234    /// `__LINKEDIT`, …). Up to 16 bytes.
235    pub segname: [u8; 16],
236    pub vmaddr: u64,
237    pub vmsize: u64,
238    pub fileoff: u64,
239    pub filesize: u64,
240    pub maxprot: u32,
241    pub initprot: u32,
242    pub nsects: u32,
243    pub flags: u32,
244    pub sections: Vec<Section64>,
245}
246
247/// A `section_64` entry within an `LC_SEGMENT_64`. Field naming
248/// matches Apple's struct verbatim.
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub struct Section64 {
251    /// Null-padded section name (`__text`, `__cstring`, …).
252    pub sectname: [u8; 16],
253    /// Null-padded enclosing segment name (`__TEXT`, …).
254    pub segname: [u8; 16],
255    pub addr: u64,
256    pub size: u64,
257    pub offset: u32,
258    pub align: u32,
259    pub reloff: u32,
260    pub nreloc: u32,
261    pub flags: u32,
262    pub reserved1: u32,
263    pub reserved2: u32,
264    pub reserved3: u32,
265}
266
267impl Segment64 {
268    /// UTF-8 (lossy) version of the null-padded segment name —
269    /// the trailing NULs are trimmed. Used by callers that want
270    /// `__TEXT` / `__DATA` style readable identifiers.
271    #[must_use]
272    pub fn name(&self) -> String {
273        cstr_name(&self.segname)
274    }
275}
276
277impl Section64 {
278    /// UTF-8 (lossy) section name with trailing NULs trimmed.
279    #[must_use]
280    pub fn name(&self) -> String {
281        cstr_name(&self.sectname)
282    }
283
284    /// UTF-8 (lossy) segment name with trailing NULs trimmed.
285    #[must_use]
286    pub fn segment_name(&self) -> String {
287        cstr_name(&self.segname)
288    }
289}
290
291fn cstr_name(buf: &[u8]) -> String {
292    let nul = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
293    String::from_utf8_lossy(&buf[..nul]).into_owned()
294}
295
296/// A parsed thin 64-bit Mach-O file in a form that round-trips
297/// byte-identically.
298///
299/// The structured fields (`header`, `commands`) are interpreted;
300/// every load command's `body` carries the raw bytes for that
301/// command, and each segment's file data is captured in
302/// `segment_data` parallel to the `LC_SEGMENT_64` entries in
303/// `commands`. Gaps between structured regions land in `padding`
304/// — same `(file_offset, bytes)` convention `Elf64File` uses.
305#[derive(Debug, Clone)]
306pub struct MachoFile {
307    pub header: MachHeader64,
308    pub commands: Vec<LoadCommand>,
309    /// Segment file content, one entry per `LC_SEGMENT_64` load
310    /// command in declaration order. `__PAGEZERO` (filesize 0)
311    /// contributes an empty vec — that's fine, it just doesn't
312    /// occupy file space.
313    segment_data: Vec<Vec<u8>>,
314    /// Index into `commands` for each entry in `segment_data`
315    /// (so callers can pair them back up).
316    segment_cmd_indices: Vec<usize>,
317    /// Bytes in gaps between structured regions. Stored as
318    /// `(file_offset, bytes)`.
319    padding: Vec<(u64, Vec<u8>)>,
320    file_size: u64,
321}
322
323/// True when `bytes` start with any Mach-O magic — thin 32- or
324/// 64-bit, either endian, plus the fat (universal) wrapper.
325/// Doesn't say whether v1 will *accept* the file (use
326/// [`is_macho64`] for that gate).
327#[must_use]
328pub fn is_macho(bytes: &[u8]) -> bool {
329    is_macho64(bytes) || is_fat(bytes) || is_macho32(bytes) || is_macho_be(bytes)
330}
331
332/// True when `bytes` are a thin 32-bit little-endian Mach-O.
333/// Detected so callers can route around it; v1 parse refuses.
334#[must_use]
335pub fn is_macho32(bytes: &[u8]) -> bool {
336    bytes.len() >= 4 && read_u32(bytes, 0) == MH_MAGIC
337}
338
339/// True when `bytes` are a big-endian thin Mach-O (typically
340/// legacy PowerPC binaries). Detected so callers can route
341/// around it.
342#[must_use]
343fn is_macho_be(bytes: &[u8]) -> bool {
344    if bytes.len() < 4 {
345        return false;
346    }
347    let be = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
348    be == MH_MAGIC_64 || be == MH_MAGIC
349}
350
351/// True when `bytes` are a thin 64-bit little-endian Mach-O —
352/// the flavour [`MachoFile::parse`] handles. Callers that route
353/// by format should gate on this and fall through to a byte-copy
354/// for unsupported variants so the round-trip contract still
355/// holds.
356#[must_use]
357pub fn is_macho64(bytes: &[u8]) -> bool {
358    bytes.len() >= 4 && read_u32(bytes, 0) == MH_MAGIC_64
359}
360
361/// True when `bytes` are a fat (universal) Mach-O wrapper. Not
362/// supported by v1 parse, but exposed so callers can route around
363/// it.
364#[must_use]
365pub fn is_fat(bytes: &[u8]) -> bool {
366    if bytes.len() < 4 {
367        return false;
368    }
369    // Fat magic is stored big-endian on disk.
370    let magic_be = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
371    magic_be == FAT_MAGIC || magic_be == FAT_MAGIC_64
372}
373
374impl MachoFile {
375    /// Parse a thin 64-bit little-endian Mach-O file.
376    pub fn parse(bytes: &[u8]) -> Result<Self> {
377        if bytes.len() < 4 {
378            return Err(Error::Truncated {
379                offset: 0,
380                needed: 4,
381                have: bytes.len() as u64,
382            });
383        }
384        let magic = read_u32(bytes, 0);
385        if magic == FAT_MAGIC || magic == FAT_MAGIC_64 {
386            return Err(Error::FatNotSupported);
387        }
388        // Big-endian variants of the fat magic on a little-endian
389        // host show up as the swapped values; refuse those too.
390        let magic_be = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
391        if magic_be == FAT_MAGIC || magic_be == FAT_MAGIC_64 {
392            return Err(Error::FatNotSupported);
393        }
394        if magic == MH_MAGIC || magic == 0xcefa_edfe {
395            return Err(Error::Macho32NotSupported(magic));
396        }
397        if magic != MH_MAGIC_64 {
398            return Err(Error::BadMagic(magic));
399        }
400        let header = MachHeader64::parse(bytes)?;
401        match header.cputype {
402            CPU_TYPE_X86_64 | CPU_TYPE_ARM64 => {}
403            other => return Err(Error::UnsupportedCpu(other)),
404        }
405
406        let commands = parse_load_commands(bytes, &header)?;
407        let (segment_data, segment_cmd_indices) = collect_segment_data(bytes, &commands)?;
408        let regions = build_regions(&header, &commands)?;
409        let padding = compute_padding(bytes, &regions);
410
411        Ok(Self {
412            header,
413            commands,
414            segment_data,
415            segment_cmd_indices,
416            padding,
417            file_size: bytes.len() as u64,
418        })
419    }
420
421    /// Reconstruct from already-parsed pieces. Used by the lower
422    /// path when assembling a Mach-O from `.ud` source.
423    #[must_use]
424    pub fn from_parts(
425        header: MachHeader64,
426        commands: Vec<LoadCommand>,
427        segment_data: Vec<Vec<u8>>,
428        segment_cmd_indices: Vec<usize>,
429        padding: Vec<(u64, Vec<u8>)>,
430        file_size: u64,
431    ) -> Self {
432        Self {
433            header,
434            commands,
435            segment_data,
436            segment_cmd_indices,
437            padding,
438            file_size,
439        }
440    }
441
442    /// Architecture flavour this file targets. Returns `None`
443    /// when the `cputype` isn't one v1 supports — `parse` already
444    /// rejects unsupported types, so this can only happen via
445    /// `from_parts`.
446    #[must_use]
447    pub fn cpu(&self) -> Option<MachoCpu> {
448        match self.header.cputype {
449            CPU_TYPE_X86_64 => Some(MachoCpu::X86_64),
450            CPU_TYPE_ARM64 => Some(MachoCpu::Arm64),
451            _ => None,
452        }
453    }
454
455    /// Walk every `LC_SEGMENT_64` and return a structurally-decoded
456    /// view of each segment + its sections. The raw bytes still
457    /// live in `commands[i].body`; this is purely a read-side
458    /// convenience for callers that don't want to re-parse the
459    /// fixed `LC_SEGMENT_64` layout themselves.
460    #[must_use]
461    pub fn segments(&self) -> Vec<Segment64> {
462        let mut out = Vec::new();
463        for (idx, cmd) in self.commands.iter().enumerate() {
464            if cmd.cmd != LC_SEGMENT_64 {
465                continue;
466            }
467            if let Some(seg) = Segment64::parse(idx, cmd) {
468                out.push(seg);
469            }
470        }
471        out
472    }
473
474    /// Segment file data parallel to the `LC_SEGMENT_64` commands
475    /// in `self.commands`.  Each entry corresponds to the segment
476    /// whose `cmd_index` is at the matching slot in
477    /// [`Self::segment_command_indices`].
478    #[must_use]
479    pub fn segment_data(&self) -> &[Vec<u8>] {
480        &self.segment_data
481    }
482
483    /// Indices into `commands` for each `segment_data` entry.
484    #[must_use]
485    pub fn segment_command_indices(&self) -> &[usize] {
486        &self.segment_cmd_indices
487    }
488
489    /// Padding bytes — gaps between structured regions, stored as
490    /// `(file_offset, bytes)`.
491    #[must_use]
492    pub fn padding(&self) -> &[(u64, Vec<u8>)] {
493        &self.padding
494    }
495
496    /// Total on-disk size in bytes.
497    #[must_use]
498    pub fn file_size(&self) -> u64 {
499        self.file_size
500    }
501
502    /// Serialize back to bytes. The contract is byte-identity:
503    /// `parse(b)?.write_to_vec() == b` for every supported input.
504    #[must_use]
505    pub fn write_to_vec(&self) -> Vec<u8> {
506        let mut out = vec![0u8; self.file_size as usize];
507        // Segments first — they cover the bulk of the file
508        // (including the bytes that overlay the header + load
509        // command table for the leading `__TEXT` segment, which
510        // is how Mach-O lays out executables). Writing the
511        // structured header + commands AFTER segments keeps the
512        // header's parsed-fields source-of-truth even when a
513        // segment overlaps it.
514        for (i, data) in self.segment_data.iter().enumerate() {
515            let cmd_idx = self.segment_cmd_indices[i];
516            let seg = self
517                .commands
518                .get(cmd_idx)
519                .and_then(|c| Segment64::parse(cmd_idx, c));
520            if let Some(seg) = seg {
521                if seg.filesize > 0 && !data.is_empty() {
522                    let off = seg.fileoff as usize;
523                    out[off..off + data.len()].copy_from_slice(data);
524                }
525            }
526        }
527
528        // Header at offset 0.
529        self.header.write(&mut out[..MACH_HEADER_64_SIZE as usize]);
530
531        // Load-command table immediately after the header.
532        let mut cursor = MACH_HEADER_64_SIZE as usize;
533        for cmd in &self.commands {
534            write_u32(&mut out, cursor, cmd.cmd);
535            write_u32(&mut out, cursor + 4, cmd.cmdsize);
536            out[cursor + 8..cursor + 8 + cmd.body.len()].copy_from_slice(&cmd.body);
537            cursor += cmd.cmdsize as usize;
538        }
539
540        // Padding (interstitial alignment bytes the parse pass
541        // captured verbatim).
542        for (offset, bytes) in &self.padding {
543            let off = *offset as usize;
544            out[off..off + bytes.len()].copy_from_slice(bytes);
545        }
546
547        out
548    }
549}
550
551impl Segment64 {
552    fn parse(cmd_index: usize, cmd: &LoadCommand) -> Option<Self> {
553        if cmd.cmd != LC_SEGMENT_64 {
554            return None;
555        }
556        // body = bytes after the `cmd`/`cmdsize` prefix; the
557        // SEGMENT_64_PREFIX_SIZE - 8 = 64 bytes describe the
558        // segment itself, followed by `nsects` x 80-byte
559        // section_64 entries.
560        let body = &cmd.body;
561        if body.len() < SEGMENT_64_PREFIX_SIZE - 8 {
562            return None;
563        }
564        let mut segname = [0u8; 16];
565        segname.copy_from_slice(&body[0..16]);
566        let vmaddr = read_u64(body, 16);
567        let vmsize = read_u64(body, 24);
568        let fileoff = read_u64(body, 32);
569        let filesize = read_u64(body, 40);
570        let maxprot = read_u32(body, 48);
571        let initprot = read_u32(body, 52);
572        let nsects = read_u32(body, 56);
573        let flags = read_u32(body, 60);
574
575        let mut sections = Vec::with_capacity(nsects as usize);
576        let sect_start = SEGMENT_64_PREFIX_SIZE - 8; // = 64
577        let sect_size = 80;
578        for i in 0..nsects as usize {
579            let off = sect_start + i * sect_size;
580            if body.len() < off + sect_size {
581                return None;
582            }
583            let s = &body[off..off + sect_size];
584            let mut sectname = [0u8; 16];
585            sectname.copy_from_slice(&s[0..16]);
586            let mut sn = [0u8; 16];
587            sn.copy_from_slice(&s[16..32]);
588            sections.push(Section64 {
589                sectname,
590                segname: sn,
591                addr: read_u64(s, 32),
592                size: read_u64(s, 40),
593                offset: read_u32(s, 48),
594                align: read_u32(s, 52),
595                reloff: read_u32(s, 56),
596                nreloc: read_u32(s, 60),
597                flags: read_u32(s, 64),
598                reserved1: read_u32(s, 68),
599                reserved2: read_u32(s, 72),
600                reserved3: read_u32(s, 76),
601            });
602        }
603
604        Some(Self {
605            cmd_index,
606            segname,
607            vmaddr,
608            vmsize,
609            fileoff,
610            filesize,
611            maxprot,
612            initprot,
613            nsects,
614            flags,
615            sections,
616        })
617    }
618
619    /// Serialize this segment back to the bytes that follow the
620    /// 8-byte `cmd`/`cmdsize` prefix of a `LC_SEGMENT_64` load
621    /// command — i.e. the body that round-trips through
622    /// [`LoadCommand::body`]. Output length is
623    /// `64 + 80 * sections.len()`.
624    #[must_use]
625    pub fn write_to_body(&self) -> Vec<u8> {
626        let mut out = vec![0u8; 64 + 80 * self.sections.len()];
627        out[0..16].copy_from_slice(&self.segname);
628        out[16..24].copy_from_slice(&self.vmaddr.to_le_bytes());
629        out[24..32].copy_from_slice(&self.vmsize.to_le_bytes());
630        out[32..40].copy_from_slice(&self.fileoff.to_le_bytes());
631        out[40..48].copy_from_slice(&self.filesize.to_le_bytes());
632        out[48..52].copy_from_slice(&self.maxprot.to_le_bytes());
633        out[52..56].copy_from_slice(&self.initprot.to_le_bytes());
634        out[56..60].copy_from_slice(&self.nsects.to_le_bytes());
635        out[60..64].copy_from_slice(&self.flags.to_le_bytes());
636        for (i, s) in self.sections.iter().enumerate() {
637            let off = 64 + i * 80;
638            out[off..off + 16].copy_from_slice(&s.sectname);
639            out[off + 16..off + 32].copy_from_slice(&s.segname);
640            out[off + 32..off + 40].copy_from_slice(&s.addr.to_le_bytes());
641            out[off + 40..off + 48].copy_from_slice(&s.size.to_le_bytes());
642            out[off + 48..off + 52].copy_from_slice(&s.offset.to_le_bytes());
643            out[off + 52..off + 56].copy_from_slice(&s.align.to_le_bytes());
644            out[off + 56..off + 60].copy_from_slice(&s.reloff.to_le_bytes());
645            out[off + 60..off + 64].copy_from_slice(&s.nreloc.to_le_bytes());
646            out[off + 64..off + 68].copy_from_slice(&s.flags.to_le_bytes());
647            out[off + 68..off + 72].copy_from_slice(&s.reserved1.to_le_bytes());
648            out[off + 72..off + 76].copy_from_slice(&s.reserved2.to_le_bytes());
649            out[off + 76..off + 80].copy_from_slice(&s.reserved3.to_le_bytes());
650        }
651        out
652    }
653}
654
655// ---------- structured load-command bodies ----------
656
657/// Structurally decoded body of an `LC_SYMTAB` command. Four
658/// `u32`s sized exactly 16 bytes on disk.
659#[derive(Debug, Clone, Copy, PartialEq, Eq)]
660pub struct LcSymtab {
661    /// File offset of the `nlist_64` table.
662    pub symoff: u32,
663    /// Number of symbols in the table.
664    pub nsyms: u32,
665    /// File offset of the string table.
666    pub stroff: u32,
667    /// String table size in bytes.
668    pub strsize: u32,
669}
670
671impl LcSymtab {
672    /// Decode from a 16-byte command body. Returns `None` on
673    /// length mismatch so the caller can fall back to opaque
674    /// bytes for unrecognised inputs.
675    #[must_use]
676    pub fn decode(body: &[u8]) -> Option<Self> {
677        if body.len() != 16 {
678            return None;
679        }
680        Some(Self {
681            symoff: read_u32(body, 0),
682            nsyms: read_u32(body, 4),
683            stroff: read_u32(body, 8),
684            strsize: read_u32(body, 12),
685        })
686    }
687
688    /// Encode back to the 16-byte body that
689    /// [`LoadCommand::body`] carries.
690    #[must_use]
691    pub fn encode(&self) -> Vec<u8> {
692        let mut out = vec![0u8; 16];
693        out[0..4].copy_from_slice(&self.symoff.to_le_bytes());
694        out[4..8].copy_from_slice(&self.nsyms.to_le_bytes());
695        out[8..12].copy_from_slice(&self.stroff.to_le_bytes());
696        out[12..16].copy_from_slice(&self.strsize.to_le_bytes());
697        out
698    }
699}
700
701/// Structurally decoded body of an `LC_DYSYMTAB` command — 18
702/// `u32`s describing partitions of the LC_SYMTAB table and
703/// auxiliary indirect-symbol / module / reference tables.
704#[allow(clippy::struct_field_names)]
705#[derive(Debug, Clone, Copy, PartialEq, Eq)]
706pub struct LcDysymtab {
707    pub ilocalsym: u32,
708    pub nlocalsym: u32,
709    pub iextdefsym: u32,
710    pub nextdefsym: u32,
711    pub iundefsym: u32,
712    pub nundefsym: u32,
713    pub tocoff: u32,
714    pub ntoc: u32,
715    pub modtaboff: u32,
716    pub nmodtab: u32,
717    pub extrefsymoff: u32,
718    pub nextrefsyms: u32,
719    pub indirectsymoff: u32,
720    pub nindirectsyms: u32,
721    pub extreloff: u32,
722    pub nextrel: u32,
723    pub locreloff: u32,
724    pub nlocrel: u32,
725}
726
727impl LcDysymtab {
728    #[must_use]
729    pub fn decode(body: &[u8]) -> Option<Self> {
730        if body.len() != 72 {
731            return None;
732        }
733        Some(Self {
734            ilocalsym: read_u32(body, 0),
735            nlocalsym: read_u32(body, 4),
736            iextdefsym: read_u32(body, 8),
737            nextdefsym: read_u32(body, 12),
738            iundefsym: read_u32(body, 16),
739            nundefsym: read_u32(body, 20),
740            tocoff: read_u32(body, 24),
741            ntoc: read_u32(body, 28),
742            modtaboff: read_u32(body, 32),
743            nmodtab: read_u32(body, 36),
744            extrefsymoff: read_u32(body, 40),
745            nextrefsyms: read_u32(body, 44),
746            indirectsymoff: read_u32(body, 48),
747            nindirectsyms: read_u32(body, 52),
748            extreloff: read_u32(body, 56),
749            nextrel: read_u32(body, 60),
750            locreloff: read_u32(body, 64),
751            nlocrel: read_u32(body, 68),
752        })
753    }
754
755    #[must_use]
756    pub fn encode(&self) -> Vec<u8> {
757        let mut out = vec![0u8; 72];
758        for (i, v) in [
759            self.ilocalsym,
760            self.nlocalsym,
761            self.iextdefsym,
762            self.nextdefsym,
763            self.iundefsym,
764            self.nundefsym,
765            self.tocoff,
766            self.ntoc,
767            self.modtaboff,
768            self.nmodtab,
769            self.extrefsymoff,
770            self.nextrefsyms,
771            self.indirectsymoff,
772            self.nindirectsyms,
773            self.extreloff,
774            self.nextrel,
775            self.locreloff,
776            self.nlocrel,
777        ]
778        .iter()
779        .enumerate()
780        {
781            out[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
782        }
783        out
784    }
785}
786
787/// Body of `LC_LOAD_DYLINKER` (and the analogous `LC_ID_DYLINKER`
788/// — same shape). The `offset` is from the start of the command
789/// (including the cmd/cmdsize prefix), which in the canonical
790/// linker output is `0xc` — i.e. the name begins at body offset
791/// 4. `name` holds the C string up to (but not including) the
792/// first NUL; `tail_padding` carries the NUL + any trailing
793/// alignment NULs verbatim, so the encoded length always matches
794/// the original.
795#[derive(Debug, Clone, PartialEq, Eq)]
796pub struct LcDylinker {
797    pub offset: u32,
798    pub name: Vec<u8>,
799    pub tail_padding: Vec<u8>,
800}
801
802impl LcDylinker {
803    #[must_use]
804    pub fn decode(body: &[u8]) -> Option<Self> {
805        if body.len() < 4 {
806            return None;
807        }
808        let offset = read_u32(body, 0);
809        // offset is measured from start-of-command, so the name
810        // begins at body offset `offset - 8`.
811        let name_off = offset.checked_sub(8)? as usize;
812        if name_off > body.len() {
813            return None;
814        }
815        let tail = &body[name_off..];
816        let nul = tail.iter().position(|&b| b == 0).unwrap_or(tail.len());
817        let name = tail[..nul].to_vec();
818        let tail_padding = tail[nul..].to_vec();
819        // Body offset 4..name_off is reserved zero padding the
820        // linker rarely (never?) sets non-zero. We require it to
821        // be zero so the structured form is unambiguous; the
822        // caller falls back to opaque bytes if it isn't.
823        if body[4..name_off].iter().any(|&b| b != 0) {
824            return None;
825        }
826        Some(Self {
827            offset,
828            name,
829            tail_padding,
830        })
831    }
832
833    #[must_use]
834    pub fn encode(&self) -> Vec<u8> {
835        let name_off = (self.offset as usize).saturating_sub(8);
836        let mut out = vec![0u8; name_off + self.name.len() + self.tail_padding.len()];
837        out[0..4].copy_from_slice(&self.offset.to_le_bytes());
838        // bytes 4..name_off stay zero
839        out[name_off..name_off + self.name.len()].copy_from_slice(&self.name);
840        out[name_off + self.name.len()..].copy_from_slice(&self.tail_padding);
841        out
842    }
843}
844
845/// Body of `LC_LOAD_DYLIB` / `LC_ID_DYLIB` / `LC_LOAD_WEAK_DYLIB`
846/// / `LC_REEXPORT_DYLIB`. The same 4-u32 dylib record followed by
847/// the NUL-padded name string.
848#[derive(Debug, Clone, PartialEq, Eq)]
849pub struct LcDylib {
850    pub offset: u32,
851    pub timestamp: u32,
852    pub current_version: u32,
853    pub compatibility_version: u32,
854    pub name: Vec<u8>,
855    pub tail_padding: Vec<u8>,
856}
857
858impl LcDylib {
859    #[must_use]
860    pub fn decode(body: &[u8]) -> Option<Self> {
861        if body.len() < 16 {
862            return None;
863        }
864        let offset = read_u32(body, 0);
865        let timestamp = read_u32(body, 4);
866        let current_version = read_u32(body, 8);
867        let compatibility_version = read_u32(body, 12);
868        let name_off = offset.checked_sub(8)? as usize;
869        if name_off > body.len() || name_off < 16 {
870            return None;
871        }
872        if body[16..name_off].iter().any(|&b| b != 0) {
873            return None;
874        }
875        let tail = &body[name_off..];
876        let nul = tail.iter().position(|&b| b == 0).unwrap_or(tail.len());
877        Some(Self {
878            offset,
879            timestamp,
880            current_version,
881            compatibility_version,
882            name: tail[..nul].to_vec(),
883            tail_padding: tail[nul..].to_vec(),
884        })
885    }
886
887    #[must_use]
888    pub fn encode(&self) -> Vec<u8> {
889        let name_off = (self.offset as usize).saturating_sub(8);
890        let mut out = vec![0u8; name_off + self.name.len() + self.tail_padding.len()];
891        out[0..4].copy_from_slice(&self.offset.to_le_bytes());
892        out[4..8].copy_from_slice(&self.timestamp.to_le_bytes());
893        out[8..12].copy_from_slice(&self.current_version.to_le_bytes());
894        out[12..16].copy_from_slice(&self.compatibility_version.to_le_bytes());
895        out[name_off..name_off + self.name.len()].copy_from_slice(&self.name);
896        out[name_off + self.name.len()..].copy_from_slice(&self.tail_padding);
897        out
898    }
899}
900
901/// Body of `LC_BUILD_VERSION`. Records the platform / minimum OS
902/// / SDK / per-tool versions. `ntools` is recovered from
903/// `tools.len()` at encode time.
904#[derive(Debug, Clone, PartialEq, Eq)]
905pub struct LcBuildVersion {
906    pub platform: u32,
907    pub minos: u32,
908    pub sdk: u32,
909    pub tools: Vec<BuildVersionTool>,
910}
911
912#[derive(Debug, Clone, Copy, PartialEq, Eq)]
913pub struct BuildVersionTool {
914    pub tool: u32,
915    pub version: u32,
916}
917
918impl LcBuildVersion {
919    #[must_use]
920    pub fn decode(body: &[u8]) -> Option<Self> {
921        if body.len() < 16 {
922            return None;
923        }
924        let platform = read_u32(body, 0);
925        let minos = read_u32(body, 4);
926        let sdk = read_u32(body, 8);
927        let ntools = read_u32(body, 12) as usize;
928        if body.len() != 16 + 8 * ntools {
929            return None;
930        }
931        let mut tools = Vec::with_capacity(ntools);
932        for i in 0..ntools {
933            let off = 16 + 8 * i;
934            tools.push(BuildVersionTool {
935                tool: read_u32(body, off),
936                version: read_u32(body, off + 4),
937            });
938        }
939        Some(Self {
940            platform,
941            minos,
942            sdk,
943            tools,
944        })
945    }
946
947    #[must_use]
948    pub fn encode(&self) -> Vec<u8> {
949        let mut out = vec![0u8; 16 + 8 * self.tools.len()];
950        out[0..4].copy_from_slice(&self.platform.to_le_bytes());
951        out[4..8].copy_from_slice(&self.minos.to_le_bytes());
952        out[8..12].copy_from_slice(&self.sdk.to_le_bytes());
953        out[12..16].copy_from_slice(&(self.tools.len() as u32).to_le_bytes());
954        for (i, t) in self.tools.iter().enumerate() {
955            let off = 16 + 8 * i;
956            out[off..off + 4].copy_from_slice(&t.tool.to_le_bytes());
957            out[off + 4..off + 8].copy_from_slice(&t.version.to_le_bytes());
958        }
959        out
960    }
961}
962
963/// Body of `LC_MAIN`. Entry-point offset + initial stack size.
964#[derive(Debug, Clone, Copy, PartialEq, Eq)]
965pub struct LcMain {
966    pub entryoff: u64,
967    pub stacksize: u64,
968}
969
970impl LcMain {
971    #[must_use]
972    pub fn decode(body: &[u8]) -> Option<Self> {
973        if body.len() != 16 {
974            return None;
975        }
976        Some(Self {
977            entryoff: read_u64(body, 0),
978            stacksize: read_u64(body, 8),
979        })
980    }
981
982    #[must_use]
983    pub fn encode(&self) -> Vec<u8> {
984        let mut out = vec![0u8; 16];
985        out[0..8].copy_from_slice(&self.entryoff.to_le_bytes());
986        out[8..16].copy_from_slice(&self.stacksize.to_le_bytes());
987        out
988    }
989}
990
991/// Body of every `linkedit_data_command`-shaped load command
992/// (LC_CODE_SIGNATURE, LC_FUNCTION_STARTS, LC_DATA_IN_CODE,
993/// LC_DYLD_EXPORTS_TRIE, LC_DYLD_CHAINED_FIXUPS, ...).
994#[derive(Debug, Clone, Copy, PartialEq, Eq)]
995pub struct LcLinkeditData {
996    pub dataoff: u32,
997    pub datasize: u32,
998}
999
1000impl LcLinkeditData {
1001    #[must_use]
1002    pub fn decode(body: &[u8]) -> Option<Self> {
1003        if body.len() != 8 {
1004            return None;
1005        }
1006        Some(Self {
1007            dataoff: read_u32(body, 0),
1008            datasize: read_u32(body, 4),
1009        })
1010    }
1011
1012    #[must_use]
1013    pub fn encode(&self) -> Vec<u8> {
1014        let mut out = vec![0u8; 8];
1015        out[0..4].copy_from_slice(&self.dataoff.to_le_bytes());
1016        out[4..8].copy_from_slice(&self.datasize.to_le_bytes());
1017        out
1018    }
1019}
1020
1021/// Body of `LC_SOURCE_VERSION`: one packed 64-bit version.
1022#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1023pub struct LcSourceVersion(pub u64);
1024
1025impl LcSourceVersion {
1026    #[must_use]
1027    pub fn decode(body: &[u8]) -> Option<Self> {
1028        if body.len() != 8 {
1029            return None;
1030        }
1031        Some(Self(read_u64(body, 0)))
1032    }
1033
1034    #[must_use]
1035    pub fn encode(&self) -> Vec<u8> {
1036        self.0.to_le_bytes().to_vec()
1037    }
1038}
1039
1040/// Body of `LC_UUID`: 16 raw bytes.
1041#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1042pub struct LcUuid(pub [u8; 16]);
1043
1044impl LcUuid {
1045    #[must_use]
1046    pub fn decode(body: &[u8]) -> Option<Self> {
1047        if body.len() != 16 {
1048            return None;
1049        }
1050        let mut u = [0u8; 16];
1051        u.copy_from_slice(body);
1052        Some(Self(u))
1053    }
1054
1055    #[must_use]
1056    pub fn encode(&self) -> Vec<u8> {
1057        self.0.to_vec()
1058    }
1059}
1060
1061/// True when `cmd` is one of the linkedit_data_command-shaped
1062/// load commands (the 4+4-byte body pattern).
1063#[must_use]
1064pub fn is_linkedit_data_cmd(cmd: u32) -> bool {
1065    matches!(
1066        cmd,
1067        LC_CODE_SIGNATURE
1068            | LC_FUNCTION_STARTS
1069            | LC_DATA_IN_CODE
1070            | LC_DYLD_EXPORTS_TRIE
1071            | LC_DYLD_CHAINED_FIXUPS
1072            | LC_DYLIB_CODE_SIGN_DRS
1073            | LC_LINKER_OPTIMIZATION_HINT
1074    )
1075}
1076
1077/// True when `cmd` is one of the dylib-shaped load commands.
1078#[must_use]
1079pub fn is_dylib_cmd(cmd: u32) -> bool {
1080    matches!(
1081        cmd,
1082        LC_LOAD_DYLIB | LC_LOAD_WEAK_DYLIB | LC_REEXPORT_DYLIB | LC_ID_DYLIB
1083    )
1084}
1085
1086// ---------- internal helpers ----------
1087
1088fn ensure_len(bytes: &[u8], offset: u64, needed: u64) -> Result<()> {
1089    let end = offset.saturating_add(needed);
1090    if (bytes.len() as u64) < end {
1091        return Err(Error::Truncated {
1092            offset,
1093            needed,
1094            have: bytes.len() as u64,
1095        });
1096    }
1097    Ok(())
1098}
1099
1100fn read_u32(bytes: &[u8], offset: usize) -> u32 {
1101    u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap())
1102}
1103
1104fn read_u64(bytes: &[u8], offset: usize) -> u64 {
1105    u64::from_le_bytes(bytes[offset..offset + 8].try_into().unwrap())
1106}
1107
1108fn write_u32(bytes: &mut [u8], offset: usize, value: u32) {
1109    bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
1110}
1111
1112fn parse_load_commands(bytes: &[u8], header: &MachHeader64) -> Result<Vec<LoadCommand>> {
1113    let table_start = MACH_HEADER_64_SIZE;
1114    let table_end = table_start + u64::from(header.sizeofcmds);
1115    ensure_len(bytes, table_start, u64::from(header.sizeofcmds))?;
1116
1117    let mut commands = Vec::with_capacity(header.ncmds as usize);
1118    let mut cursor = table_start;
1119    for _ in 0..header.ncmds {
1120        if cursor + 8 > table_end {
1121            return Err(Error::LoadCmdOverrun {
1122                cursor,
1123                end: table_end,
1124            });
1125        }
1126        let cmd = read_u32(bytes, cursor as usize);
1127        let cmdsize = read_u32(bytes, cursor as usize + 4);
1128        if cmdsize < 8 {
1129            return Err(Error::BadLoadCmdSize {
1130                offset: cursor,
1131                cmdsize,
1132            });
1133        }
1134        let next = cursor + u64::from(cmdsize);
1135        if next > table_end {
1136            return Err(Error::LoadCmdOverrun {
1137                cursor: next,
1138                end: table_end,
1139            });
1140        }
1141        let body_start = cursor as usize + 8;
1142        let body_end = next as usize;
1143        let body = bytes[body_start..body_end].to_vec();
1144        commands.push(LoadCommand { cmd, cmdsize, body });
1145        cursor = next;
1146    }
1147    if cursor != table_end {
1148        return Err(Error::LoadCmdOverrun {
1149            cursor,
1150            end: table_end,
1151        });
1152    }
1153    Ok(commands)
1154}
1155
1156fn collect_segment_data(
1157    bytes: &[u8],
1158    commands: &[LoadCommand],
1159) -> Result<(Vec<Vec<u8>>, Vec<usize>)> {
1160    let mut data = Vec::new();
1161    let mut indices = Vec::new();
1162    for (idx, cmd) in commands.iter().enumerate() {
1163        if cmd.cmd != LC_SEGMENT_64 {
1164            continue;
1165        }
1166        let Some(seg) = Segment64::parse(idx, cmd) else {
1167            continue;
1168        };
1169        indices.push(idx);
1170        if seg.filesize == 0 {
1171            data.push(Vec::new());
1172            continue;
1173        }
1174        let end = seg
1175            .fileoff
1176            .checked_add(seg.filesize)
1177            .ok_or_else(|| Error::RegionOverflow {
1178                label: format!("segment #{idx} ({:?})", seg.name()),
1179                offset: seg.fileoff,
1180                size: seg.filesize,
1181            })?;
1182        ensure_len(bytes, seg.fileoff, seg.filesize)?;
1183        data.push(bytes[seg.fileoff as usize..end as usize].to_vec());
1184    }
1185    Ok((data, indices))
1186}
1187
1188struct Region {
1189    /// Human label, retained for future overlap diagnostics
1190    /// (matching the ELF crate's pattern); unused today because
1191    /// the Mach-O layout is intentionally permissive about
1192    /// overlapping segment / header ranges.
1193    #[allow(dead_code)]
1194    label: String,
1195    range: Range<u64>,
1196}
1197
1198fn build_regions(header: &MachHeader64, commands: &[LoadCommand]) -> Result<Vec<Region>> {
1199    let mut regions = Vec::new();
1200
1201    // Header.
1202    regions.push(Region {
1203        label: "Mach-O header".into(),
1204        range: 0..MACH_HEADER_64_SIZE,
1205    });
1206
1207    // Load-command table.
1208    if header.sizeofcmds > 0 {
1209        regions.push(Region {
1210            label: "load-command table".into(),
1211            range: MACH_HEADER_64_SIZE..MACH_HEADER_64_SIZE + u64::from(header.sizeofcmds),
1212        });
1213    }
1214
1215    // Segments. We treat segments as opaque regions: their file
1216    // ranges may overlap with the header/load-command table (the
1217    // leading `__TEXT` segment of an executable straddles offset
1218    // 0), and they may overlap with each other in pathological
1219    // cases — we *don't* enforce non-overlap here, just merge
1220    // identical-start regions into one for padding purposes.
1221    for (idx, cmd) in commands.iter().enumerate() {
1222        if cmd.cmd != LC_SEGMENT_64 {
1223            continue;
1224        }
1225        let Some(seg) = Segment64::parse(idx, cmd) else {
1226            continue;
1227        };
1228        if seg.filesize == 0 {
1229            continue;
1230        }
1231        let end = seg
1232            .fileoff
1233            .checked_add(seg.filesize)
1234            .ok_or_else(|| Error::RegionOverflow {
1235                label: format!("segment #{idx} ({:?})", seg.name()),
1236                offset: seg.fileoff,
1237                size: seg.filesize,
1238            })?;
1239        regions.push(Region {
1240            label: format!("segment #{idx} ({:?})", seg.name()),
1241            range: seg.fileoff..end,
1242        });
1243    }
1244
1245    regions.sort_by_key(|r| r.range.start);
1246    Ok(regions)
1247}
1248
1249fn compute_padding(bytes: &[u8], regions: &[Region]) -> Vec<(u64, Vec<u8>)> {
1250    let mut padding = Vec::new();
1251    let file_end = bytes.len() as u64;
1252    let mut cursor = 0u64;
1253    for region in regions {
1254        if region.range.start > cursor {
1255            let start = cursor as usize;
1256            let end = region.range.start as usize;
1257            padding.push((cursor, bytes[start..end].to_vec()));
1258        }
1259        cursor = cursor.max(region.range.end);
1260    }
1261    if cursor < file_end {
1262        let start = cursor as usize;
1263        let end = file_end as usize;
1264        padding.push((cursor, bytes[start..end].to_vec()));
1265    }
1266    padding
1267}
1268
1269#[cfg(test)]
1270mod tests {
1271    use super::*;
1272
1273    #[test]
1274    fn magic_detection() {
1275        let buf = [0xcf, 0xfa, 0xed, 0xfe];
1276        assert!(is_macho(&buf));
1277        assert!(is_macho64(&buf));
1278        let fat = [0xca, 0xfe, 0xba, 0xbe];
1279        assert!(is_macho(&fat));
1280        assert!(is_fat(&fat));
1281        assert!(!is_macho64(&fat));
1282    }
1283
1284    #[test]
1285    fn rejects_short_input() {
1286        let err = MachoFile::parse(b"\x7fELF").unwrap_err();
1287        assert!(matches!(err, Error::BadMagic(_)));
1288    }
1289}