// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

/*! Mach-O primitives related to code signing

Code signing data is embedded within the named `__LINKEDIT` segment of
the Mach-O binary. An `LC_CODE_SIGNATURE` load command in the Mach-O header
will point you at this data. See `find_signature_data()` for this logic.

Within the `__LINKEDIT` segment is a superblob defining embedded signature
data.
*/

use {
    crate::{
        cryptography::DigestType, embedded_signature::EmbeddedSignature, error::AppleCodesignError,
    },
    goblin::mach::{
        constants::{SEG_LINKEDIT, SEG_TEXT},
        header::MH_EXECUTE,
        load_command::{
            CommandVariant, LinkeditDataCommand, LC_BUILD_VERSION, SIZEOF_LINKEDIT_DATA_COMMAND,
        },
        parse_magic_and_ctx,
        segment::Segment,
        Mach, MachO, SingleArch,
    },
    rayon::prelude::*,
    scroll::Pread,
};

/// A Mach-O binary.
pub struct MachOBinary<'a> {
    /// Index within a fat binary this Mach-O resides at.
    ///
    /// If `None`, this is not inside a fat binary.
    pub index: Option<usize>,

    /// The parsed Mach-O binary.
    pub macho: MachO<'a>,

    /// The raw data backing the Mach-O binary.
    pub data: &'a [u8],
}

impl<'a> MachOBinary<'a> {
    /// Parse a non-universal Mach-O binary from raw data.
    pub fn parse(data: &'a [u8]) -> Result<Self, AppleCodesignError> {
        let macho = MachO::parse(data, 0)?;

        Ok(Self {
            index: None,
            macho,
            data,
        })
    }
}

impl<'a> MachOBinary<'a> {
    /// Find the __LINKEDIT segment and its segment index.
    pub fn linkedit_index_and_segment(&self) -> Option<(usize, &Segment<'a>)> {
        self.macho
            .segments
            .iter()
            .enumerate()
            .find(|(_, segment)| matches!(segment.name(), Ok(SEG_LINKEDIT)))
    }

    /// Find the __LINKEDIT segment.
    pub fn linkedit_segment(&self) -> Option<&Segment<'a>> {
        self.linkedit_index_and_segment().map(|(_, x)| x)
    }

    /// Find the __LINKEDIT segment, asserting it exists and it is the final segment.
    pub fn linkedit_segment_assert_last(&self) -> Result<&Segment<'a>, AppleCodesignError> {
        let last_segment = self
            .segments_by_file_offset()
            .last()
            .copied()
            .ok_or(AppleCodesignError::MissingLinkedit)?;

        if !matches!(last_segment.name(), Ok(SEG_LINKEDIT)) {
            Err(AppleCodesignError::LinkeditNotLast)
        } else {
            Ok(last_segment)
        }
    }

    /// Attempt to extract a reference to raw signature data in a Mach-O binary.
    ///
    /// An `LC_CODE_SIGNATURE` load command in the Mach-O file header points to
    /// signature data in the `__LINKEDIT` segment.
    ///
    /// This function is used as part of parsing signature data. You probably want to
    /// use a function that parses referenced data.
    pub fn find_signature_data(
        &self,
    ) -> Result<Option<MachOSignatureData<'a>>, AppleCodesignError> {
        if let Some(linkedit_data_command) = self.code_signature_load_command() {
            // Now find the slice of data in the __LINKEDIT segment we need to parse.
            let (linkedit_segment_index, linkedit) = self
                .linkedit_index_and_segment()
                .ok_or(AppleCodesignError::MissingLinkedit)?;

            let linkedit_segment_start_offset = linkedit.fileoff as usize;
            let linkedit_segment_end_offset = linkedit_segment_start_offset + linkedit.data.len();
            let signature_file_start_offset = linkedit_data_command.dataoff as usize;
            let signature_file_end_offset =
                signature_file_start_offset + linkedit_data_command.datasize as usize;
            let signature_segment_start_offset =
                linkedit_data_command.dataoff as usize - linkedit.fileoff as usize;
            let signature_segment_end_offset =
                signature_segment_start_offset + linkedit_data_command.datasize as usize;

            let signature_data =
                &linkedit.data[signature_segment_start_offset..signature_segment_end_offset];

            Ok(Some(MachOSignatureData {
                linkedit_segment_index,
                linkedit_segment_start_offset,
                linkedit_segment_end_offset,
                signature_file_start_offset,
                signature_file_end_offset,
                signature_segment_start_offset,
                signature_segment_end_offset,
                linkedit_segment_data: linkedit.data,
                signature_data,
            }))
        } else {
            Ok(None)
        }
    }

    /// Obtain the code signature in the entity.
    ///
    /// Returns `Ok(None)` if no signature exists, `Ok(Some)` if it does, or
    /// `Err` if there is a parse error.
    pub fn code_signature(&self) -> Result<Option<EmbeddedSignature<'_>>, AppleCodesignError> {
        if let Some(signature) = self.find_signature_data()? {
            Ok(Some(EmbeddedSignature::from_bytes(
                signature.signature_data,
            )?))
        } else {
            Ok(None)
        }
    }

    /// Determine the start and end offset of the executable segment of a binary.
    pub fn executable_segment_boundary(&self) -> Result<(u64, u64), AppleCodesignError> {
        let segment = self
            .macho
            .segments
            .iter()
            .find(|segment| matches!(segment.name(), Ok(SEG_TEXT)))
            .ok_or_else(|| AppleCodesignError::InvalidBinary("no __TEXT segment".into()))?;

        Ok((segment.fileoff, segment.fileoff + segment.data.len() as u64))
    }

    /// Whether this is an executable Mach-O file.
    pub fn is_executable(&self) -> bool {
        self.macho.header.filetype == MH_EXECUTE
    }

    /// The start offset of the code signature data within the __LINKEDIT segment.
    pub fn code_signature_linkedit_start_offset(&self) -> Option<u32> {
        let segment = self.linkedit_segment();

        if let (Some(segment), Some(command)) = (segment, self.code_signature_load_command()) {
            Some((command.dataoff as u64 - segment.fileoff) as u32)
        } else {
            None
        }
    }

    /// The end offset of the code signature data within the __LINKEDIT segment.
    pub fn code_signature_linkedit_end_offset(&self) -> Option<u32> {
        let start_offset = self.code_signature_linkedit_start_offset()?;

        self.code_signature_load_command()
            .map(|command| start_offset + command.datasize)
    }

    /// Obtain Mach-O segments by file offset order.
    ///
    /// The header-defined order may vary by the file layout order. This ensures the ordering
    /// is by file layout.
    pub fn segments_by_file_offset(&self) -> Vec<&Segment<'a>> {
        let mut segments = self.macho.segments.iter().collect::<Vec<_>>();

        segments.sort_by(|a, b| a.fileoff.cmp(&b.fileoff));

        segments
    }

    /// The byte offset within the binary at which point "code" stops.
    ///
    /// If a signature is present, this is the offset of the start of the
    /// signature. Else it represents the end of the binary.
    pub fn code_limit_binary_offset(&self) -> Result<u64, AppleCodesignError> {
        let last_segment = self.linkedit_segment_assert_last()?;

        if let Some(offset) = self.code_signature_linkedit_start_offset() {
            Ok(last_segment.fileoff + offset as u64)
        } else {
            Ok(last_segment.fileoff + last_segment.data.len() as u64)
        }
    }

    /// Obtain __LINKEDIT segment data before the signature data.
    ///
    /// If there is no signature, returns all the data for the __LINKEDIT segment.
    pub fn linkedit_data_before_signature(&self) -> Option<&[u8]> {
        let segment = self.linkedit_segment();

        if let Some(segment) = segment {
            if let Some(offset) = self.code_signature_linkedit_start_offset() {
                Some(&segment.data[0..offset as usize])
            } else {
                Some(segment.data)
            }
        } else {
            None
        }
    }

    /// Obtain Mach-O binary data to be digested in code digests.
    ///
    /// Returns the raw data whose digests will be captured by the Code Directory code digests.
    pub fn digested_code_data(&self) -> Result<&[u8], AppleCodesignError> {
        let code_limit = self.code_limit_binary_offset()?;

        Ok(&self.data[0..code_limit as _])
    }

    /// Obtain the size in bytes of all code digests given a digest type and page size.
    pub fn code_digests_size(
        &self,
        digest: DigestType,
        page_size: usize,
    ) -> Result<usize, AppleCodesignError> {
        let empty = digest.digest_data(b"")?;

        Ok(self.digested_code_data()?.chunks(page_size).count() * empty.len())
    }

    /// Compute digests over code in this binary.
    pub fn code_digests(
        &self,
        digest: DigestType,
        page_size: usize,
    ) -> Result<Vec<Vec<u8>>, AppleCodesignError> {
        let data = self.digested_code_data()?;

        // Premature parallelism can be slower due to overhead of having to spin up threads.
        // So only do parallel digests if we have enough data to warrant it.
        if data.len() > 64 * 1024 * 1024 {
            data.par_chunks(page_size)
                .map(|c| digest.digest_data(c))
                .collect::<Result<Vec<_>, AppleCodesignError>>()
        } else {
            self.digested_code_data()?
                .chunks(page_size)
                .map(|chunk| digest.digest_data(chunk))
                .collect::<Result<Vec<_>, AppleCodesignError>>()
        }
    }

    /// Resolve the load command for the code signature.
    pub fn code_signature_load_command(&self) -> Option<LinkeditDataCommand> {
        self.macho.load_commands.iter().find_map(|lc| {
            if let CommandVariant::CodeSignature(command) = lc.command {
                Some(command)
            } else {
                None
            }
        })
    }

    /// Attempt to locate embedded Info.plist data.
    pub fn embedded_info_plist(&self) -> Result<Option<Vec<u8>>, AppleCodesignError> {
        // Mach-O binaries can have the Info.plist data in an `__info_plist` section
        // within the __TEXT segment.
        for segment in &self.macho.segments {
            if matches!(segment.name(), Ok(SEG_TEXT)) {
                for (section, data) in segment.sections()? {
                    if matches!(section.name(), Ok("__info_plist")) {
                        return Ok(Some(data.to_vec()));
                    }
                }
            }
        }

        Ok(None)
    }

    /// Determines whether this crate is capable of signing a given Mach-O binary.
    ///
    /// Code in this crate is limited in the amount of Mach-O binary manipulation
    /// it can perform (supporting rewriting all valid Mach-O binaries effectively
    /// requires low-level awareness of all Mach-O constructs in order to perform
    /// offset manipulation). This function can be used to test signing
    /// compatibility.
    ///
    /// We currently only support signing Mach-O files already containing an
    /// embedded signature. Often linked binaries automatically contain an embedded
    /// signature containing just the code directory (without a cryptographically
    /// signed signature), so this limitation hopefully isn't impactful.
    pub fn check_signing_capability(&self) -> Result<(), AppleCodesignError> {
        let last_segment = self.linkedit_segment_assert_last()?;

        // Rules:
        //
        // 1. If there is an existing signature, there must be no data in
        //    the binary after it. (We don't know how to update references to
        //    other data to reflect offset changes.)
        // 2. If there isn't an existing signature, there must be "room" between
        //    the last load command and the first section to write a new load
        //    command for the signature.

        if let Some(offset) = self.code_signature_linkedit_end_offset() {
            if offset as usize == last_segment.data.len() {
                Ok(())
            } else {
                Err(AppleCodesignError::DataAfterSignature)
            }
        } else {
            let last_load_command = self
                .macho
                .load_commands
                .iter()
                .last()
                .ok_or_else(|| AppleCodesignError::InvalidBinary("no load commands".into()))?;

            let first_section = self
                .macho
                .segments
                .iter()
                .map(|segment| segment.sections())
                .collect::<Result<Vec<_>, _>>()?
                .into_iter()
                .flatten()
                .next()
                .ok_or_else(|| AppleCodesignError::InvalidBinary("no sections".into()))?;

            let load_commands_end_offset =
                last_load_command.offset + last_load_command.command.cmdsize();

            if first_section.0.offset as usize - load_commands_end_offset
                >= SIZEOF_LINKEDIT_DATA_COMMAND
            {
                Ok(())
            } else {
                Err(AppleCodesignError::LoadCommandNoRoom)
            }
        }
    }

    /// Attempt to resolve the mach-o targeting settings.
    pub fn find_targeting(&self) -> Result<Option<MachoTarget>, AppleCodesignError> {
        let ctx = parse_magic_and_ctx(self.data, 0)?
            .1
            .expect("context should have been parsed before");

        for lc in &self.macho.load_commands {
            if lc.command.cmd() == LC_BUILD_VERSION {
                let build_version = self
                    .data
                    .pread_with::<BuildVersionCommand>(lc.offset, ctx.le)?;

                return Ok(Some(MachoTarget {
                    platform: build_version.platform.into(),
                    minimum_os_version: parse_version_nibbles(build_version.minos),
                    sdk_version: parse_version_nibbles(build_version.sdk),
                }));
            }
        }

        for lc in &self.macho.load_commands {
            let command = match lc.command {
                CommandVariant::VersionMinMacosx(c) => Some((c, Platform::MacOs)),
                CommandVariant::VersionMinIphoneos(c) => Some((c, Platform::IOs)),
                CommandVariant::VersionMinTvos(c) => Some((c, Platform::TvOs)),
                CommandVariant::VersionMinWatchos(c) => Some((c, Platform::WatchOs)),
                _ => None,
            };

            if let Some((command, platform)) = command {
                return Ok(Some(MachoTarget {
                    platform,
                    minimum_os_version: parse_version_nibbles(command.version),
                    sdk_version: parse_version_nibbles(command.sdk),
                }));
            }
        }

        Ok(None)
    }
}

/// Describes signature data embedded within a Mach-O binary.
pub struct MachOSignatureData<'a> {
    /// Which segment offset is the `__LINKEDIT` segment.
    pub linkedit_segment_index: usize,

    /// Start offset of `__LINKEDIT` segment within the binary.
    pub linkedit_segment_start_offset: usize,

    /// End offset of `__LINKEDIT` segment within the binary.
    pub linkedit_segment_end_offset: usize,

    /// Start offset of signature data in `__LINKEDIT` within the binary.
    pub signature_file_start_offset: usize,

    /// End offset of signature data in `__LINKEDIT` within the binary.
    pub signature_file_end_offset: usize,

    /// The start offset of the signature data within the `__LINKEDIT` segment.
    pub signature_segment_start_offset: usize,

    /// The end offset of the signature data within the `__LINKEDIT` segment.
    pub signature_segment_end_offset: usize,

    /// Raw data in the `__LINKEDIT` segment.
    pub linkedit_segment_data: &'a [u8],

    /// The signature data within the `__LINKEDIT` segment.
    pub signature_data: &'a [u8],
}

/// Content of an `LC_BUILD_VERSION` load command.
#[derive(Clone, Debug, Pread)]
pub struct BuildVersionCommand {
    /// LC_BUILD_VERSION
    pub cmd: u32,
    /// Size of load command data.
    ///
    /// sizeof(self) + self.ntools * sizeof(BuildToolsVersion)
    pub cmdsize: u32,
    /// Platform identifier.
    pub platform: u32,
    /// Minimum operating system version.
    ///
    /// X.Y.Z encoded in nibbles as xxxx.yy.zz.
    pub minos: u32,
    /// SDK version.
    ///
    /// X.Y.Z encoded in nibbles as xxxx.yy.zz.
    pub sdk: u32,
    /// Number of tools entries following this structure.
    pub ntools: u32,
}

/// Represents `PLATFORM_` mach-o constants.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Platform {
    MacOs,
    IOs,
    TvOs,
    WatchOs,
    BridgeOs,
    MacCatalyst,
    IosSimulator,
    TvOsSimulator,
    WatchOsSimulator,
    DriverKit,
    Unknown(u32),
}

impl std::fmt::Display for Platform {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::MacOs => f.write_str("macOS"),
            Self::IOs => f.write_str("iOS"),
            Self::TvOs => f.write_str("tvOS"),
            Self::WatchOs => f.write_str("watchOS"),
            Self::BridgeOs => f.write_str("bridgeOS"),
            Self::MacCatalyst => f.write_str("macCatalyst"),
            Self::IosSimulator => f.write_str("iOSSimulator"),
            Self::TvOsSimulator => f.write_str("tvOSSimulator"),
            Self::WatchOsSimulator => f.write_str("watchOSSimulator"),
            Self::DriverKit => f.write_str("driverKit"),
            Self::Unknown(v) => f.write_fmt(format_args!("Unknown ({v})")),
        }
    }
}

impl From<u32> for Platform {
    fn from(v: u32) -> Self {
        match v {
            1 => Self::MacOs,
            2 => Self::IOs,
            3 => Self::TvOs,
            4 => Self::WatchOs,
            5 => Self::BridgeOs,
            6 => Self::MacCatalyst,
            7 => Self::IosSimulator,
            8 => Self::TvOsSimulator,
            9 => Self::WatchOsSimulator,
            10 => Self::DriverKit,
            _ => Self::Unknown(v),
        }
    }
}

impl From<Platform> for u32 {
    fn from(val: Platform) -> Self {
        match val {
            Platform::MacOs => 1,
            Platform::IOs => 2,
            Platform::TvOs => 3,
            Platform::WatchOs => 4,
            Platform::BridgeOs => 5,
            Platform::MacCatalyst => 6,
            Platform::IosSimulator => 7,
            Platform::TvOsSimulator => 8,
            Platform::WatchOsSimulator => 9,
            Platform::DriverKit => 10,
            Platform::Unknown(v) => v,
        }
    }
}

impl Platform {
    /// Resolve SHA-256 digest/signatures support for a given platform type.
    pub fn sha256_digest_support(&self) -> Result<semver::VersionReq, AppleCodesignError> {
        let version = match self {
            // macOS 10.11.4 introduced support for SHA-256.
            Self::MacOs => ">=10.11.4",
            // 11.0+ support SHA-256.
            Self::IOs | Self::TvOs => ">=11.0.0",
            // WatchOS always uses SHA-1 it appears.
            Self::WatchOs => ">9999",
            // Assume no platform needs SHA-1.
            Self::Unknown(0) => ">9999",
            // Assume everything else is new and supports SHA-256.
            _ => "*",
        };

        Ok(semver::VersionReq::parse(version)?)
    }
}

/// Targeting settings for a Mach-O binary.
pub struct MachoTarget {
    /// The OS/platform being targeted.
    pub platform: Platform,
    /// Minimum required OS version.
    pub minimum_os_version: semver::Version,
    /// SDK version targeting.
    pub sdk_version: semver::Version,
}

impl MachoTarget {
    /// Convert the instance to a LC_BUILD_VERSION load command.
    pub fn to_build_version_command_vec(&self, endian: object::Endianness) -> Vec<u8> {
        let command = object::macho::BuildVersionCommand {
            cmd: object::U32::new(endian, object::macho::LC_BUILD_VERSION),
            cmdsize: object::U32::new(
                endian,
                std::mem::size_of::<object::macho::BuildVersionCommand<object::Endianness>>() as _,
            ),
            platform: object::U32::new(endian, self.platform.into()),
            minos: object::U32::new(
                endian,
                semver_to_macho_target_version(&self.minimum_os_version),
            ),
            sdk: object::U32::new(endian, semver_to_macho_target_version(&self.sdk_version)),
            ntools: object::U32::new(endian, 0),
        };

        object::bytes_of(&command).to_vec()
    }
}

/// Parses and integer with nibbles xxxx.yy.zz into a [semver::Version].
pub fn parse_version_nibbles(v: u32) -> semver::Version {
    let major = v >> 16;
    let minor = v << 16 >> 24;
    let patch = v & 0xff;

    semver::Version::new(major as _, minor as _, patch as _)
}

/// Convert a [semver::Version] to a u32 with nibble encoding used by Mach-O.
pub fn semver_to_macho_target_version(version: &semver::Version) -> u32 {
    let major = version.major as u32;
    let minor = version.minor as u32;
    let patch = version.patch as u32;

    (major << 16) | ((minor & 0xff) << 8) | (patch & 0xff)
}

/// Represents a semi-parsed Mach[-O] binary.
pub struct MachFile<'a> {
    #[allow(unused)]
    data: &'a [u8],

    machos: Vec<MachOBinary<'a>>,
}

impl<'a> MachFile<'a> {
    /// Construct an instance from data.
    pub fn parse(data: &'a [u8]) -> Result<Self, AppleCodesignError> {
        let mach = Mach::parse(data)?;

        let machos = match mach {
            Mach::Binary(macho) => vec![MachOBinary {
                index: None,
                macho,
                data,
            }],
            Mach::Fat(multiarch) => {
                let mut machos = vec![];

                for (index, arch) in multiarch.arches()?.into_iter().enumerate() {
                    let macho = match multiarch.get(index)? {
                        SingleArch::MachO(m) => m,
                        SingleArch::Archive(_) => continue,
                    };

                    machos.push(MachOBinary {
                        index: Some(index),
                        macho,
                        data: arch.slice(data),
                    });
                }

                machos
            }
        };

        Ok(Self { data, machos })
    }

    /// Whether this Mach-O data has multiple architectures.
    pub fn is_fat(&self) -> bool {
        self.machos.len() > 1
    }

    /// Iterate [MachO] instances in this data.
    ///
    /// The `Option<usize>` is `Some` if this is a universal Mach-O or `None` otherwise.
    pub fn iter_macho(&self) -> impl Iterator<Item = &MachOBinary<'_>> {
        self.machos.iter()
    }

    pub fn iter_macho_mut(&mut self) -> impl Iterator<Item = &mut MachOBinary<'a>> + '_ {
        self.machos.iter_mut()
    }

    pub fn nth_macho(&self, index: usize) -> Result<&MachOBinary<'a>, AppleCodesignError> {
        self.machos
            .get(index)
            .ok_or(AppleCodesignError::InvalidMachOIndex(index))
    }

    pub fn nth_macho_mut(
        &mut self,
        index: usize,
    ) -> Result<&mut MachOBinary<'a>, AppleCodesignError> {
        self.machos
            .get_mut(index)
            .ok_or(AppleCodesignError::InvalidMachOIndex(index))
    }
}

impl<'a> IntoIterator for MachFile<'a> {
    type Item = MachOBinary<'a>;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        self.machos.into_iter()
    }
}

#[cfg(test)]
mod tests {
    use {
        super::*,
        crate::embedded_signature::Blob,
        std::{
            io::Read,
            path::{Path, PathBuf},
        },
    };

    const MACHO_UNIVERSAL_MAGIC: [u8; 4] = [0xca, 0xfe, 0xba, 0xbe];
    const MACHO_64BIT_MAGIC: [u8; 4] = [0xfe, 0xed, 0xfa, 0xcf];

    /// Find files in a directory appearing to be Mach-O by sniffing magic.
    ///
    /// Ignores file I/O errors.
    fn find_likely_macho_files(path: &Path) -> Vec<PathBuf> {
        let mut res = Vec::new();

        let dir = std::fs::read_dir(path).unwrap();

        for entry in dir {
            let entry = entry.unwrap();

            if let Ok(mut fh) = std::fs::File::open(entry.path()) {
                let mut magic = [0; 4];

                if let Ok(size) = fh.read(&mut magic) {
                    if size == 4 && (magic == MACHO_UNIVERSAL_MAGIC || magic == MACHO_64BIT_MAGIC) {
                        res.push(entry.path());
                    }
                }
            }
        }

        res
    }

    fn find_apple_embedded_signature<'a>(macho: &'a MachOBinary) -> Option<EmbeddedSignature<'a>> {
        if let Ok(Some(signature)) = macho.code_signature() {
            Some(signature)
        } else {
            None
        }
    }

    fn validate_macho(path: &Path, macho: &MachOBinary) {
        // We found signature data in the binary.
        if let Some(signature) = find_apple_embedded_signature(macho) {
            // Attempt a deep parse of all blobs.
            for blob in &signature.blobs {
                match blob.clone().into_parsed_blob() {
                    Ok(parsed) => {
                        // Attempt to roundtrip the blob data.
                        match parsed.blob.to_blob_bytes() {
                            Ok(serialized) => {
                                if serialized != blob.data {
                                    println!("blob serialization roundtrip failure on {}: index {}, magic {:?}",
                                        path.display(),
                                        blob.index,
                                        blob.magic,
                                    );
                                }
                            }
                            Err(e) => {
                                println!(
                                    "blob serialization failure on {}; index {}, magic {:?}: {:?}",
                                    path.display(),
                                    blob.index,
                                    blob.magic,
                                    e
                                );
                            }
                        }
                    }
                    Err(e) => {
                        println!(
                            "blob parse failure on {}; index {}, magic {:?}: {:?}",
                            path.display(),
                            blob.index,
                            blob.magic,
                            e
                        );
                    }
                }
            }

            // Found a CMS signed data blob.
            if matches!(signature.signature_data(), Ok(Some(_))) {
                match signature.signed_data() {
                    Ok(Some(signed_data)) => {
                        for signer in signed_data.signers() {
                            if let Err(e) = signer.verify_signature_with_signed_data(&signed_data) {
                                println!(
                                    "signature verification failed for {}: {}",
                                    path.display(),
                                    e
                                );
                            }

                            if let Ok(()) =
                                signer.verify_message_digest_with_signed_data(&signed_data)
                            {
                                println!(
                                    "message digest verification unexpectedly correct for {}",
                                    path.display()
                                );
                            }
                        }
                    }
                    Ok(None) => {
                        // This has been observed to occur in the wild. But not from Apple
                        // signed binaries. Mostly ignore it.
                        eprintln!(
                            "{} has a signature blob without CMS data; weird",
                            path.display()
                        );
                    }
                    Err(e) => {
                        println!("error performing CMS parse of {}: {:?}", path.display(), e);
                    }
                }
            }
        }
    }

    fn validate_macho_in_dir(dir: &Path) {
        for path in find_likely_macho_files(dir).into_iter() {
            if let Ok(file_data) = std::fs::read(&path) {
                if let Ok(mach) = MachFile::parse(&file_data) {
                    for macho in mach.into_iter() {
                        validate_macho(&path, &macho);
                    }
                }
            }
        }
    }

    #[test]
    fn parse_applications_macho_signatures() {
        // This test scans common directories containing Mach-O files on macOS and
        // verifies we can parse CMS blobs within.

        if let Ok(dir) = std::fs::read_dir("/Applications") {
            for entry in dir {
                let entry = entry.unwrap();

                let search_dir = entry.path().join("Contents").join("MacOS");

                if search_dir.exists() {
                    validate_macho_in_dir(&search_dir);
                }
            }
        }

        for dir in &["/usr/bin", "/usr/local/bin", "/opt/homebrew/bin"] {
            let dir = PathBuf::from(dir);

            if dir.exists() {
                validate_macho_in_dir(&dir);
            }
        }
    }

    #[test]
    fn version_nibbles() {
        assert_eq!(
            parse_version_nibbles(12 << 16 | 1 << 8 | 2),
            semver::Version::new(12, 1, 2)
        );
        assert_eq!(
            parse_version_nibbles(11 << 16 | 10 << 8 | 15),
            semver::Version::new(11, 10, 15)
        );
        assert_eq!(
            semver_to_macho_target_version(&semver::Version::new(12, 1, 2)),
            12 << 16 | 1 << 8 | 2
        );
    }
}
