use std::path::PathBuf;
use std::process::Command;
use nornir::graph::extract::binary::{extract_graph, extract_object, ObjectClass};
use nornir::graph::extract::{Provenance, BINARY_GRAPH_PROVENANCE};
fn find_libz() -> Option<PathBuf> {
let dirs = [
"/usr/lib/x86_64-linux-gnu",
"/lib/x86_64-linux-gnu",
"/usr/lib64",
"/lib64",
"/usr/lib",
"/lib",
"/usr/lib/aarch64-linux-gnu",
];
for d in dirs {
for cand in ["libz.so", "libz.so.1"] {
let p = PathBuf::from(d).join(cand);
if p.exists() {
return Some(p);
}
}
if let Ok(rd) = std::fs::read_dir(d) {
for e in rd.flatten() {
let name = e.file_name();
let name = name.to_string_lossy();
if name.starts_with("libz.so") {
return Some(e.path());
}
}
}
}
None
}
#[test]
fn extracts_dt_needed_soname_and_symbols_from_real_libz() {
let Some(libz) = find_libz() else {
eprintln!("skip: no libz.so on this host");
return;
};
let obj = extract_object(&libz).expect("libz parses as ELF");
assert_eq!(obj.provenance, Provenance::Binary);
assert_eq!(obj.class, ObjectClass::SharedObject, "libz is a shared object");
let soname = obj.soname.clone().expect("libz declares DT_SONAME");
assert!(soname.starts_with("libz.so"), "soname = {soname}");
assert_eq!(obj.name, soname, "node key is the soname");
assert!(
obj.needed.iter().any(|n| n.starts_with("libc.so")),
"libz must DT_NEEDED libc, got {:?}",
obj.needed
);
for want in ["deflate", "inflate", "compress"] {
assert!(obj.exports.contains(want), "libz must export `{want}`");
}
assert!(!obj.imports.is_empty(), "libz has UND imports from libc");
if let Some(readelf_needed) = readelf_needed(&libz) {
for n in &obj.needed {
assert!(
readelf_needed.iter().any(|r| r == n),
"our DT_NEEDED `{n}` must appear in readelf output {readelf_needed:?}"
);
}
}
let graph = extract_graph(&[libz.clone()]).expect("graph builds");
assert_eq!(graph.objects.len(), 1);
let libc_edge = graph
.edges
.iter()
.find(|e| e.to.starts_with("libc.so"))
.expect("edge to libc kept");
assert!(!libc_edge.resolved, "libc wasn't analysed → unresolved edge");
let wg = graph.to_workspace_graph();
assert!(wg.has_component(&soname), "libz is a node in the shared graph");
assert!(
wg.facts.get(&soname).unwrap().produces.contains("deflate"),
"exports carried through as `produces`"
);
let rows = graph.to_symbol_rows();
let deflate = rows.iter().find(|r| r.item_name == "deflate").expect("deflate row");
assert_eq!(deflate.item_kind, "exported_symbol");
assert_eq!(deflate.module_path, BINARY_GRAPH_PROVENANCE, "provenance tag on the node");
let export_rows = rows.iter().filter(|r| r.item_kind == "exported_symbol").count();
let import_rows = rows.iter().filter(|r| r.item_kind == "imported_symbol").count();
nornir_testmatrix::functional_status(
"graph-binary-extract",
"elf_dt_needed_soname_symbols_reconstructed",
obj.needed.iter().any(|n| n.starts_with("libc.so"))
&& obj.exports.contains("deflate")
&& !obj.imports.is_empty(),
&format!(
"soname={soname} needed={:?} exports={export_rows} imports={import_rows} \
call_edges={}",
obj.needed,
graph.call_edges.len()
),
);
}
#[test]
fn call_edges_from_plt_got_relocations() {
let Some(libz) = find_libz() else {
eprintln!("skip: no libz.so on this host");
return;
};
let graph = extract_graph(&[libz]).expect("graph builds");
assert!(
!graph.call_edges.is_empty(),
"PLT/GOT relocations must yield call edges"
);
for c in &graph.call_edges {
assert!(!c.symbol.is_empty());
assert_eq!(c.provenance, Provenance::Binary);
}
nornir_testmatrix::functional_status(
"graph-binary-extract",
"plt_got_relocations_yield_call_edges",
!graph.call_edges.is_empty(),
&format!("call_edges={}", graph.call_edges.len()),
);
}
fn readelf_needed(path: &PathBuf) -> Option<Vec<String>> {
let out = Command::new("readelf").arg("-d").arg(path).output().ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout);
let mut needed = Vec::new();
for line in text.lines() {
if line.contains("(NEEDED)") {
if let Some(start) = line.find('[') {
if let Some(end) = line[start..].find(']') {
needed.push(line[start + 1..start + end].to_string());
}
}
}
}
Some(needed)
}