macho-core 0.5.2

Byte-safe structural Mach-O parsing and validation
Documentation
use macho_core::{ParseErrorKind, ParseLimits, ParseMode, ParseOptions};
use macho_test_support::{CPU_TYPE_ARM64, CPU_TYPE_X86_64};

fn forensic_with(mut limits: ParseLimits, bytes: &[u8]) -> Result<(), macho_core::ParseError> {
    limits.max_sections = limits.max_sections.max(1);
    macho_core::parse_with_options(
        bytes,
        &ParseOptions {
            mode: ParseMode::Forensic,
            limits,
        },
    )
    .map(|_| ())
}

fn parse_error(bytes: &[u8]) -> macho_core::ParseError {
    match macho_core::parse(bytes) {
        Ok(_) => panic!("fixture unexpectedly parsed"),
        Err(error) => error,
    }
}

#[test]
fn thin_and_multi_arch_fat_fixtures_parse() {
    let arm = macho_test_support::thin64_arm64(2);
    let x86 = macho_test_support::thin64_x86_64(2);
    macho_core::parse(&arm).expect("valid thin fixture");
    let fat = macho_test_support::fat32(&[(CPU_TYPE_ARM64, 0, arm), (CPU_TYPE_X86_64, 0, x86)]);
    let parsed = macho_core::parse(&fat).expect("valid multi-arch fixture");
    assert_eq!(parsed.macho_files().count(), 2);
}

#[test]
fn invalid_fat_fixture_matrix_is_typed() {
    for (name, bytes, kind) in [
        (
            "zero architecture",
            macho_test_support::zero_arch_fat(),
            ParseErrorKind::InvalidFormat,
        ),
        (
            "overlapping slices",
            macho_test_support::overlapping_fat_slices(),
            ParseErrorKind::InvalidFormat,
        ),
        (
            "truncated slice",
            macho_test_support::truncated_fat_slice(),
            ParseErrorKind::InvalidFormat,
        ),
    ] {
        let error = parse_error(&bytes);
        assert_eq!(error.kind, kind, "{name}");
    }
}

#[test]
fn fat_and_load_command_limits_fail_before_input_derived_allocation() {
    let fat = macho_test_support::fat32(&[
        (CPU_TYPE_ARM64, 0, macho_test_support::thin64_arm64(2)),
        (CPU_TYPE_X86_64, 0, macho_test_support::thin64_x86_64(2)),
    ]);
    let error = forensic_with(
        ParseLimits {
            max_fat_arches: 1,
            ..ParseLimits::default()
        },
        &fat,
    )
    .expect_err("fat architecture limit");
    assert_eq!(error.kind, ParseErrorKind::LimitExceeded);

    let command = macho_test_support::unknown_load_command_image();
    let error = forensic_with(
        ParseLimits {
            max_load_commands: 0,
            ..ParseLimits::default()
        },
        &command,
    )
    .expect_err("load-command limit");
    assert_eq!(error.kind, ParseErrorKind::LimitExceeded);
}

#[test]
fn known_unknown_truncated_and_impossible_load_commands_are_distinct() {
    let known = macho_test_support::thin64_x86_64_with_symbols(&[]);
    macho_core::parse(&known).expect("bounded known load commands");

    let unknown_bytes = macho_test_support::unknown_load_command_image();
    let unknown = macho_core::parse(&unknown_bytes).expect("bounded unknown command is preserved");
    assert_eq!(
        unknown
            .first_macho()
            .expect("thin image")
            .load_commands()
            .len(),
        1
    );

    for bytes in [
        macho_test_support::truncated_load_command_image(),
        macho_test_support::invalid_cmdsize_image(),
    ] {
        let error = parse_error(&bytes);
        assert_eq!(error.kind, ParseErrorKind::InvalidLoadCommand);
        assert!(matches!(
            error.context.last(),
            Some(macho_core::ContextFrame::LoadCommand { index: 0 })
        ));
    }
}

#[test]
fn invalid_fileset_entries_are_rejected() {
    macho_core::parse(&macho_test_support::fileset64_arm64()).expect("valid fileset");
    for bytes in [
        macho_test_support::fileset64_truncated_command(),
        macho_test_support::fileset64_out_of_bounds(),
    ] {
        assert!(macho_core::parse(&bytes).is_err());
    }
}