use std::sync::Arc;
use symbolic::debuginfo::{pdb::PdbObject, pe::PeObject, Object};
use crate::common;
use crate::mapping::PathMappings;
use crate::object_info::ObjectInfo;
use crate::platform::Platform;
impl ObjectInfo {
pub fn from_pdb(
pdb: PdbObject,
pdb_name: &str,
pe_name: Option<&str>,
pe: Option<PeObject>,
mapping: Option<Arc<PathMappings>>,
collect_inlines: bool,
) -> common::Result<Self> {
let pdb = Object::Pdb(pdb);
let pe = pe.map(Object::Pe);
ObjectInfo::from_object(
&pdb,
pdb_name,
pe.as_ref(),
pe_name,
Platform::Win,
mapping,
collect_inlines,
)
}
pub fn from_pe(pe_name: &str, pe: PeObject) -> common::Result<Self> {
let pdb_name = pe.debug_file_name().unwrap_or(pe_name.into()).to_string();
let pe = Object::Pe(pe);
let pdb_name = win_path_file_name(&pdb_name).to_string();
ObjectInfo::from_object(
&pe,
&pdb_name,
None,
Some(pe_name),
Platform::Win,
None,
false,
)
}
}
fn win_path_file_name(pdb_name: &str) -> &str {
let index = pdb_name.rfind('\\').map_or(0, |i| i + 1);
&pdb_name[index..]
}
#[cfg(test)]
mod tests {
use bitflags::bitflags;
use hashbrown::{HashMap, HashSet};
use std::fs::File;
use std::io::{Cursor, Read};
use std::path::PathBuf;
use symbolic::debuginfo::breakpad::{
BreakpadError, BreakpadFileMap, BreakpadFuncRecord, BreakpadLineRecord, BreakpadObject,
};
use super::*;
bitflags! {
struct TestFlags: u32 {
const ALL = 0;
const NO_MULTIPLICITY = 0b1;
const NO_FILE_LINE = 0b10;
const NO_FUNCS_LENGTH = 0b100;
}
}
const MS: &str = "https://msdl.microsoft.com/download/symbols";
fn dl_from_server(url: &str) -> Vec<u8> {
let mut response = reqwest::blocking::get(url).expect("GET request");
let mut buf = Vec::new();
response.copy_to(&mut buf).expect("Data from server");
buf
}
fn get_data_from_server(url: &str) -> (Vec<u8>, &str) {
let toks: Vec<_> = url.rsplitn(4, '/').collect();
let name = toks[2];
let pe_buf = dl_from_server(url);
let pe_buf = crate::utils::read_cabinet(pe_buf, PathBuf::from(name)).unwrap();
let (pe, pdb_buf, pdb_name) = crate::windows::utils::get_pe_pdb_buf(
&PathBuf::from("."),
&pe_buf,
crate::cache::get_sym_servers(Some(&format!("SRV*~/symcache*{MS}"))).as_ref(),
)
.unwrap();
let pdb = PdbObject::parse(&pdb_buf).unwrap();
let mut output = Vec::new();
let cursor = Cursor::new(&mut output);
let pdb = ObjectInfo::from_pdb(pdb, &pdb_name, Some(name), Some(pe), None, false).unwrap();
pdb.dump(cursor).unwrap();
let toks: Vec<_> = name.rsplitn(2, '.').collect();
(output, toks[1])
}
fn get_new_bp(file_name: &str, mapping: Option<Arc<PathMappings>>) -> Vec<u8> {
let path = PathBuf::from("./test_data/windows");
let mut path = path.join(file_name);
if !path.exists() {
path.set_extension("exe");
}
let pe_buf = crate::utils::read_file(&path);
let (pe, pdb_buf, pdb_name) = crate::windows::utils::get_pe_pdb_buf(
&path,
&pe_buf,
crate::cache::get_sym_servers(Some(&format!("SRV*~/symcache*{MS}"))).as_ref(),
)
.unwrap_or_else(|| (PeObject::parse(&pe_buf).unwrap(), vec![], "".to_string()));
let mut output = Vec::new();
let cursor = Cursor::new(&mut output);
if pdb_buf.is_empty() {
let pe = ObjectInfo::from_pe(file_name, pe).unwrap();
pe.dump(cursor).unwrap();
} else {
let pdb = PdbObject::parse(&pdb_buf).unwrap();
let pdb =
ObjectInfo::from_pdb(pdb, &pdb_name, Some(file_name), Some(pe), mapping, false)
.unwrap();
pdb.dump(cursor).unwrap();
}
output
}
fn get_data(file_name: &str) -> Vec<u8> {
let path = PathBuf::from("./test_data/windows");
let mut path = path.join(file_name);
path.set_extension("sym");
let mut file = File::open(&path).unwrap();
let mut buf = Vec::new();
file.read_to_end(&mut buf).unwrap();
buf
}
fn clean_old_lines(func: &BreakpadFuncRecord) -> Vec<BreakpadLineRecord> {
let mut res: Vec<BreakpadLineRecord> = Vec::new();
func.lines().for_each(|l| {
let line = l.unwrap();
if line.line >= 0x00f0_0000 {
res.last_mut().unwrap().size += line.size
} else {
res.push(line);
}
});
res
}
fn check_headers(new: &BreakpadObject, old: &BreakpadObject) {
assert_eq!(new.code_id(), old.code_id(), "Bad code id");
assert_eq!(new.debug_id(), old.debug_id(), "Bad debug id");
assert_eq!(new.arch(), old.arch(), "Bad arch");
assert_eq!(new.name(), old.name(), "Bad name");
assert_eq!(new.kind(), old.kind(), "Bad kind");
}
fn check_func_len(
func_new: &[std::result::Result<BreakpadFuncRecord<'_>, BreakpadError>],
func_old: &[std::result::Result<BreakpadFuncRecord<'_>, BreakpadError>],
) {
if func_new.len() != func_old.len() {
let mut old_addresses = HashMap::<u64, u32>::default();
let mut old_keys = HashSet::<u64>::default();
for addr in func_old.iter().map(|f| f.as_ref().unwrap().address) {
*old_addresses.entry(addr).or_insert(0) += 1;
old_keys.insert(addr);
}
let mut new_addresses = HashMap::<u64, u32>::default();
let mut new_keys = HashSet::<u64>::default();
for addr in func_new.iter().map(|f| f.as_ref().unwrap().address) {
*new_addresses.entry(addr).or_insert(0) += 1;
new_keys.insert(addr);
}
let diff: Vec<_> = old_keys.symmetric_difference(&new_keys).collect();
for &addr in diff.iter() {
old_addresses.remove(addr);
new_addresses.remove(addr);
}
let mut diff_value = Vec::new();
for (addr, value) in old_addresses.iter() {
let new_value = new_addresses.get(addr).unwrap();
if new_value != value {
diff_value.push((addr, new_value, value));
}
}
let diff: Vec<_> = diff.iter().map(|x| format!("0x{x:x}")).collect();
let values: Vec<_> = diff_value
.iter()
.map(|(a, n, o)| format!("At 0x{a:x}: new {n}, old: {o}"))
.collect();
panic!(
"Not the same number of FUNC (new: {}, old: {}):\n - Diff keys: {:?}\n - Diff values: {:?}",
func_new.len(),
func_old.len(),
diff,
values,
);
}
}
fn check_func(
pos: usize,
new: &BreakpadFuncRecord,
old: &BreakpadFuncRecord,
file_map_new: &BreakpadFileMap,
file_map_old: &BreakpadFileMap,
flags: &TestFlags,
) {
assert_eq!(
new.address,
old.address,
"Not the same address for FUNC at position {}",
pos + 1
);
if !flags.intersects(TestFlags::NO_MULTIPLICITY) {
assert_eq!(
new.multiple, old.multiple,
"Not the same multiplicity for FUNC at rva {:x}",
new.address
);
}
assert_eq!(
new.size, old.size,
"Not the same size for FUNC at rva {:x}",
new.address
);
assert_eq!(
new.parameter_size, old.parameter_size,
"Not the same parameter size for FUNC at rva {:x}",
new.address
);
assert_eq!(
new.name, old.name,
"Not the same name for FUNC at rva {:x}",
new.address
);
let line_old = clean_old_lines(old);
let line_new = new.lines();
assert_eq!(
line_new.clone().count(),
line_old.len(),
"Not the same number of lines for FUNC at rva {:x}",
new.address
);
for (i, (line_n, line_o)) in line_new.zip(line_old.iter()).enumerate() {
let line_n = line_n.unwrap();
assert_eq!(
line_n.address,
line_o.address,
"Not the same address for line at position {} in FUNC at rva {:x}",
i + 1,
new.address
);
if flags.intersects(TestFlags::NO_FILE_LINE) {
continue;
}
if i < line_old.len() - 1 {
assert_eq!(
line_n.size,
line_o.size,
"Not the same size for line at position {} in FUNC at rva {:x}",
i + 1,
new.address
);
}
assert_eq!(
line_n.line,
line_o.line,
"Not the same line number for line at position {} in FUNC at rva {:x}",
i + 1,
new.address
);
assert_eq!(
file_map_new.get(&line_n.file_id),
file_map_old.get(&line_o.file_id),
"Not the same file for line at position {} in FUNC at rva {:x}",
i + 1,
new.address
);
}
}
fn test_file(name: &str, flags: TestFlags) {
let (out, name) = if name.starts_with("https://") {
get_data_from_server(name)
} else {
let dll = name.to_string() + ".dll";
(get_new_bp(&dll, None), name)
};
let new = BreakpadObject::parse(&out).unwrap();
let out = get_data(name);
let old = BreakpadObject::parse(&out).unwrap();
check_headers(&new, &old);
let file_map_old = old.file_map();
let file_map_new = new.file_map();
let files_old: HashSet<_> = file_map_old.values().collect();
let files_new: HashSet<_> = file_map_new.values().collect();
for old_file in &files_old {
assert!(
files_new.contains(old_file),
"{}",
"Missing path: {old_file}"
);
}
for new_file in &files_new {
assert!(files_old.contains(new_file), "{}", "Extra path: {new_file}");
}
let mut func_old: Vec<_> = old.func_records().collect();
let mut func_new: Vec<_> = new.func_records().collect();
func_old.sort_by_key(|f| f.as_ref().unwrap().address);
func_new.sort_by_key(|f| f.as_ref().unwrap().address);
if !flags.intersects(TestFlags::NO_FUNCS_LENGTH) {
check_func_len(&func_new, &func_old);
}
for (i, (func_n, func_o)) in func_new.iter().zip(func_old.iter()).enumerate() {
let func_n = func_n.as_ref().unwrap();
let func_o = func_o.as_ref().unwrap();
check_func(i, func_n, func_o, &file_map_new, &file_map_old, &flags);
}
let public_old = old.public_records();
let public_old = public_old.filter(|x| {
let x = x.as_ref().unwrap();
!x.name.contains("::FNODOBFM::`string'")
});
let public_new = new.public_records();
assert_eq!(
public_new.clone().count(),
public_old.clone().count(),
"Not the same number of PUBLIC"
);
for (i, (public_n, public_o)) in public_new.zip(public_old).enumerate() {
let public_n = public_n.unwrap();
let public_o = public_o.unwrap();
assert_eq!(
public_n.address,
public_o.address,
"Not the same address for PUBLIC at position {} ({})",
i + 1,
public_n.name
);
if !flags.intersects(TestFlags::NO_MULTIPLICITY) {
assert_eq!(
public_n.multiple, public_o.multiple,
"Not the same multiplicity for PUBLIC at rva {:x}",
public_n.address
);
}
assert_eq!(
public_n.parameter_size, public_o.parameter_size,
"Not the same parameter size for PUBLIC at rva {:x}",
public_n.address
);
}
}
#[test]
fn test_basic32() {
test_file("basic32", TestFlags::ALL);
}
#[test]
fn test_basic32_dwarf() {
test_file("basic32-dwarf", TestFlags::ALL);
}
#[test]
fn test_basic32_min() {
test_file("basic32-min", TestFlags::ALL);
}
#[test]
fn test_basic64() {
test_file("basic64", TestFlags::ALL);
}
#[test]
fn test_basic64_dwarf() {
test_file("basic64-dwarf", TestFlags::ALL);
}
#[test]
fn test_basic_opt32() {
test_file("basic-opt32", TestFlags::ALL);
}
#[test]
fn test_basic_opt64() {
test_file("basic-opt64", TestFlags::ALL);
}
#[test]
fn test_dump_syms_regtest64() {
test_file("dump_syms_regtest64", TestFlags::ALL);
}
#[test]
fn test_mozwer() {
test_file("mozwer", TestFlags::ALL);
}
#[test]
fn test_ntdll() {
test_file(
&format!("{MS}/ntdll.dll/5D6AA5581AD000/ntdll.dll"),
TestFlags::NO_MULTIPLICITY,
);
}
#[test]
fn test_oleaut32() {
test_file(
&format!("{MS}/oleaut32.dll/BCDE805BC4000/oleaut32.dll"),
TestFlags::NO_MULTIPLICITY,
);
}
#[test]
fn test_win_mapping() {
let mapping = PathMappings::new(
&Some(vec!["rev=abcdef"]),
&Some(vec![r"d:\\agent\\_work\\3\\s\\src\\(.*)"]),
&Some(vec!["https://source/{rev}/{1}"]),
&None,
)
.unwrap();
let dll = "basic32.dll";
let output = get_new_bp(dll, mapping.map(Arc::new));
let bp = BreakpadObject::parse(&output).unwrap();
let map = bp.file_map();
let files: Vec<_> = map.values().collect();
assert_eq!(
files[6].replace('\\', "/"),
"https://source/abcdef/vctools/crt/vcstartup/src/eh/i386/secchk.c"
);
assert_eq!(
files[7].replace('\\', "/"),
"https://source/abcdef/vctools/crt/vcstartup/src/heap/delete_scalar_size.cpp"
);
assert_eq!(
files[files.len() - 1].replace('\\', "/"),
"https://source/abcdef/vctools/crt/vcruntime/src/string/i386/memcmp.c"
);
}
}