use std::collections::BTreeSet;
use std::path::Path;
use std::process::Command;
use qld::macho::read::{MachOFile, Source};
use super::{
base_args, clang_for, compile, host_can_run, ld64_lld, link_bytes, objdump, run, scratch, skip,
strings, symbols,
};
fn link(args: &[String], output: &Path) -> Vec<u8> {
let mut all: Vec<std::ffi::OsString> = args.iter().map(Into::into).collect();
all.push("-o".into());
all.push(output.into());
let (bytes, _) = link_bytes(&all).unwrap_or_else(|e| panic!("link failed: {e}"));
std::fs::write(output, &bytes).unwrap();
super::make_executable(output);
bytes
}
fn link_lld(lld: &Path, args: &[String], output: &Path) {
let result = Command::new(lld)
.args(args)
.arg("-o")
.arg(output)
.output()
.unwrap();
assert!(
result.status.success(),
"ld64.lld: {}",
String::from_utf8_lossy(&result.stderr)
);
}
fn metadata(file: &Path) -> Option<Vec<(String, BTreeSet<String>)>> {
let text = objdump(&["--macho", "--objc-meta-data"], file)?;
let mut sections: Vec<(String, BTreeSet<String>)> = Vec::new();
let mut entry = String::new();
let flush = |entry: &mut String, sections: &mut Vec<(String, BTreeSet<String>)>| {
if let Some((_, set)) = sections.last_mut()
&& !entry.is_empty()
{
set.insert(std::mem::take(entry));
}
entry.clear();
};
for line in text.lines().skip(1) {
let line: String = line
.split_whitespace()
.filter(|w| !w.starts_with("0x") && !w.starts_with("(0x"))
.collect::<Vec<_>>()
.join(" ");
if line.starts_with("Contents of") {
flush(&mut entry, &mut sections);
sections.push((line, BTreeSet::new()));
} else if line.len() >= 16 && line.bytes().take(16).all(|b| b.is_ascii_hexdigit()) {
flush(&mut entry, &mut sections);
entry.push_str("entry\n");
} else if sections
.last()
.is_some_and(|(name, _)| name.contains("refs)"))
{
flush(&mut entry, &mut sections);
entry.push_str(&line);
flush(&mut entry, &mut sections);
} else {
entry.push_str(&line);
entry.push('\n');
}
}
flush(&mut entry, &mut sections);
Some(sections)
}
fn same_metadata(
ours: &[(String, BTreeSet<String>)],
theirs: &[(String, BTreeSet<String>)],
what: &str,
) {
let names =
|m: &[(String, BTreeSet<String>)]| m.iter().map(|s| s.0.clone()).collect::<Vec<_>>();
assert_eq!(names(ours), names(theirs), "{what}: sections");
for ((name, left), (_, right)) in ours.iter().zip(theirs) {
if left != right {
let only_left: Vec<&String> = left.difference(right).collect();
let only_right: Vec<&String> = right.difference(left).collect();
panic!(
"{what}: {name} differs\n--- only in the first:\n{}\n--- only in the second:\n{}",
only_left
.iter()
.map(|e| e.as_str())
.collect::<Vec<_>>()
.join("\n"),
only_right
.iter()
.map(|e| e.as_str())
.collect::<Vec<_>>()
.join("\n"),
);
}
}
}
fn pointer_at(data: &[u8], sections: &[(u64, u64, u64)], address: u64) -> Option<u64> {
let (addr, _, offset) = sections
.iter()
.find(|&&(a, size, _)| address >= a && address < a + size)?;
let at = (offset + (address - addr)) as usize;
Some(u64::from_le_bytes(data.get(at..at + 8)?.try_into().ok()?))
}
fn string_at(data: &[u8], sections: &[(u64, u64, u64)], address: u64) -> Option<String> {
let (addr, _, offset) = sections
.iter()
.find(|&&(a, size, _)| address >= a && address < a + size)?;
let at = (offset + (address - addr)) as usize;
let rest = data.get(at..)?;
let end = rest.iter().position(|&b| b == 0)?;
Some(String::from_utf8_lossy(&rest[..end]).into_owned())
}
fn relative_methods(data: &[u8]) -> BTreeSet<String> {
let file = MachOFile::parse(data, Source::new(Path::new("out"))).unwrap();
let mut sections = Vec::new();
let mut methlist = None;
for command in file.load_commands() {
if let Ok(segment) = command.unwrap().segment() {
for section in segment.sections.iter() {
sections.push((section.addr, section.size, u64::from(section.offset)));
if section.sectname == b"__objc_methlist" {
assert_eq!(section.segname, b"__TEXT");
methlist = Some((section.addr, section.size, u64::from(section.offset)));
}
}
}
}
let (addr, size, offset) = methlist.expect("__objc_methlist");
let functions: Vec<(u64, String)> = symbols(data)
.into_iter()
.filter(|s| s.is_defined())
.map(|s| (s.n_value, s.name))
.collect();
let read32 = |at: u64| {
let at = at as usize;
u32::from_le_bytes(data[at..at + 4].try_into().unwrap())
};
let mut out = BTreeSet::new();
let mut at = 0u64;
while at < size {
at = at.next_multiple_of(4);
if at >= size {
break;
}
let header = read32(offset + at);
let count = read32(offset + at + 4);
assert_eq!(header & 0x8000_ffff, 0x8000_000c, "relative, entsize 12");
for index in 0..u64::from(count) {
let entry = at + 8 + index * 12;
let field = |n: u64| {
let place = addr + entry + n * 4;
place.wrapping_add(read32(offset + entry + n * 4) as i32 as i64 as u64)
};
let selref = field(0);
let name_address = pointer_at(data, §ions, selref).expect("selector reference");
let selector = string_at(data, §ions, name_address).expect("selector");
let types = string_at(data, §ions, field(1)).expect("types");
let imp = field(2);
let function = functions
.iter()
.find(|(address, name)| *address == imp && name.contains('['))
.map_or_else(|| format!("{imp:#x}"), |(_, name)| name.clone());
out.insert(format!("{selector} {types} {function}"));
}
at += 8 + u64::from(count) * 12;
}
out
}
#[test]
fn relative_method_lists_and_category_merging() {
let mut arm64_metadata = std::collections::HashMap::new();
for arch in ["arm64", "x86_64"] {
if !clang_for(arch) {
skip(
"relative_method_lists_and_category_merging",
&format!("clang cannot target {arch}-apple-macos"),
);
continue;
}
let object = compile("objc_categories", "objc_categories.m", arch, &[]);
let dir = scratch("objc_categories");
for (variant, extra) in [
("relative", &[][..]),
("merged", &["-objc_category_merging"][..]),
("absolute", &["-no_objc_relative_method_lists"][..]),
(
"absolute-merged",
&["-no_objc_relative_method_lists", "-objc_category_merging"][..],
),
] {
let mut args = base_args(arch);
args.extend(strings(extra));
args.extend(strings(&[object.to_str().unwrap(), "-lobjc", "-lSystem"]));
let exe = dir.join(format!("{variant}-{arch}"));
link(&args, &exe);
if host_can_run(arch) {
assert_eq!(run(&exe).unwrap(), "categories 305\n", "{arch} {variant}");
}
args.push("-no_fixup_chains".into());
let legacy = dir.join(format!("{variant}-{arch}-legacy"));
let bytes = link(&args, &legacy);
let relative = !variant.starts_with("absolute");
let names = super::section_names(&bytes);
assert_eq!(
names.iter().any(|n| n == "__objc_methlist"),
relative,
"{arch} {variant}: {names:?}"
);
if relative {
let methods = relative_methods(&bytes);
for wanted in [
"base i16@0:8 -[Base(Doubling) base]",
"base i16@0:8 -[Base base]",
"factor i16@0:8 +[Base(Doubling) factor]",
"second i16@0:8 +[NSObject(Second) second]",
"load v16@0:8 +[Base(Loading) load]",
] {
assert!(
methods.contains(wanted),
"{arch} {variant}: {wanted} in {methods:#?}"
);
}
}
let ours = metadata(&legacy);
if arch == "arm64" {
arm64_metadata.insert(variant, ours.clone());
} else if let (Some(ours), Some(Some(arm64))) = (&ours, arm64_metadata.get(variant)) {
same_metadata(ours, arm64, &format!("{variant}: x86_64 vs arm64"));
}
if variant.ends_with("merged") {
let catlist = super::section_contents(&bytes, "__objc_catlist").unwrap();
assert_eq!(catlist.len(), 16, "{arch} {variant}");
}
let Some(lld) = ld64_lld() else {
continue;
};
let reference = dir.join(format!("{variant}-{arch}-lld"));
link_lld(&lld, &args, &reference);
if relative {
let reference_bytes = std::fs::read(&reference).unwrap();
assert_eq!(
relative_methods(&bytes),
relative_methods(&reference_bytes),
"{arch} {variant}: qld vs ld64.lld"
);
}
if arch == "x86_64" && variant.ends_with("merged") {
continue;
}
if let (Some(ours), Some(theirs)) = (ours, metadata(&reference)) {
same_metadata(
&ours,
&theirs,
&format!("{arch} {variant}: qld vs ld64.lld"),
);
}
}
}
}