//! All of these tests are specific to the MacOS task dumper #![cfg(target_os = "macos")] use minidump_writer::{mach::LoadCommand, task_dumper::TaskDumper}; use std::fmt::Write; fn call_otool(args: &[&str]) -> String { let mut cmd = std::process::Command::new("otool"); cmd.args(args); let exe_path = std::env::current_exe().expect("unable to retrieve test executable path"); cmd.arg(exe_path); let output = cmd.output().expect("failed to spawn otool"); assert!(output.status.success()); String::from_utf8(output.stdout).expect("stdout was invalid utf-8") } /// Validates we can iterate the load commands for all of the images in the task #[test] fn iterates_load_commands() { let lc_str = call_otool(&["-l"]); let mut expected = String::new(); let mut lc_index = 0; expected.push('\n'); while let Some(nlc) = lc_str[lc_index..].find("Load command ") { lc_index += nlc; let block = match lc_str[lc_index + 13..].find("Load command ") { Some(ind) => &lc_str[lc_index + 13..lc_index + 13 + ind], None => &lc_str[lc_index..], }; // otool prints the load command index for each command, but we only // handle the small subset of the available load commands we care about // so just ignore that let block = &block[block.find('\n').unwrap() + 1..]; // otool also prints all the sections for LC_SEGMENT_* commands, but // we don't care about those, so ignore them let block = match block.find("Section") { Some(ind) => &block[..ind], None => block, }; lc_index += 13; let cmd = block .find("cmd ") .expect("load commnd didn't specify cmd kind"); let cmd_end = block[cmd..] .find('\n') .expect("load cmd didn't end with newline"); if matches!( &block[cmd + 4..cmd + cmd_end], "LC_SEGMENT_64" | "LC_UUID" | "LC_ID_DYLIB" | "LC_LOAD_DYLINKER" ) { expected.push_str(block); } } let task_dumper = TaskDumper::new( // SAFETY: syscall unsafe { mach2::traps::mach_task_self() }, ); let mut actual = String::new(); // Unfortunately, Apple decided to move dynamic libs into a shared cache, // removing them from the file system completely, and unless I'm missing it // there is no way to get the load commands for the dylibs since otool // only understands file paths? So we just get the load commands for the main // executable instead, this means that we miss the `LC_ID_DYLIB` commands // since they only apply to dylibs, but this test is more that we can // correctly iterate through the load commands themselves, so this _should_ // be fine... let exe_img = task_dumper .read_executable_image() .expect("failed to read executable image"); { let lcmds = task_dumper .read_load_commands(&exe_img) .expect("failed to read load commands"); for lc in lcmds.iter() { match lc { LoadCommand::Segment(seg) => { let segname = std::str::from_utf8(&seg.segment_name).unwrap(); let segname = &segname[..segname.find('\0').unwrap()]; write!( &mut actual, " cmd LC_SEGMENT_64 cmdsize {} segname {} vmaddr 0x{:016x} vmsize 0x{:016x} fileoff {} filesize {} maxprot 0x{:08x} initprot 0x{:08x} nsects {} flags 0x{:x}", seg.cmd_size, segname, seg.vm_addr, seg.vm_size, seg.file_off, seg.file_size, seg.max_prot, seg.init_prot, seg.num_sections, seg.flags, ) .unwrap(); } LoadCommand::Dylib(_dylib) => { unreachable!(); } LoadCommand::Uuid(uuid) => { let id = uuid::Uuid::from_bytes(uuid.uuid); let mut uuid_buf = [0u8; uuid::fmt::Hyphenated::LENGTH]; let uuid_str = id.hyphenated().encode_upper(&mut uuid_buf); write!( &mut actual, " cmd LC_UUID cmdsize {} uuid {uuid_str} ", uuid.cmd_size, ) .unwrap(); } LoadCommand::DylinkerCommand(dy_cmd) => { write!( &mut actual, " cmd LC_LOAD_DYLINKER cmdsize {} name {} (offset {})", dy_cmd.cmd_size, dy_cmd.name, dy_cmd.name_offset, ) .unwrap(); } } } } similar_asserts::assert_eq!(expected, actual); }