use std::path::PathBuf;
use unstrip::funcsig::{recover_methods_from_types, render_method_signature, TypeCache};
use unstrip::gobin::GoBinary;
use unstrip::moduledata::ModuleData;
use unstrip::types;
fn fixture(name: &str) -> Option<PathBuf> {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("testdata")
.join(name);
if path.exists() {
Some(path)
} else {
eprintln!("skipping: fixture {name} not built");
None
}
}
#[test]
fn recovers_methods_from_hello_binary() {
let Some(path) = fixture("hello.linux-amd64.stripped") else {
return;
};
let bin = GoBinary::open(&path).expect("open binary");
let md = ModuleData::locate(&bin).expect("locate moduledata");
let recovered_types = types::recover_all(&bin, &md).expect("recover types");
let methods = recover_methods_from_types(&bin, &md, &recovered_types);
assert!(
methods.len() >= 50,
"expected at least 50 recovered methods, got {}. \
First 10: {:#?}",
methods.len(),
methods.iter().take(10).collect::<Vec<_>>()
);
}
#[test]
fn recovered_methods_carry_resolvable_text_pcs() {
let Some(path) = fixture("hello.linux-amd64.stripped") else {
return;
};
let bin = GoBinary::open(&path).expect("open binary");
let md = ModuleData::locate(&bin).expect("locate moduledata");
let recovered_types = types::recover_all(&bin, &md).expect("recover types");
let methods = recover_methods_from_types(&bin, &md, &recovered_types);
let mut in_range = 0usize;
let mut zero_tfn = 0usize;
let mut out_of_range_examples = Vec::new();
for m in &methods {
if m.tfn_pc == 0 {
zero_tfn += 1;
continue;
}
if m.tfn_pc >= md.text && m.tfn_pc < md.etext {
in_range += 1;
} else if out_of_range_examples.len() < 5 {
out_of_range_examples.push((m.receiver.clone(), m.name.clone(), m.tfn_pc));
}
}
let non_zero = methods.len() - zero_tfn;
let out_of_range = non_zero - in_range;
assert!(
out_of_range * 100 < non_zero,
"{out_of_range} of {non_zero} non-zero method PCs landed outside [{:#x}, {:#x}); first 5: {:?}",
md.text,
md.etext,
out_of_range_examples
);
}
#[test]
fn recovered_methods_have_non_empty_names() {
let Some(path) = fixture("hello.linux-amd64.stripped") else {
return;
};
let bin = GoBinary::open(&path).expect("open binary");
let md = ModuleData::locate(&bin).expect("locate moduledata");
let recovered_types = types::recover_all(&bin, &md).expect("recover types");
let methods = recover_methods_from_types(&bin, &md, &recovered_types);
let named = methods.iter().filter(|m| !m.name.is_empty()).count();
let total = methods.len();
assert!(
named * 100 >= total * 95,
"only {named} of {total} methods have a name; names blob walk is off"
);
}
fn render_all(path: PathBuf) -> Vec<(String, String, String)> {
let bin = GoBinary::open(&path).expect("open binary");
let md = ModuleData::locate(&bin).expect("locate moduledata");
let recovered_types = types::recover_all(&bin, &md).expect("recover types");
let methods = recover_methods_from_types(&bin, &md, &recovered_types);
let mut cache = TypeCache::new(&bin, &md);
cache.seed_from(&recovered_types);
let mut out = Vec::new();
for m in &methods {
if let Some(sig) = render_method_signature(m, &mut cache) {
out.push((m.receiver.clone(), m.name.clone(), sig));
}
}
out
}
#[test]
fn renders_well_known_stdlib_method_signatures() {
let Some(path) = fixture("hello.linux-amd64.stripped") else {
return;
};
let rendered = render_all(path);
let expected: &[(&str, &str, &str)] = &[
("*errors.errorString", "Error", "() string"),
("reflect.Kind", "String", "() string"),
("*os.File", "Write", "(_0 []uint8) (int, error)"),
("*os.File", "Read", "(_0 []uint8) (int, error)"),
("*atomic.Int64", "Swap", "(_0 int64) int64"),
("syscall.Errno", "Is", "(_0 error) bool"),
];
for (recv, name, want) in expected {
let got = rendered
.iter()
.find(|(r, n, _)| r == recv && n == name)
.map(|(_, _, s)| s.as_str());
assert_eq!(
got,
Some(*want),
"{recv}.{name}: expected {want:?}, got {got:?}"
);
}
}
#[test]
fn signature_rendering_covers_at_least_one_third_of_methods() {
let Some(path) = fixture("hello.linux-amd64.stripped") else {
return;
};
let bin = GoBinary::open(&path).expect("open binary");
let md = ModuleData::locate(&bin).expect("locate moduledata");
let recovered_types = types::recover_all(&bin, &md).expect("recover types");
let methods = recover_methods_from_types(&bin, &md, &recovered_types);
let mut cache = TypeCache::new(&bin, &md);
cache.seed_from(&recovered_types);
let rendered = methods
.iter()
.filter(|m| render_method_signature(m, &mut cache).is_some())
.count();
let total = methods.len();
assert!(
rendered * 3 >= total,
"only {rendered} of {total} methods got a rendered signature \
({:.1}%); Day-2 floor is 33%",
100.0 * rendered as f64 / total as f64
);
}
#[test]
fn variadic_method_renders_with_dotdotdot() {
let Some(path) = fixture("hello.linux-amd64.stripped") else {
return;
};
let rendered = render_all(path);
let any_variadic = rendered.iter().find(|(_, _, sig)| sig.contains("..."));
if let Some((recv, name, sig)) = any_variadic {
assert!(
!sig.contains("...[]"),
"{recv}.{name}: variadic should be ...T not ...[]T: {sig}"
);
}
}
#[test]
fn itab_merge_widens_signature_coverage() {
let Some(path) = fixture("hello.linux-amd64.stripped") else {
return;
};
let bin = GoBinary::open(&path).expect("open binary");
let md = ModuleData::locate(&bin).expect("locate moduledata");
let recovered_types = types::recover_all(&bin, &md).expect("recover types");
let recovered_itabs = unstrip::itabs::recover_all(&bin, &md).expect("recover itabs");
let from_types = recover_methods_from_types(&bin, &md, &recovered_types);
let from_itabs =
unstrip::funcsig::recover_methods_from_itabs(&recovered_types, &recovered_itabs);
let merged = unstrip::funcsig::merge_methods(from_types.clone(), from_itabs.clone());
assert!(
merged.len() <= from_types.len() + from_itabs.len(),
"merge created duplicates: {} > {} + {}",
merged.len(),
from_types.len(),
from_itabs.len()
);
assert!(
merged.len() >= from_types.len().max(from_itabs.len()),
"merge dropped unique records: {} < max({}, {})",
merged.len(),
from_types.len(),
from_itabs.len()
);
}
#[test]
fn signatures_by_pc_keys_on_function_entry() {
let Some(path) = fixture("hello.linux-amd64.stripped") else {
return;
};
let bin = GoBinary::open(&path).expect("open binary");
let md = ModuleData::locate(&bin).expect("locate moduledata");
let recovered_types = types::recover_all(&bin, &md).expect("recover types");
let methods = recover_methods_from_types(&bin, &md, &recovered_types);
let mut cache = TypeCache::new(&bin, &md);
cache.seed_from(&recovered_types);
let by_pc = unstrip::funcsig::signatures_by_pc(&methods, &mut cache);
assert!(
!by_pc.is_empty(),
"expected at least one PC-keyed signature on a hello-world binary"
);
for &pc in by_pc.keys() {
assert!(
pc >= md.text && pc < md.etext,
"signature key 0x{pc:x} falls outside [text=0x{:x}, etext=0x{:x})",
md.text,
md.etext
);
}
}