use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use qld::coff::read::consts::{
self, IMAGE_COMDAT_SELECT_ANY, IMAGE_COMDAT_SELECT_ASSOCIATIVE, IMAGE_FILE_MACHINE_AMD64,
IMAGE_FILE_MACHINE_ARM64, IMAGE_FILE_MACHINE_I386, IMAGE_SUBSYSTEM_WINDOWS_GUI,
IMAGE_SYM_CLASS_FILE, IMAGE_WEAK_EXTERN_SEARCH_ALIAS, IMPORT_NAME_EXPORTAS,
};
use qld::coff::read::{
AuxFunctionDefinition, AuxSectionDefinition, AuxWeakExternal, CoffFile, CoffObject, Directive,
ExportTarget, ImportName, LongImportDlls, LongImportMember, PeImage, ShortImport, Source,
Symbol, classify_long_import, parse_module_definition,
};
use qld::input::archive::Archive;
use qld::target::Architecture;
fn data_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/coff_read")
}
fn fixture(name: &str) -> (PathBuf, Vec<u8>) {
let path = data_dir().join(name);
let data = std::fs::read(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
(path, data)
}
fn scratch_dir(test: &str) -> PathBuf {
let dir = Path::new(env!("CARGO_TARGET_TMPDIR"))
.join("coff_read")
.join(test);
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn skip(reason: &str) {
if std::env::var_os("QLD_REQUIRE_COFF_TOOLS").is_some_and(|v| !v.is_empty() && v != "0") {
panic!("required tool unavailable: {reason}");
}
println!("SKIPPED: {reason}");
}
fn run(tool: &str, args: &[&str]) -> Option<String> {
match Command::new(tool).args(args).output() {
Ok(output) if output.status.success() => {
Some(String::from_utf8_lossy(&output.stdout).into_owned())
}
Ok(output) => {
skip(&format!(
"`{tool} {}` failed: {}",
args.join(" "),
String::from_utf8_lossy(&output.stderr).trim()
));
None
}
Err(error) => {
skip(&format!("{tool} unavailable: {error}"));
None
}
}
}
fn build(tool: &str, args: &[&str], out: &Path) -> Option<Vec<u8>> {
run(tool, args)?;
match std::fs::read(out) {
Ok(data) => Some(data),
Err(error) => {
skip(&format!("{tool} did not write {}: {error}", out.display()));
None
}
}
}
fn path_str(path: &Path) -> &str {
path.to_str().expect("UTF-8 path")
}
fn text(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).into_owned()
}
fn source(path: &Path) -> Source<'_> {
Source::new(path)
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Sec {
number: u32,
name: String,
virtual_size: u64,
virtual_address: u64,
raw_size: u64,
pointer_to_raw_data: u64,
pointer_to_relocations: u64,
relocation_count: u64,
characteristics: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Rel {
section: u32,
offset: u64,
kind: String,
symbol: String,
index: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Sym {
name: String,
value: u64,
section: i64,
base_type: u64,
complex_type: u64,
storage_class: u64,
aux_count: u64,
aux: Vec<(String, Vec<(String, String)>)>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
struct Inventory {
header: Vec<(String, String)>,
sections: Vec<Sec>,
relocations: Vec<Rel>,
symbols: Vec<Sym>,
}
fn pairs(items: &[(&str, String)]) -> Vec<(String, String)> {
items
.iter()
.map(|(k, v)| ((*k).to_owned(), v.clone()))
.collect()
}
fn aux_records(symbol: &Symbol<'_>) -> Vec<(String, Vec<(String, String)>)> {
let mut records = Vec::new();
for i in 0..symbol.number_of_aux_symbols {
let raw = symbol.aux_record(i).expect("aux record");
if symbol.is_function_definition() {
let def = AuxFunctionDefinition::decode(raw).unwrap();
records.push((
"AuxFunctionDef".to_owned(),
pairs(&[
("TagIndex", def.tag_index.to_string()),
("TotalSize", def.total_size.to_string()),
("PointerToLineNumber", def.pointer_to_linenumber.to_string()),
(
"PointerToNextFunction",
def.pointer_to_next_function.to_string(),
),
]),
));
} else if symbol.is_undefined() || symbol.is_weak_external() {
let weak = AuxWeakExternal::decode(raw).unwrap();
records.push((
"AuxWeakExternal".to_owned(),
pairs(&[
("Linked", weak.tag_index.to_string()),
("Search", weak.characteristics.to_string()),
]),
));
} else if symbol.is_file() {
records.push((
"AuxFileRecord".to_owned(),
pairs(&[("FileName", text(symbol.file_name().unwrap()))]),
));
break;
} else if symbol.is_section_definition() {
let def = AuxSectionDefinition::decode(raw, symbol.bigobj).unwrap();
let mut fields = pairs(&[
("Length", def.length.to_string()),
("RelocationCount", def.number_of_relocations.to_string()),
("LineNumberCount", def.number_of_linenumbers.to_string()),
("Checksum", def.check_sum.to_string()),
("Number", def.number.to_string()),
("Selection", def.selection.to_string()),
]);
if let Some(section) = def.associative_section() {
fields.push(("AssocSection".to_owned(), section.to_string()));
}
records.push(("AuxSectionDef".to_owned(), fields));
}
}
records
}
fn object_inventory(object: &CoffObject<'_>) -> Inventory {
let header = object.header();
let mut inventory = Inventory {
header: pairs(&[
("Machine", u64::from(header.machine).to_string()),
("SectionCount", header.number_of_sections.to_string()),
(
"PointerToSymbolTable",
header.pointer_to_symbol_table.to_string(),
),
("SymbolCount", header.number_of_symbols.to_string()),
(
"StringTableSize",
object.symbols().strings().len().to_string(),
),
(
"OptionalHeaderSize",
header.size_of_optional_header.to_string(),
),
("Characteristics", header.characteristics.to_string()),
]),
..Inventory::default()
};
for section in object.sections() {
let section = section.unwrap();
let h = section.header;
inventory.sections.push(Sec {
number: section.number,
name: text(section.name),
virtual_size: h.virtual_size.into(),
virtual_address: h.virtual_address.into(),
raw_size: h.size_of_raw_data.into(),
pointer_to_raw_data: h.pointer_to_raw_data.into(),
pointer_to_relocations: h.pointer_to_relocations.into(),
relocation_count: h.number_of_relocations.into(),
characteristics: h.characteristics.into(),
});
object.section_data(&h).unwrap();
for reloc in object.relocations(&h).unwrap().iter() {
let symbol = object.symbol(reloc.symbol_table_index).unwrap();
inventory.relocations.push(Rel {
section: section.number,
offset: reloc.virtual_address.into(),
kind: consts::relocation_name(object.machine(), reloc.r_type)
.expect("known relocation type")
.to_owned(),
symbol: text(symbol.name),
index: reloc.symbol_table_index.into(),
});
}
}
for symbol in object.symbols().iter() {
let symbol = symbol.unwrap();
inventory.symbols.push(Sym {
name: text(symbol.name),
value: symbol.value.into(),
section: symbol.section_number.into(),
base_type: symbol.base_type().into(),
complex_type: symbol.complex_type().into(),
storage_class: symbol.storage_class.into(),
aux_count: symbol.number_of_aux_symbols.into(),
aux: aux_records(&symbol),
});
}
inventory
}
#[derive(Debug, Default)]
struct Node {
head: String,
children: Vec<Node>,
}
impl Node {
fn value(&self, key: &str) -> Option<&str> {
self.children.iter().find_map(|c| {
let rest = c.head.strip_prefix(key)?.strip_prefix(':')?;
Some(rest.strip_prefix(' ').unwrap_or(rest))
})
}
fn get(&self, key: &str) -> &str {
self.value(key)
.unwrap_or_else(|| panic!("no `{key}` under `{}`", self.head))
}
fn scopes<'s>(&'s self, name: &'s str) -> impl Iterator<Item = &'s Node> + 's {
self.children
.iter()
.filter(move |c| c.head == name || c.head.starts_with(&format!("{name} ")))
}
fn scope<'s>(&'s self, name: &'s str) -> Option<&'s Node> {
self.scopes(name).next()
}
}
fn parse_tree(output: &str) -> Node {
let mut stack = vec![Node::default()];
for line in output.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if line == "}" || line == "]" {
let node = stack.pop().expect("balanced output");
stack
.last_mut()
.expect("balanced output")
.children
.push(node);
} else if let Some(head) = line.strip_suffix(" {").or_else(|| line.strip_suffix(" [")) {
stack.push(Node {
head: head.to_owned(),
children: Vec::new(),
});
} else if let Some((head, flags)) = line.split_once(" [ (") {
stack.push(Node {
head: format!("{head}: {}", flags.trim_end_matches(')')),
children: Vec::new(),
});
} else {
stack.last_mut().unwrap().children.push(Node {
head: line.to_owned(),
children: Vec::new(),
});
}
}
assert_eq!(stack.len(), 1, "unbalanced readobj output");
stack.pop().unwrap()
}
fn normalize(value: &str) -> String {
let number = value
.strip_suffix(')')
.and_then(|v| v.rsplit_once('(').map(|(_, n)| n))
.unwrap_or(value);
let parsed = if let Some(hex) = number.strip_prefix("0x") {
u64::from_str_radix(hex, 16).ok().map(|n| n.to_string())
} else if let Some(hex) = number.strip_prefix("-0x") {
i64::from_str_radix(hex, 16).ok().map(|n| (-n).to_string())
} else {
number.parse::<i64>().ok().map(|n| n.to_string())
};
parsed.unwrap_or_else(|| value.to_owned())
}
fn number(value: &str) -> i64 {
normalize(value)
.parse()
.unwrap_or_else(|_| panic!("not a number: {value:?}"))
}
fn unumber(value: &str) -> u64 {
u64::try_from(number(value)).unwrap()
}
fn flags(node: &Node) -> u64 {
let head = node
.children
.iter()
.find(|c| c.head.starts_with("Characteristics: "))
.expect("Characteristics");
unumber(head.head.trim_start_matches("Characteristics: "))
}
fn readobj_inventory(root: &Node) -> Inventory {
let mut inventory = Inventory::default();
if let Some(header) = root.scope("ImageFileHeader") {
let machine = header.get("Machine");
let mut fields = vec![("Machine", unumber(machine).to_string())];
for key in [
"SectionCount",
"PointerToSymbolTable",
"SymbolCount",
"StringTableSize",
"OptionalHeaderSize",
] {
fields.push((key, unumber(header.get(key)).to_string()));
}
fields.push(("Characteristics", flags(header).to_string()));
inventory.header = pairs(&fields);
}
if let Some(sections) = root.scope("Sections") {
for section in sections.scopes("Section") {
let name = section.get("Name");
let name = name.rsplit_once(" (").map_or(name, |(n, _)| n);
inventory.sections.push(Sec {
number: u32::try_from(unumber(section.get("Number"))).unwrap(),
name: name.to_owned(),
virtual_size: unumber(section.get("VirtualSize")),
virtual_address: unumber(section.get("VirtualAddress")),
raw_size: unumber(section.get("RawDataSize")),
pointer_to_raw_data: unumber(section.get("PointerToRawData")),
pointer_to_relocations: unumber(section.get("PointerToRelocations")),
relocation_count: unumber(section.get("RelocationCount")),
characteristics: flags(section),
});
}
}
if let Some(relocations) = root.scope("Relocations") {
for section in relocations.scopes("Section") {
let number = section
.head
.strip_prefix("Section (")
.and_then(|h| h.split_once(')'))
.map(|(n, _)| n.parse().unwrap())
.unwrap();
for line in §ion.children {
let (body, index) = line.head.rsplit_once(" (").unwrap();
let mut parts = body.splitn(3, ' ');
inventory.relocations.push(Rel {
section: number,
offset: unumber(parts.next().unwrap()),
kind: parts.next().unwrap().to_owned(),
symbol: parts.next().unwrap_or("").to_owned(),
index: unumber(index.trim_end_matches(')')),
});
}
}
}
if let Some(symbols) = root.scope("Symbols") {
for symbol in symbols.scopes("Symbol") {
let aux = symbol
.children
.iter()
.filter(|c| c.head.starts_with("Aux") && !c.head.contains(':'))
.map(|record| {
let fields = record
.children
.iter()
.filter_map(|leaf| leaf.head.split_once(": "))
.map(|(k, v)| {
let v = if k == "FileName" {
v.to_owned()
} else {
normalize(v)
};
(k.to_owned(), v)
})
.collect();
(record.head.clone(), fields)
})
.collect();
inventory.symbols.push(Sym {
name: symbol.get("Name").to_owned(),
value: unumber(symbol.get("Value")),
section: number(symbol.get("Section")),
base_type: unumber(symbol.get("BaseType")),
complex_type: unumber(symbol.get("ComplexType")),
storage_class: unumber(symbol.get("StorageClass")),
aux_count: unumber(symbol.get("AuxSymbolCount")),
aux,
});
}
}
inventory
}
fn readobj(args: &[&str], path: &Path) -> Option<String> {
let mut all = args.to_vec();
all.push(path_str(path));
run("llvm-readobj", &all)
}
fn assert_same<T: std::fmt::Debug + PartialEq>(what: &str, path: &Path, ours: &[T], theirs: &[T]) {
for (i, (a, b)) in ours.iter().zip(theirs).enumerate() {
assert_eq!(a, b, "{}: {what} #{i} differs", path.display());
}
assert_eq!(
ours.len(),
theirs.len(),
"{}: {what} count differs",
path.display()
);
}
fn compare_object_with_readobj(path: &Path, object: &CoffObject<'_>) -> bool {
let Some(output) = readobj(
&["--file-headers", "--sections", "--relocations", "--symbols"],
path,
) else {
return false;
};
let theirs = readobj_inventory(&parse_tree(&output));
let ours = object_inventory(object);
assert_same("header field", path, &ours.header, &theirs.header);
assert_same("section", path, &ours.sections, &theirs.sections);
assert_same("relocation", path, &ours.relocations, &theirs.relocations);
assert_same("symbol", path, &ours.symbols, &theirs.symbols);
if let Some(output) = readobj(&["--coff-directives"], path) {
let raw: Vec<u8> = object
.sections_named(b".drectve")
.map(|s| {
let s = s.unwrap();
object.section_data(&s.header).unwrap().to_vec()
})
.next()
.unwrap_or_default();
let printed = output
.lines()
.find_map(|l| l.strip_prefix("Directive(s): "))
.unwrap_or("");
assert_eq!(
text(&raw).trim_end_matches('\0'),
printed.trim_end_matches('\0'),
"{}: directives",
path.display()
);
}
true
}
fn parse_object<'a>(path: &'a Path, data: &'a [u8]) -> CoffObject<'a> {
CoffObject::parse(data, source(path)).unwrap_or_else(|e| panic!("{e}"))
}
fn symbol<'a>(object: &CoffObject<'a>, name: &str) -> Symbol<'a> {
object
.symbols()
.iter()
.map(Result::unwrap)
.find(|s| s.name == name.as_bytes())
.unwrap_or_else(|| panic!("no symbol {name}"))
}
fn directives(object: &CoffObject<'_>) -> Vec<Directive<'static>> {
let mut all = Vec::new();
for section in object.directives() {
for directive in section.unwrap() {
all.push(into_owned(directive.unwrap()));
}
}
all
}
fn owned(cow: std::borrow::Cow<'_, [u8]>) -> std::borrow::Cow<'static, [u8]> {
std::borrow::Cow::Owned(cow.into_owned())
}
fn into_owned(directive: Directive<'_>) -> Directive<'static> {
match directive {
Directive::Export(spec) => Directive::Export(qld::coff::read::ExportSpec {
name: owned(spec.name),
internal_name: spec.internal_name.map(owned),
import_name: spec.import_name.map(owned),
export_as: spec.export_as.map(owned),
ordinal: spec.ordinal,
noname: spec.noname,
data: spec.data,
private: spec.private,
constant: spec.constant,
}),
Directive::Include(name) => Directive::Include(owned(name)),
Directive::DefaultLib(name) => Directive::DefaultLib(owned(name)),
Directive::AlternateName { alias, target } => Directive::AlternateName {
alias: owned(alias),
target: owned(target),
},
Directive::AlignComm {
symbol,
alignment_log2,
} => Directive::AlignComm {
symbol: owned(symbol),
alignment_log2,
},
other => Directive::Other {
name: std::borrow::Cow::Owned(format!("{other:?}").into_bytes()),
value: None,
},
}
}
fn export(name: &str, data: bool) -> Directive<'static> {
Directive::Export(qld::coff::read::ExportSpec {
name: std::borrow::Cow::Owned(name.as_bytes().to_vec()),
data,
..Default::default()
})
}
fn check_mingw_object(object: &CoffObject<'_>) {
assert_eq!(object.machine(), IMAGE_FILE_MACHINE_AMD64);
assert_eq!(object.architecture(), Some(Architecture::X86_64));
let file = object
.symbols()
.iter()
.map(Result::unwrap)
.find(|s| s.storage_class == IMAGE_SYM_CLASS_FILE)
.expect(".file symbol");
assert_eq!(file.file_name(), Some(&b"mingw.c"[..]));
assert!(file.is_debug());
let common = symbol(object, "common_symbol");
assert!(common.is_common() && !common.is_undefined());
assert_eq!(common.value, 4);
let weak_ref = symbol(object, "weak_reference");
assert!(weak_ref.is_weak_external());
let aux = weak_ref.weak_external().expect("weak external aux");
let default = object.symbol(aux.tag_index).unwrap();
assert!(
default.name.starts_with(b".weak.weak_reference."),
"{default:?}"
);
let weak_def = symbol(object, "weak_definition");
assert!(weak_def.is_weak_external());
let local = symbol(object, "local_helper");
assert_eq!(local.storage_class, consts::IMAGE_SYM_CLASS_STATIC);
let long = object
.sections()
.map(Result::unwrap)
.find(|s| s.name == b".custom_long_section_name")
.expect("long section name");
assert!(long.header.has_long_name());
let refptr = object
.sections()
.map(Result::unwrap)
.find(|s| s.name.starts_with(b".rdata$.refptr."))
.expect("refptr COMDAT");
assert!(refptr.header.is_comdat());
assert!(refptr.header.alignment().is_some());
let def = symbol(object, std::str::from_utf8(refptr.name).unwrap())
.section_definition()
.unwrap();
assert_eq!(def.selection, IMAGE_COMDAT_SELECT_ANY);
let text_section = object.section(1).unwrap();
assert_eq!(text_section.name, b".text");
assert!(text_section.header.is_code());
assert_eq!(text_section.header.alignment(), Some(16));
assert!(!object.relocations(&text_section.header).unwrap().is_empty());
assert!(
object
.symbols()
.iter()
.any(|s| s.unwrap().name == b"__emutls_v.thread_local_value")
);
let found = directives(object);
assert!(
found.contains(&Directive::AlignComm {
symbol: b"aligned_common".to_vec().into(),
alignment_log2: 5
}),
"{found:?}"
);
assert!(
found.contains(&export("exported_function", false)),
"{found:?}"
);
assert!(found.contains(&export("exported_data", true)), "{found:?}");
assert!(object.feat00().unwrap().is_none());
assert!(object.addrsig().unwrap().is_none());
}
fn check_msvc_object(object: &CoffObject<'_>, machine: u16) {
assert_eq!(object.machine(), machine);
assert!(!object.is_bigobj());
let x86 = machine == IMAGE_FILE_MACHINE_I386;
let mangle = |name: &str| name.to_owned();
let feat = object.feat00().unwrap().expect("@feat.00");
assert!(feat.guard_cf());
assert_eq!(feat.safe_seh(), x86);
let mut comdat_leaders = 0;
let mut associative = 0;
for s in object.symbols().iter().map(Result::unwrap) {
if let Some(def) = s.section_definition() {
let section = object
.section(u32::try_from(s.section_number).unwrap())
.unwrap();
if def.selection == IMAGE_COMDAT_SELECT_ANY && section.header.is_comdat() {
comdat_leaders += 1;
}
if def.selection == IMAGE_COMDAT_SELECT_ASSOCIATIVE {
associative += 1;
let target = object.section(def.associative_section().unwrap()).unwrap();
assert!(target.header.is_comdat(), "{}", text(target.name));
}
}
}
assert!(comdat_leaders >= 3, "COMDAT sections: {comdat_leaders}");
assert_eq!(associative > 0, !x86, "associative sections: {associative}");
let weak = symbol(object, "?weak_function@@YAHXZ");
assert!(weak.is_weak_external());
assert_eq!(
weak.weak_external().unwrap().characteristics,
IMAGE_WEAK_EXTERN_SEARCH_ALIAS
);
let tls = object
.sections()
.map(Result::unwrap)
.find(|s| s.is_tls())
.expect(".tls$ section");
assert_eq!(tls.name, b".tls$");
assert!(symbol(object, "?thread_value@@3HA").section_number > 0);
let drectve = object.sections_named(b".drectve").next().unwrap().unwrap();
assert!(drectve.header.is_info() && drectve.header.is_remove());
let found = directives(object);
let expected = [
Directive::DefaultLib(b"fixturelib.lib".to_vec().into()),
Directive::AlternateName {
alias: mangle("alias_symbol").into_bytes().into(),
target: mangle("real_symbol").into_bytes().into(),
},
Directive::Include(mangle("forced_symbol").into_bytes().into()),
export("?exported_function@@YAHH@Z", false),
export("?exported_data@@3HA", true),
];
assert_eq!(found, expected);
let addrsig: Vec<u32> = object
.addrsig()
.unwrap()
.expect(".llvm_addrsig")
.map(Result::unwrap)
.collect();
let names: Vec<_> = addrsig
.iter()
.map(|&i| text(object.symbol(i).unwrap().name))
.collect();
assert!(
names.contains(&"?weak_function@@YAHXZ".to_owned())
|| names.iter().any(|n| n.contains("weak_function")),
"{names:?}"
);
}
#[test]
fn mingw_object_fixture() {
let (path, data) = fixture("mingw-x86_64.o");
let object = parse_object(&path, &data);
assert!(!object.is_bigobj());
check_mingw_object(&object);
compare_object_with_readobj(&path, &object);
}
#[test]
fn bigobj_fixture() {
let (path, data) = fixture("mingw-bigobj.o");
let object = parse_object(&path, &data);
assert!(object.is_bigobj());
assert_eq!(object.symbols().record_size(), 20);
check_mingw_object(&object);
assert!(matches!(
CoffFile::parse(&data, source(&path)),
Ok(CoffFile::Object(_))
));
compare_object_with_readobj(&path, &object);
let (regular_path, regular_data) = fixture("mingw-x86_64.o");
let regular = parse_object(®ular_path, ®ular_data);
let names = |o: &CoffObject<'_>| -> Vec<Vec<u8>> {
o.symbols()
.iter()
.map(|s| s.unwrap().name.to_vec())
.collect()
};
assert_eq!(names(&object), names(®ular));
}
#[test]
fn msvc_object_fixtures() {
for (name, machine) in [
("msvc-x86_64.obj", IMAGE_FILE_MACHINE_AMD64),
("msvc-i686.obj", IMAGE_FILE_MACHINE_I386),
("msvc-aarch64.obj", IMAGE_FILE_MACHINE_ARM64),
] {
let (path, data) = fixture(name);
let object = parse_object(&path, &data);
check_msvc_object(&object, machine);
compare_object_with_readobj(&path, &object);
}
}
#[test]
fn resource_fixtures() {
for (name, expected) in [
("resource-windres.o", &[&b".rsrc"[..]][..]),
("resource-cvtres.obj", &[&b".rsrc$01"[..], b".rsrc$02"][..]),
] {
let (path, data) = fixture(name);
let object = parse_object(&path, &data);
let names: Vec<_> = object
.resource_sections()
.map(|s| s.unwrap().name)
.collect();
assert_eq!(names, expected, "{name}");
for section in object.resource_sections() {
let section = section.unwrap();
assert!(!object.section_data(§ion.header).unwrap().is_empty());
}
compare_object_with_readobj(&path, &object);
}
}
fn expected_short_imports(i386: bool) -> Vec<(String, u8, ImportName<'static>)> {
let prefix = if i386 { "_" } else { "" };
let name = |n: &'static str| ImportName::Name {
hint: 0,
name: n.as_bytes(),
};
vec![
(format!("{prefix}plain_function"), 0, name("plain_function")),
(format!("{prefix}plain_data"), 1, name("plain_data")),
(format!("{prefix}by_ordinal"), 0, ImportName::Ordinal(7)),
(format!("{prefix}public_name"), 0, name("public_name")),
(format!("{prefix}constant_value"), 2, name("constant_value")),
(format!("{prefix}hinted"), 0, name("hinted")),
(format!("{prefix}exported_as"), 0, name("real_export")),
]
}
fn short_imports<'a>(path: &'a Path, data: &'a [u8]) -> Vec<ShortImport<'a>> {
let archive = Archive::parse(path, data).unwrap();
archive
.members()
.map(Result::unwrap)
.filter_map(|member| {
let bytes = member.bytes().unwrap();
match CoffFile::parse(bytes, source(path)).unwrap() {
CoffFile::ShortImport(import) => Some(import),
CoffFile::Object(_) => None,
CoffFile::Image(_) => panic!("image in an import library"),
}
})
.collect()
}
#[test]
fn short_import_fixtures() {
for (name, i386) in [
("short-import-x86_64.lib", false),
("short-import-i386.lib", true),
] {
let (path, data) = fixture(name);
let imports = short_imports(&path, &data);
let expected = expected_short_imports(i386);
assert_eq!(imports.len(), expected.len(), "{name}");
for (import, (symbol, kind, import_name)) in imports.iter().zip(&expected) {
assert_eq!(text(import.symbol_name), *symbol);
assert_eq!(import.dll_name, b"testlib.dll");
assert_eq!(import.import_type, *kind, "{symbol}");
assert_eq!(import.defines_symbol_name(), *kind != 1);
let expected_arch = if i386 {
Architecture::X86
} else {
Architecture::X86_64
};
assert_eq!(import.architecture(), Some(expected_arch));
match (import.import_name(), import_name) {
(ImportName::Name { name, .. }, ImportName::Name { name: want, .. }) => {
assert_eq!(name, *want, "{symbol}");
}
(got, want) => assert_eq!(got, *want, "{symbol}"),
}
if import.name_type == IMPORT_NAME_EXPORTAS {
assert_eq!(import.export_as, Some(&b"real_export"[..]));
}
}
compare_short_imports_with_readobj(&path, &imports);
}
}
fn name_type_name(name_type: u8) -> &'static str {
match name_type {
0 => "ordinal",
1 => "name",
2 => "noprefix",
3 => "undecorate",
4 => "export as",
_ => "unknown",
}
}
fn compare_short_imports_with_readobj(path: &Path, imports: &[ShortImport<'_>]) {
let Some(output) = readobj(&[], path) else {
return;
};
let mut theirs = Vec::new();
for block in output.split("\nFile: ").skip(1) {
if !block.contains("Format: COFF-import-file") {
continue;
}
let mut entry = BTreeMap::new();
let mut symbols = Vec::new();
for line in block.lines() {
if let Some(symbol) = line.strip_prefix("Symbol: ") {
symbols.push(symbol.to_owned());
} else if let Some((key, value)) = line.split_once(": ") {
entry.insert(key.to_owned(), value.to_owned());
}
}
theirs.push((entry, symbols));
}
assert_eq!(theirs.len(), imports.len(), "{}", path.display());
for (import, (entry, symbols)) in imports.iter().zip(&theirs) {
let kind = match import.import_type {
0 => "code",
1 => "data",
_ => "const",
};
assert_eq!(entry["Type"], kind);
assert_eq!(entry["Name type"], name_type_name(import.name_type));
match import.import_name() {
ImportName::Name { name, .. } => assert_eq!(entry["Export name"], text(name)),
ImportName::Ordinal(_) => assert!(!entry.contains_key("Export name")),
}
let mut ours = vec![format!("__imp_{}", text(import.symbol_name))];
if import.defines_symbol_name() {
ours.push(text(import.symbol_name));
}
assert_eq!(&ours, symbols, "{}", path.display());
}
}
fn long_imports(path: &Path, data: &[u8]) -> (Vec<(String, String, String)>, usize, usize) {
let archive = Archive::parse(path, data).unwrap();
let members: Vec<_> = archive.members().map(Result::unwrap).collect();
let objects: Vec<_> = members
.iter()
.map(|m| CoffObject::parse(m.bytes().unwrap(), source(path)).unwrap())
.collect();
let classified: Vec<_> = objects
.iter()
.map(|o| classify_long_import(o).unwrap())
.collect();
let mut dlls = LongImportDlls::new();
let (mut heads, mut tails) = (0, 0);
for member in classified.iter().flatten() {
match member {
LongImportMember::Head { .. } => heads += 1,
LongImportMember::Tail { .. } => tails += 1,
LongImportMember::Symbol(_) => {}
}
dlls.add(member);
}
let mut symbols = Vec::new();
for member in classified.iter().flatten() {
if let LongImportMember::Symbol(symbol) = member {
let import = match symbol.import {
ImportName::Name { name, .. } => text(name),
ImportName::Ordinal(ordinal) => format!("#{ordinal}"),
};
let dll = dlls
.dll_name(symbol.head_symbol)
.map(text)
.unwrap_or_else(|| "?".to_owned());
symbols.push((text(symbol.symbol_name()), import, dll));
}
}
symbols.sort();
(symbols, heads, tails)
}
#[test]
fn long_import_fixture() {
let (path, data) = fixture("long-import-x86_64.a");
let (symbols, heads, tails) = long_imports(&path, &data);
assert_eq!((heads, tails), (1, 1));
assert_eq!(symbols, expected_long_imports());
}
fn expected_long_imports() -> Vec<(String, String, String)> {
let mut expected: Vec<_> = [
("EXPORTAS", "EXPORTAS"),
("by_ordinal", "#7"),
("constant_value", "constant_value"),
("exported_as", "exported_as"),
("hinted", "hinted"),
("plain_data", "plain_data"),
("plain_function", "plain_function"),
("public_name", "public_name"),
("real_export", "real_export"),
]
.iter()
.map(|(s, i)| ((*s).to_owned(), (*i).to_owned(), "testlib.dll".to_owned()))
.collect();
expected.sort();
expected
}
#[test]
fn dll_fixture() {
let (path, data) = fixture("testdll.dll");
let image = PeImage::parse(&data, source(&path)).unwrap();
check_test_dll(&image);
compare_exports_with_readobj(&path, &image);
assert!(matches!(
CoffFile::parse(&data, source(&path)),
Ok(CoffFile::Image(_))
));
assert!(CoffObject::parse(&data, source(&path)).is_err());
}
fn check_test_dll(image: &PeImage<'_>) {
assert!(image.is_dll());
assert!(image.is_pe32_plus());
assert_eq!(image.machine(), IMAGE_FILE_MACHINE_AMD64);
assert_eq!(image.architecture(), Some(Architecture::X86_64));
assert_ne!(image.image_base(), 0);
assert!(matches!(
image.subsystem(),
consts::IMAGE_SUBSYSTEM_WINDOWS_CUI | IMAGE_SUBSYSTEM_WINDOWS_GUI
));
let exports = image.exports().unwrap().expect("export directory");
assert_eq!(exports.dll_name, b"testdll.dll");
assert_eq!(exports.ordinal_base, 1);
let named: Vec<_> = exports.named().map(Result::unwrap).collect();
let names: Vec<_> = named.iter().map(|e| text(e.name.unwrap())).collect();
assert_eq!(
names,
["dll_data", "dll_function", "dll_rodata", "forwarded_sleep"]
);
let by_name = |n: &str| named.iter().find(|e| e.name == Some(n.as_bytes())).unwrap();
assert_eq!(by_name("dll_function").ordinal, 1);
assert_eq!(
by_name("forwarded_sleep").target,
ExportTarget::Forwarder(b"kernel32.Sleep")
);
let ExportTarget::Rva(function) = by_name("dll_function").target else {
panic!("dll_function is not an RVA")
};
let ExportTarget::Rva(data) = by_name("dll_data").target else {
panic!("dll_data is not an RVA")
};
assert!(!image.is_data_rva(function));
assert!(image.is_data_rva(data));
let bytes = image.data_at_rva(data, 4).unwrap();
assert_eq!(bytes, 42u32.to_le_bytes());
let all = exports.all().unwrap();
let hidden = all.iter().find(|e| e.ordinal == 5).expect("ordinal 5");
assert_eq!(hidden.name, None);
assert!(all.iter().all(|e| e.ordinal != 4), "unused slot listed");
}
fn compare_exports_with_readobj(path: &Path, image: &PeImage<'_>) {
let Some(output) = readobj(&["--coff-exports"], path) else {
return;
};
let root = parse_tree(&output);
let mut theirs = Vec::new();
for export in root.scopes("Export") {
let name = export.get("Name");
let target = match export.value("ForwardedTo") {
Some(to) => format!("-> {to}"),
None => {
let rva = unumber(export.get("RVA"));
if rva == 0 {
continue;
}
format!("{rva:#x}")
}
};
theirs.push((unumber(export.get("Ordinal")), name.to_owned(), target));
}
let ours: Vec<_> = image
.exports()
.unwrap()
.map(|dir| dir.all().unwrap())
.unwrap_or_default()
.into_iter()
.map(|e| {
let target = match e.target {
ExportTarget::Rva(rva) => format!("{rva:#x}"),
ExportTarget::Forwarder(to) => format!("-> {}", text(to)),
};
(
u64::from(e.ordinal),
e.name.map(text).unwrap_or_default(),
target,
)
})
.collect();
assert_same("export", path, &ours, &theirs);
}
#[test]
fn def_fixtures() {
let (path, data) = fixture("testlib.def");
let def = parse_module_definition(&data, source(&path)).unwrap();
assert_eq!(def.name, Some(&b"testlib.dll"[..]));
let exports: Vec<_> = def
.exports
.iter()
.map(|e| {
(
text(&e.name),
e.internal_name.as_deref().map(text),
e.ordinal,
e.noname,
e.data,
e.constant,
e.export_as.as_deref().map(text),
)
})
.collect();
let s = |v: &str| v.to_owned();
assert_eq!(
exports,
[
(s("plain_function"), None, None, false, false, false, None),
(s("plain_data"), None, None, false, true, false, None),
(s("by_ordinal"), None, Some(7), true, false, false, None),
(
s("public_name"),
Some(s("internal_name")),
None,
false,
false,
false,
None
),
(s("constant_value"), None, None, false, false, true, None),
(s("hinted"), None, Some(12), false, false, false, None),
(
s("exported_as"),
None,
None,
false,
false,
false,
Some(s("real_export"))
),
]
);
let (path, data) = fixture("testdll.def");
let def = parse_module_definition(&data, source(&path)).unwrap();
assert_eq!(def.exports.len(), 5);
assert_eq!(def.exports[4].forwarder(), Some(&b"kernel32.Sleep"[..]));
assert_eq!(def.exports[4].ordinal, Some(6));
}
#[test]
fn coff_file_dispatch() {
let (path, data) = fixture("short-import-x86_64.lib");
let imports = short_imports(&path, &data);
assert!(!imports.is_empty());
let (path, data) = fixture("mingw-x86_64.o");
assert!(matches!(
CoffFile::parse(&data, source(&path)),
Ok(CoffFile::Object(_))
));
assert!(ShortImport::parse(&data, source(&path)).is_err());
assert!(PeImage::parse(&data, source(&path)).is_err());
}
#[test]
fn types_are_send_and_sync() {
fn check<T: Send + Sync>() {}
check::<CoffObject<'_>>();
check::<CoffFile<'_>>();
check::<PeImage<'_>>();
check::<ShortImport<'_>>();
check::<LongImportDlls<'_>>();
check::<qld::coff::read::ModuleDefinition<'_>>();
check::<qld::coff::read::Directives<'_>>();
check::<qld::coff::read::ExportDirectory<'_>>();
}
#[test]
fn mingw_gcc_objects_match_readobj() {
let dir = scratch_dir("mingw_gcc");
let src = data_dir().join("mingw.c");
for (name, extra) in [("regular.o", None), ("big.o", Some("-Wa,-mbig-obj"))] {
let out = dir.join(name);
let mut args = vec![
"-c",
"-O0",
"-fcommon",
path_str(&src),
"-o",
path_str(&out),
];
args.extend(extra);
let Some(data) = build("x86_64-w64-mingw32-gcc", &args, &out) else {
return;
};
let object = parse_object(&out, &data);
assert_eq!(object.is_bigobj(), extra.is_some());
check_mingw_object(&object);
if !compare_object_with_readobj(&out, &object) {
return;
}
}
}
#[test]
fn mingw_gxx_comdat_objects_match_readobj() {
let dir = scratch_dir("mingw_gxx");
let src = dir.join("comdat.cpp");
std::fs::write(
&src,
"template <typename T> T twice(T v) { return v + v; }\n\
inline int shared_inline() { static int n; return ++n; }\n\
struct Widget { virtual ~Widget() {} virtual int f() { return twice(3); } };\n\
int use() { Widget w; return w.f() + twice(2.0) + shared_inline(); }\n",
)
.unwrap();
for (target, name) in [
("x86_64-w64-mingw32-g++", "x86_64.o"),
("i686-w64-mingw32-g++", "i686.o"),
] {
let out = dir.join(name);
let Some(data) = build(
target,
&["-c", "-O0", path_str(&src), "-o", path_str(&out)],
&out,
) else {
continue;
};
let object = parse_object(&out, &data);
let comdats = object
.sections()
.map(Result::unwrap)
.filter(|s| s.header.is_comdat())
.count();
assert!(comdats >= 4, "{name}: {comdats} COMDAT sections");
if !compare_object_with_readobj(&out, &object) {
return;
}
}
}
#[test]
fn clang_msvc_objects_match_readobj() {
let dir = scratch_dir("clang_msvc");
let src = data_dir().join("msvc.cpp");
for (target, machine) in [
("x86_64-pc-windows-msvc", IMAGE_FILE_MACHINE_AMD64),
("i686-pc-windows-msvc", IMAGE_FILE_MACHINE_I386),
("aarch64-pc-windows-msvc", IMAGE_FILE_MACHINE_ARM64),
] {
let out = dir.join(format!("{target}.obj"));
let target_arg = format!("--target={target}");
let Some(data) = build(
"clang",
&[
&target_arg,
"-c",
"-O0",
"-Xclang",
"-cfguard",
path_str(&src),
"-o",
path_str(&out),
],
&out,
) else {
return;
};
let object = parse_object(&out, &data);
check_msvc_object(&object, machine);
compare_object_with_readobj(&out, &object);
}
}
#[test]
fn many_sections_bigobj() {
let dir = scratch_dir("many_sections");
let src = dir.join("many.cpp");
let mut code = String::new();
for i in 0..3000 {
code.push_str(&format!(
"inline int function_with_a_long_name_{i}() {{ return {i}; }}\n\
int use_{i}() {{ return function_with_a_long_name_{i}(); }}\n"
));
}
std::fs::write(&src, code).unwrap();
let out = dir.join("many.o");
let Some(data) = build(
"x86_64-w64-mingw32-g++",
&[
"-c",
"-O0",
"-Wa,-mbig-obj",
"-ffunction-sections",
path_str(&src),
"-o",
path_str(&out),
],
&out,
) else {
return;
};
let object = parse_object(&out, &data);
assert!(object.is_bigobj());
assert!(object.section_count() > 6000, "{}", object.section_count());
for s in object.symbols().iter().map(Result::unwrap) {
if let Some(number) = s.section_definition().and_then(|d| d.associative_section()) {
assert!(object.section(number).unwrap().header.is_comdat());
}
}
compare_object_with_readobj(&out, &object);
}
#[test]
fn windres_objects() {
let dir = scratch_dir("windres");
let rc = data_dir().join("resource.rc");
let out = dir.join("resource.o");
if let Some(data) = build(
"x86_64-w64-mingw32-windres",
&[path_str(&rc), "-o", path_str(&out)],
&out,
) {
let object = parse_object(&out, &data);
assert_eq!(object.resource_sections().count(), 1);
compare_object_with_readobj(&out, &object);
}
let out = dir.join("resource.obj");
if let Some(data) = build(
"llvm-windres",
&[
"--target=x86_64-pc-windows-msvc",
path_str(&rc),
"-o",
path_str(&out),
],
&out,
) {
let object = parse_object(&out, &data);
let names: Vec<_> = object
.resource_sections()
.map(|s| text(s.unwrap().name))
.collect();
assert_eq!(names, [".rsrc$01", ".rsrc$02"]);
compare_object_with_readobj(&out, &object);
}
}
#[test]
fn llvm_import_libraries() {
let dir = scratch_dir("llvm_import");
let def = data_dir().join("testlib.def");
for (machine, i386) in [("i386:x86-64", false), ("i386", true), ("arm64", false)] {
let out = dir.join(format!("{}.lib", machine.replace(':', "_")));
let Some(data) = build(
"llvm-dlltool",
&["-m", machine, "-d", path_str(&def), "-l", path_str(&out)],
&out,
) else {
return;
};
let imports = short_imports(&out, &data);
assert_eq!(imports.len(), expected_short_imports(i386).len());
if machine == "arm64" {
assert!(
imports
.iter()
.all(|i| i.machine == IMAGE_FILE_MACHINE_ARM64)
);
}
compare_short_imports_with_readobj(&out, &imports);
}
let out = dir.join("lib.lib");
let def_arg = format!("/def:{}", path_str(&def));
let out_arg = format!("/out:{}", path_str(&out));
if let Some(data) = build("llvm-lib", &["/machine:x64", &def_arg, &out_arg], &out) {
let imports = short_imports(&out, &data);
assert!(!imports.is_empty());
compare_short_imports_with_readobj(&out, &imports);
}
}
#[test]
fn gnu_dlltool_import_library() {
let dir = scratch_dir("gnu_dlltool");
let def = data_dir().join("testlib.def");
let out = dir.join("libtest.a");
let Some(data) = build(
"x86_64-w64-mingw32-dlltool",
&["-d", path_str(&def), "-l", path_str(&out)],
&out,
) else {
return;
};
let (symbols, heads, tails) = long_imports(&out, &data);
assert_eq!((heads, tails), (1, 1));
assert_eq!(symbols, expected_long_imports());
}
#[test]
fn mingw_dll_exports_match_readobj() {
let dir = scratch_dir("mingw_dll");
let out = dir.join("testdll.dll");
let Some(data) = build(
"x86_64-w64-mingw32-gcc",
&[
"-O1",
"-shared",
"-nostdlib",
"-Wl,-e,DllMain",
path_str(&data_dir().join("testdll.c")),
path_str(&data_dir().join("testdll.def")),
"-o",
path_str(&out),
],
&out,
) else {
return;
};
let image = PeImage::parse(&data, source(&out)).unwrap();
check_test_dll(&image);
compare_exports_with_readobj(&out, &image);
let names: Vec<_> = image.sections().map(|s| text(s.unwrap().name)).collect();
assert!(names.contains(&".text".to_owned()), "{names:?}");
let out = dir.join("crt.dll");
let src = dir.join("crt.c");
std::fs::write(
&src,
"#include <stdio.h>\n__declspec(dllexport) int hello(void) { return puts(\"hi\"); }\n",
)
.unwrap();
if let Some(data) = build(
"x86_64-w64-mingw32-gcc",
&["-g", "-shared", path_str(&src), "-o", path_str(&out)],
&out,
) {
let image = PeImage::parse(&data, source(&out)).unwrap();
let exports = image.exports().unwrap().unwrap();
assert!(exports.named().any(|e| e.unwrap().name == Some(b"hello")));
compare_exports_with_readobj(&out, &image);
let names: Vec<_> = image.sections().map(|s| text(s.unwrap().name)).collect();
assert!(
names.iter().any(|n| n.starts_with(".debug_")),
"long section names resolved through the string table: {names:?}"
);
}
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 7;
self.0 ^= self.0 << 17;
self.0
}
fn below(&mut self, n: usize) -> usize {
usize::try_from(self.next() % (n.max(1) as u64)).unwrap()
}
}
fn exercise(data: &[u8]) {
let path = Path::new("fuzz");
let src = source(path);
match CoffFile::parse(data, src) {
Ok(CoffFile::Object(object)) => exercise_object(&object),
Ok(CoffFile::ShortImport(import)) => {
let _ = import.import_name();
}
Ok(CoffFile::Image(image)) => {
for section in image.sections().flatten() {
let _ = image.section_data(§ion.header);
}
for rva in [0, 0x1000, 0x2000, u32::MAX] {
let _ = image.rva_to_file_offset(rva);
let _ = image.c_string_at_rva(rva);
let _ = image.is_data_rva(rva);
}
if let Ok(Some(exports)) = image.exports() {
for export in exports.named().take(10_000) {
let _ = export;
}
let _ = exports.all();
}
}
Err(_) => {}
}
let _ = CoffObject::parse(data, src).map(|o| exercise_object(&o));
let _ = parse_module_definition(data, src);
for directive in qld::coff::read::parse_directives(data, 0, src).take(10_000) {
let _ = directive;
}
}
fn exercise_object(object: &CoffObject<'_>) {
for section in object.sections().take(100_000) {
let Ok(section) = section else { continue };
let _ = object.section_data(§ion.header);
if let Ok(relocs) = object.relocations(§ion.header) {
for reloc in relocs.iter().take(10_000) {
let _ = object.symbol(reloc.symbol_table_index);
}
}
}
for symbol in object.symbols().iter().take(100_000).flatten() {
let _ = (
symbol.section_definition(),
symbol.weak_external(),
symbol.function_definition(),
symbol.file_name(),
symbol.aux_record(3),
);
}
if let Some(directives) = object.directives().next()
&& let Ok(directives) = directives
{
for directive in directives.take(10_000) {
let _ = directive;
}
}
if let Ok(Some(addrsig)) = object.addrsig() {
for index in addrsig.take(10_000) {
let _ = index;
}
}
let _ = object.feat00();
let _ = classify_long_import(object);
let _ = object.resource_sections().count();
}
fn corrupt_and_parse(data: &[u8], rounds: usize, seed: u64) {
let mut rng = Rng(seed | 1);
exercise(data);
for len in 0..data.len().min(64) {
exercise(&data[..len]);
}
for _ in 0..rounds {
let mut mutated = data.to_vec();
match rng.below(4) {
0 => mutated.truncate(rng.below(data.len())),
1 => {
for _ in 0..1 + rng.below(8) {
let at = rng.below(mutated.len());
mutated[at] ^= 1 << rng.below(8);
}
}
2 => {
let at = rng.below(mutated.len().saturating_sub(4));
let value: u32 = match rng.below(4) {
0 => u32::MAX,
1 => 0xffff,
2 => u32::try_from(rng.below(mutated.len() + 64)).unwrap(),
_ => rng.next() as u32,
};
if mutated.len() >= at + 4 {
mutated[at..at + 4].copy_from_slice(&value.to_le_bytes());
}
}
_ => {
let at = rng.below(mutated.len());
let len = rng.below(mutated.len() - at);
mutated.drain(at..at + len);
}
}
exercise(&mutated);
}
}
fn fixture_inputs() -> Vec<(String, Vec<u8>)> {
let mut inputs = Vec::new();
let mut names: Vec<_> = std::fs::read_dir(data_dir())
.unwrap()
.map(|e| e.unwrap().file_name().into_string().unwrap())
.filter(|n| !n.ends_with(".sh"))
.collect();
names.sort();
for name in names {
let (path, data) = fixture(&name);
if let Ok(archive) = Archive::parse(&path, &data) {
for member in archive.members().map(Result::unwrap) {
inputs.push((
format!("{name}({})", member.display_name()),
member.bytes().unwrap().to_vec(),
));
}
}
inputs.push((name, data));
}
inputs
}
#[test]
fn corrupted_fixtures_do_not_panic() {
let rounds = std::env::var("QLD_COFF_FUZZ_ROUNDS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(if cfg!(debug_assertions) { 300 } else { 3000 });
for (i, (name, data)) in fixture_inputs().iter().enumerate() {
let seed = 0x9e37_79b9_7f4a_7c15 ^ (i as u64).wrapping_mul(0x1234_5678_9abc_def1);
let result = std::panic::catch_unwind(|| corrupt_and_parse(data, rounds, seed));
assert!(result.is_ok(), "{name}: parser panicked");
}
}
#[test]
fn random_text_does_not_panic() {
let mut rng = Rng(0x5eed);
let alphabet = b" \t\r\n\0\"'\\=,;@.-/:_abcXYZ0129EXPORTSLIBRARYDATANONAMEBASE";
for _ in 0..2000 {
let len = rng.below(80);
let bytes: Vec<u8> = (0..len)
.map(|_| alphabet[rng.below(alphabet.len())])
.collect();
exercise(&bytes);
}
}
#[test]
#[ignore = "slow: runs llvm-readobj over whole archives"]
fn sysroot_archives_match_readobj() {
let archives: Vec<PathBuf> = match std::env::var_os("QLD_COFF_READOBJ_ARCHIVES") {
Some(list) => std::env::split_paths(&list).collect(),
None => {
let Some(libgcc) = run("x86_64-w64-mingw32-gcc", &["-print-libgcc-file-name"]) else {
return;
};
let mut list = vec![PathBuf::from(libgcc.trim())];
for lib in ["libmingwex.a", "libmsvcrt.a", "libkernel32.a"] {
if let Some(path) = run(
"x86_64-w64-mingw32-gcc",
&[&format!("-print-file-name={lib}")],
) {
list.push(PathBuf::from(path.trim()));
}
}
list
}
};
let mut members = 0;
for path in archives.iter().filter(|p| p.is_file()) {
let Some(output) = readobj(
&["--file-headers", "--sections", "--relocations", "--symbols"],
path,
) else {
return;
};
let data = std::fs::read(path).unwrap();
let archive = Archive::parse(path, &data).unwrap();
let blocks: Vec<_> = output
.split("\nFile: ")
.skip(1)
.filter(|b| !b.contains("Format: COFF-import-file"))
.collect();
let objects: Vec<_> = archive
.members()
.map(Result::unwrap)
.filter_map(|m| m.bytes())
.filter(|b| matches!(qld::input::identify(b), qld::input::FileFormat::Coff(_)))
.collect();
assert_eq!(blocks.len(), objects.len(), "{}", path.display());
for (bytes, block) in objects.iter().zip(&blocks) {
let object = CoffObject::parse(bytes, source(path)).unwrap();
let theirs = readobj_inventory(&parse_tree(block));
let ours = object_inventory(&object);
assert_same("header field", path, &ours.header, &theirs.header);
assert_same("section", path, &ours.sections, &theirs.sections);
assert_same("relocation", path, &ours.relocations, &theirs.relocations);
assert_same("symbol", path, &ours.symbols, &theirs.symbols);
members += 1;
}
println!("{}: {} members match", path.display(), objects.len());
}
println!("{members} members compared");
}
#[derive(Debug, Default)]
struct SweepStats {
files: usize,
archives: usize,
members: usize,
objects: usize,
bigobj: usize,
short_imports: usize,
long_import_symbols: usize,
long_import_unresolved: usize,
images: usize,
exports: usize,
defs: usize,
other: usize,
errors: Vec<String>,
}
fn sweep_roots() -> Vec<PathBuf> {
let mut roots: Vec<PathBuf> = [
"/usr/x86_64-w64-mingw32",
"/usr/i686-w64-mingw32",
"/usr/lib/gcc/x86_64-w64-mingw32",
"/usr/lib/gcc/i686-w64-mingw32",
"/usr/lib/mingw64-toolchain",
"/usr/lib/mingw32-toolchain",
]
.iter()
.map(PathBuf::from)
.collect();
for compiler in ["x86_64-w64-mingw32-gcc", "i686-w64-mingw32-gcc"] {
if let Ok(output) = Command::new(compiler).arg("-print-sysroot").output() {
let sysroot = String::from_utf8_lossy(&output.stdout).trim().to_owned();
if !sysroot.is_empty() {
roots.push(PathBuf::from(sysroot));
}
}
}
if let Some(extra) = std::env::var_os("QLD_COFF_SWEEP_DIRS") {
roots.extend(std::env::split_paths(&extra));
}
roots.retain(|r| r.is_dir());
roots.sort();
roots.dedup();
let all = roots.clone();
roots.retain(|r| !all.iter().any(|o| o != r && r.starts_with(o)));
roots
}
fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
let Ok(kind) = entry.file_type() else {
continue;
};
if kind.is_dir() {
walk(&path, out);
} else if kind.is_file()
&& path.extension().and_then(|e| e.to_str()).is_some_and(|e| {
matches!(
e.to_ascii_lowercase().as_str(),
"a" | "lib" | "rlib" | "o" | "obj" | "dll" | "exe" | "def"
)
})
{
out.push(path);
}
}
}
fn sweep_one(name: &str, data: &[u8], stats: &mut SweepStats) {
use qld::input::FileFormat;
if !matches!(
qld::input::identify(data),
FileFormat::Coff(_) | FileFormat::CoffImport(_) | FileFormat::Pe(_)
) {
stats.other += 1;
return;
}
let path = Path::new(name);
let parsed = CoffFile::parse(data, source(path));
match parsed {
Ok(CoffFile::Object(object)) => {
stats.objects += 1;
stats.bigobj += usize::from(object.is_bigobj());
let before = stats.errors.len();
let mut check = |r: Result<(), qld::Error>| {
if let Err(e) = r {
stats.errors.push(format!("{name}: {e}"));
}
};
check((|| {
for section in object.sections() {
let section = section?;
object.section_data(§ion.header)?;
for reloc in object.relocations(§ion.header)?.iter() {
object.symbol(reloc.symbol_table_index)?;
}
}
for symbol in object.symbols().iter() {
symbol?;
}
for directives in object.directives() {
for directive in directives? {
directive?;
}
}
object.feat00()?;
Ok(())
})());
if stats.errors.len() == before
&& let Err(e) = classify_long_import(&object)
{
stats.errors.push(format!("{name}: {e}"));
}
}
Ok(CoffFile::ShortImport(_)) => stats.short_imports += 1,
Ok(CoffFile::Image(image)) => {
stats.images += 1;
match image.exports() {
Ok(Some(exports)) => match exports.all() {
Ok(all) => {
stats.exports += all.len();
if !all.is_empty() && Path::new(name).is_file() {
compare_exports_with_readobj(path, &image);
}
}
Err(e) => stats.errors.push(format!("{name}: {e}")),
},
Ok(None) => {}
Err(e) => stats.errors.push(format!("{name}: {e}")),
}
}
Err(e) => stats.errors.push(format!("{name}: {e}")),
}
}
#[test]
#[ignore = "slow: sweeps the MinGW sysroot"]
fn sweep_mingw_sysroot() {
let roots = sweep_roots();
if roots.is_empty() {
skip("no MinGW sysroot found");
return;
}
let mut files = Vec::new();
for root in &roots {
walk(root, &mut files);
}
files.sort();
let mut stats = SweepStats::default();
for path in &files {
let Ok(data) = std::fs::read(path) else {
continue;
};
stats.files += 1;
let name = path.display().to_string();
if path.extension().is_some_and(|e| e == "def") {
let looks_like_def = [&b"EXPORTS"[..], b"LIBRARY", b"NAME"]
.iter()
.any(|k| data.windows(k.len()).any(|w| w == *k));
if path.components().any(|c| c.as_os_str() == "include") || !looks_like_def {
stats.other += 1;
} else if let Err(e) = parse_module_definition(&data, source(path)) {
stats.errors.push(format!("{name}: {e}"));
} else {
stats.defs += 1;
}
continue;
}
if data.starts_with(b"!<arch>\n") {
stats.archives += 1;
let archive = match Archive::parse(path, &data) {
Ok(archive) => archive,
Err(e) => {
stats.errors.push(format!("{name}: {e}"));
continue;
}
};
let mut dlls = LongImportDlls::new();
let mut pending = Vec::new();
let mut objects = Vec::new();
for member in archive.members() {
let member = match member {
Ok(member) => member,
Err(e) => {
stats.errors.push(format!("{name}: {e}"));
break;
}
};
let Some(bytes) = member.bytes() else {
continue;
};
stats.members += 1;
let member_name = format!("{name}({})", member.display_name());
sweep_one(&member_name, bytes, &mut stats);
if let Ok(object) = CoffObject::parse(bytes, source(path)) {
objects.push(object);
}
}
for object in &objects {
if let Ok(Some(member)) = classify_long_import(object) {
dlls.add(&member);
if let LongImportMember::Symbol(symbol) = member {
pending.push(symbol.head_symbol);
}
}
}
stats.long_import_symbols += pending.len();
let unresolved = pending
.iter()
.filter(|head| dlls.dll_name(head).is_none())
.count();
if unresolved != 0 {
stats
.errors
.push(format!("{name}: {unresolved} long imports without a DLL"));
}
stats.long_import_unresolved += unresolved;
} else {
sweep_one(&name, &data, &mut stats);
}
}
println!("roots: {roots:?}");
println!(
"files {} (archives {}, members {}): objects {} (bigobj {}), short imports {}, \
long import symbols {} (unresolved {}), images {} with {} exports, .def files {}, \
other {}",
stats.files,
stats.archives,
stats.members,
stats.objects,
stats.bigobj,
stats.short_imports,
stats.long_import_symbols,
stats.long_import_unresolved,
stats.images,
stats.exports,
stats.defs,
stats.other
);
for error in stats.errors.iter().take(50) {
println!("error: {error}");
}
assert!(stats.errors.is_empty(), "{} errors", stats.errors.len());
}