use std::collections::HashMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use pdb::FallibleIterator as _;
use crate::discovery::{self, ResolveOutcome};
use kernal_api::symbolize::wire::{DiscoveryConfig, DiscoverySource, ModuleRef};
pub struct SymbolTable {
entries: Vec<(u32, String)>,
}
impl SymbolTable {
pub fn for_image(image: &Path) -> Option<Self> {
match locate(image, &search_dirs()) {
Located::Found(path) => Self::from_pdb(&path),
_ => None,
}
}
pub fn from_pdb(pdb_path: &Path) -> Option<Self> {
load_pdb(pdb_path).map(|(_, table)| table)
}
fn from_open_pdb(mut pdb: pdb::PDB<'_, File>) -> Option<Self> {
let address_map = pdb.address_map().ok()?;
let mut entries: Vec<(u32, String)> = Vec::new();
if let Ok(symbols) = pdb.global_symbols() {
let mut iter = symbols.iter();
while let Ok(Some(symbol)) = iter.next() {
if let Ok(pdb::SymbolData::Public(data)) = symbol.parse() {
if !data.function {
continue;
}
if let Some(rva) = data.offset.to_rva(&address_map) {
entries.push((rva.0, data.name.to_string().into_owned()));
}
}
}
}
if entries.is_empty() {
return None;
}
entries.sort_unstable_by_key(|(rva, _)| *rva);
entries.dedup_by_key(|(rva, _)| *rva);
Some(Self { entries })
}
pub fn lookup(&self, relative_address: u64) -> Option<&str> {
let target = u32::try_from(relative_address).ok()?;
let index = match self.entries.binary_search_by_key(&target, |(rva, _)| *rva) {
Ok(exact) => exact,
Err(0) => return None,
Err(next) => next - 1,
};
Some(self.entries[index].1.as_str())
}
pub fn symbol_containing_name(&self, needle: &str) -> Option<(u32, String)> {
self.entries
.iter()
.find(|(_, name)| name.contains(needle))
.cloned()
}
pub fn addresses_for_names_containing(&self, needle: &str, limit: usize) -> Vec<u64> {
self.entries
.iter()
.filter(|(_, name)| name.contains(needle))
.map(|(rva, _)| u64::from(*rva))
.take(limit)
.collect()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
pub enum ModuleSymbols {
Found {
table: SymbolTable,
symbol_file: String,
source: DiscoverySource,
retained: Option<tempfile::TempPath>,
},
NotFound,
Mismatched {
rejected: usize,
},
NoDebugDirectory,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DebugId {
pub guid: [u8; 16],
pub age: u32,
}
struct ImageDebugInfo {
identity: DebugId,
pdb_name: PathBuf,
}
fn image_debug_info(image: &Path) -> Option<ImageDebugInfo> {
use object::Object as _;
use std::io::Read as _;
let file = File::open(image).ok()?;
let mut bytes = Vec::new();
file.take(discovery::MAX_SYMBOL_BYTES + 1)
.read_to_end(&mut bytes)
.ok()?;
if bytes.len() as u64 > discovery::MAX_SYMBOL_BYTES {
return None;
}
let file = object::File::parse(&*bytes).ok()?;
let cv = file.pdb_info().ok()??;
let recorded = String::from_utf8_lossy(cv.path());
let pdb_name = recorded
.rsplit(['/', '\\'])
.find(|part| !part.is_empty())
.filter(|name| {
*name != "."
&& *name != ".."
&& !name.chars().any(|character| {
matches!(
character,
'<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*'
)
})
})
.map(PathBuf::from)?;
Some(ImageDebugInfo {
identity: DebugId {
guid: guid_pe_to_rfc4122(cv.guid()),
age: cv.age(),
},
pdb_name,
})
}
pub fn image_debug_id(image: &Path) -> Option<DebugId> {
Some(image_debug_info(image)?.identity)
}
impl DebugId {
pub fn canonical(self) -> String {
let guid = self
.guid
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
format!("pdb:{guid}-{}", self.age)
}
pub fn parse(text: &str) -> Option<Self> {
let (guid, age) = text.strip_prefix("pdb:")?.rsplit_once('-')?;
if guid.len() != 32 {
return None;
}
let mut bytes = [0_u8; 16];
for (index, slot) in bytes.iter_mut().enumerate() {
*slot = u8::from_str_radix(&guid[index * 2..index * 2 + 2], 16).ok()?;
}
Some(Self {
guid: bytes,
age: age.parse().ok()?,
})
}
}
fn guid_pe_to_rfc4122(mut raw: [u8; 16]) -> [u8; 16] {
raw[0..4].reverse();
raw[4..6].reverse();
raw[6..8].reverse();
raw
}
fn pdb_debug_id(pdb_path: &Path) -> Option<DebugId> {
if std::fs::metadata(pdb_path).ok()?.len() > discovery::MAX_SYMBOL_BYTES {
return None;
}
let file = File::open(pdb_path).ok()?;
let mut pdb = pdb::PDB::open(file).ok()?;
let info = pdb.pdb_information().ok()?;
Some(DebugId {
guid: *info.guid.as_bytes(),
age: info.age,
})
}
fn load_pdb(pdb_path: &Path) -> Option<(DebugId, SymbolTable)> {
let file = File::open(pdb_path).ok()?;
if file.metadata().ok()?.len() > discovery::MAX_SYMBOL_BYTES {
return None;
}
let mut pdb = pdb::PDB::open(file).ok()?;
let identity = {
let info = pdb.pdb_information().ok()?;
DebugId {
guid: *info.guid.as_bytes(),
age: info.age,
}
};
let table = SymbolTable::from_open_pdb(pdb)?;
Some((identity, table))
}
pub fn discover_module(module: &ModuleRef, config: &DiscoveryConfig) -> ModuleSymbols {
if module.path_hint.is_none() && module.debug_id.is_none() {
return ModuleSymbols::NotFound;
}
if !crate::discovery::captured_image_still_matches(module) {
return ModuleSymbols::Mismatched { rejected: 1 };
}
let image_info = module
.path_hint
.as_deref()
.and_then(|path| image_debug_info(Path::new(path)));
let declared_identity = module.debug_id.as_deref().and_then(DebugId::parse);
let Some(expected) =
declared_identity.or_else(|| image_info.as_ref().map(|info| info.identity))
else {
return ModuleSymbols::NoDebugDirectory;
};
let symbol_file_name = module
.debug_file
.as_deref()
.map(PathBuf::from)
.or_else(|| image_info.map(|info| info.pdb_name))
.unwrap_or_else(|| PathBuf::from(&module.name).with_extension("pdb"));
let Some(symbol_file_name) = symbol_file_name.file_name().map(PathBuf::from) else {
return ModuleSymbols::NotFound;
};
let mut verified_table = None;
let local = discovery::resolve_symbols(
module,
config,
&expected.canonical(),
crate::discovery::SymbolArtifactFormat::Pdb,
&symbol_file_name,
&search_dirs(),
|candidate| {
let Some((actual, table)) = load_pdb(candidate) else {
return false;
};
if !identity_matches(expected, actual) {
return false;
}
verified_table = Some(table);
true
},
);
match local {
ResolveOutcome::Found(resolved) => {
let Some(table) = verified_table.take() else {
return ModuleSymbols::Mismatched { rejected: 1 };
};
ModuleSymbols::Found {
table,
symbol_file: resolved.path.to_string_lossy().into_owned(),
source: resolved.source,
retained: None,
}
}
ResolveOutcome::NotFound => server_symbols(expected, &symbol_file_name, 0),
ResolveOutcome::Mismatched {
rejected: local_rejected,
} => server_symbols(expected, &symbol_file_name, local_rejected),
}
}
fn server_symbols(
expected: DebugId,
symbol_file_name: &Path,
local_rejected: usize,
) -> ModuleSymbols {
match discovery::resolve_configured_server(&expected.canonical(), symbol_file_name, |path| {
let (actual, table) = load_pdb(path)?;
identity_matches(expected, actual).then_some(table)
}) {
discovery::ServerResolve::Found {
url,
value: table,
retained,
} => ModuleSymbols::Found {
table,
symbol_file: url,
source: DiscoverySource::ConfiguredServer,
retained: Some(retained),
},
discovery::ServerResolve::NotFound if local_rejected == 0 => ModuleSymbols::NotFound,
discovery::ServerResolve::NotFound => ModuleSymbols::Mismatched {
rejected: local_rejected,
},
discovery::ServerResolve::Mismatched { rejected } => ModuleSymbols::Mismatched {
rejected: local_rejected + rejected,
},
}
}
pub fn identity_matches(image: DebugId, pdb: DebugId) -> bool {
image == pdb
}
pub(crate) fn search_dirs() -> Vec<PathBuf> {
parse_search_dirs(std::env::var_os(discovery::SYMBOL_PATH_ENV))
}
fn parse_search_dirs(raw: Option<std::ffi::OsString>) -> Vec<PathBuf> {
let Some(raw) = raw else {
return Vec::new();
};
std::env::split_paths(&raw)
.filter(|p| !p.as_os_str().is_empty())
.collect()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Located {
Found(PathBuf),
NotFound,
Mismatched {
rejected: usize,
},
NoDebugDirectory,
}
pub fn locate(image: &Path, extra: &[PathBuf]) -> Located {
let Some(expected) = image_debug_id(image) else {
return Located::NoDebugDirectory;
};
let Some(file_name) = image
.with_extension("pdb")
.file_name()
.map(|n| n.to_owned())
else {
return Located::NotFound;
};
let beside = image.with_extension("pdb");
let candidates = std::iter::once(beside).chain(extra.iter().map(|dir| dir.join(&file_name)));
let mut rejected = 0usize;
for candidate in candidates {
if !candidate.is_file() {
continue;
}
match pdb_debug_id(&candidate) {
Some(actual) if identity_matches(expected, actual) => return Located::Found(candidate),
_ => rejected += 1,
}
}
if rejected == 0 {
Located::NotFound
} else {
Located::Mismatched { rejected }
}
}
pub fn pdb_path_for_with_search(image: &Path, extra: &[PathBuf]) -> Option<PathBuf> {
match locate(image, extra) {
Located::Found(path) => Some(path),
_ => None,
}
}
pub fn resolve_lines(pdb_path: &Path, addresses: &[u64]) -> HashMap<u64, (String, u32)> {
let mut resolved = HashMap::new();
if addresses.is_empty() {
return resolved;
}
let Ok(file) = File::open(pdb_path) else {
return resolved;
};
let Ok(pdb) = pdb_addr2line::pdb::PDB::open(file) else {
return resolved;
};
let Ok(data) = pdb_addr2line::ContextPdbData::try_from_pdb(pdb) else {
return resolved;
};
let Ok(context) = data.make_context() else {
return resolved;
};
for &address in addresses {
let Ok(probe) = u32::try_from(address) else {
continue;
};
let Ok(Some(frames)) = context.find_frames(probe) else {
continue;
};
let Some(frame) = frames.frames.first() else {
continue;
};
let (Some(file), Some(line)) = (frame.file.as_ref(), frame.line) else {
continue;
};
resolved.insert(address, (file.to_string(), line));
}
resolved
}
#[cfg(test)]
mod tests {
use super::*;
fn own_pdb() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
let candidate = exe.with_extension("pdb");
if candidate.is_file() {
return Some(candidate);
}
assert!(
std::env::var_os("GITHUB_ACTIONS").is_none(),
"no PDB at {} during a CI run; the symbol tests would skip and assert nothing",
candidate.display()
);
None
}
#[test]
fn a_real_pdb_yields_file_and_line_for_this_crates_code() {
let Some(pdb_path) = own_pdb() else {
eprintln!("skipping: no PDB beside the test binary");
return;
};
let Some(table) = SymbolTable::from_pdb(&pdb_path) else {
eprintln!("skipping: PDB had no public function symbols");
return;
};
let addresses: Vec<u64> = table
.entries
.iter()
.filter(|(_, name)| name.contains("kernal_api"))
.map(|(rva, _)| u64::from(*rva))
.take(64)
.collect();
assert!(
!addresses.is_empty(),
"no symbol named this crate; the anchor is wrong, not the resolver"
);
let resolved = resolve_lines(&pdb_path, &addresses);
assert!(
!resolved.is_empty(),
"none of {} of this crate's symbols resolved to a line",
addresses.len()
);
for (file, line) in resolved.values() {
assert!(
file.to_ascii_lowercase().ends_with(".rs"),
"resolved to {file}, which is not a Rust source file"
);
assert!(*line > 0, "line numbers are 1-based; got {line}");
}
}
#[test]
fn no_addresses_means_no_work_and_no_answers() {
let Some(pdb_path) = own_pdb() else {
eprintln!("skipping: no PDB beside the test binary");
return;
};
assert!(resolve_lines(&pdb_path, &[]).is_empty());
}
#[test]
fn an_address_too_large_for_an_rva_has_no_line() {
let Some(pdb_path) = own_pdb() else {
eprintln!("skipping: no PDB beside the test binary");
return;
};
let huge = u64::from(u32::MAX) + 1;
assert!(resolve_lines(&pdb_path, &[huge]).is_empty());
}
#[test]
fn a_missing_pdb_yields_no_lines_rather_than_failing() {
let dir = tempfile::tempdir().expect("tempdir");
let absent = dir.path().join("nothing.pdb");
assert!(resolve_lines(&absent, &[0x1000]).is_empty());
}
#[test]
fn a_real_pdb_yields_symbols() {
let Some(pdb_path) = own_pdb() else {
eprintln!("skipping: no PDB beside the test binary");
return;
};
let table = SymbolTable::from_pdb(&pdb_path).expect("the test binary's PDB has symbols");
assert!(
!table.is_empty(),
"the test binary PDB should list functions"
);
}
#[test]
fn debug_identity_canonical_form_round_trips() {
let identity = DebugId {
guid: [0xAB; 16],
age: 17,
};
assert_eq!(DebugId::parse(&identity.canonical()), Some(identity));
assert_eq!(
identity.canonical(),
"pdb:abababababababababababababababab-17"
);
}
#[test]
fn symbol_addresses_fall_inside_executable_sections() {
use object::{Object as _, ObjectSection as _};
let Some(pdb_path) = own_pdb() else {
eprintln!("skipping: no PDB beside the test binary");
return;
};
let Some(table) = SymbolTable::from_pdb(&pdb_path) else {
eprintln!("skipping: PDB had no public function symbols");
return;
};
let exe = std::env::current_exe().expect("current exe");
let bytes = std::fs::read(&exe).expect("read own image");
let file = object::File::parse(&*bytes).expect("parse own PE");
let base = file.relative_address_base();
let ranges: Vec<(u64, u64)> = file
.sections()
.filter(|s| match s.flags() {
object::SectionFlags::Coff { characteristics } => {
characteristics & 0x2000_0000 != 0
}
_ => false,
})
.map(|s| {
let start = s.address().saturating_sub(base);
(start, start + s.size())
})
.collect();
assert!(!ranges.is_empty(), "the PE should have executable sections");
let outside: Vec<_> = table
.entries
.iter()
.filter(|(rva, _)| {
let rva = u64::from(*rva);
!ranges
.iter()
.any(|(start, end)| rva >= *start && rva < *end)
})
.take(5)
.collect();
assert!(
outside.is_empty(),
"function symbols outside every executable section {ranges:?}: {outside:?} — the PDB address map is likely not being applied",
);
}
fn id(guid_byte: u8, age: u32) -> DebugId {
DebugId {
guid: [guid_byte; 16],
age,
}
}
#[test]
fn the_pe_guid_is_reordered_field_wise() {
let pe = [
0xF9, 0xE2, 0xA7, 0xBC, 0x1D, 0xCF, 0x63, 0x4A, 0x86, 0xA8, 0xD1, 0xE6, 0xD2, 0x8B,
0xB3, 0xD0,
];
let expected = [
0xBC, 0xA7, 0xE2, 0xF9, 0xCF, 0x1D, 0x4A, 0x63, 0x86, 0xA8, 0xD1, 0xE6, 0xD2, 0x8B,
0xB3, 0xD0,
];
assert_eq!(guid_pe_to_rfc4122(pe), expected);
}
#[test]
fn the_guid_conversion_is_its_own_inverse() {
let pe = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10,
];
assert_eq!(guid_pe_to_rfc4122(guid_pe_to_rfc4122(pe)), pe);
}
#[test]
fn an_exact_identity_matches() {
assert!(identity_matches(id(0xAB, 3), id(0xAB, 3)));
}
#[test]
fn a_different_guid_never_matches() {
assert!(!identity_matches(id(0xAB, 3), id(0xCD, 3)));
assert!(!identity_matches(id(0xAB, 3), id(0xCD, 99)));
}
#[test]
fn a_higher_pdb_age_does_not_match() {
assert!(!identity_matches(id(0xAB, 3), id(0xAB, 4)));
}
#[test]
fn a_lower_pdb_age_does_not_match() {
assert!(
!identity_matches(id(0xAB, 5), id(0xAB, 4)),
"a PDB older than the image cannot describe it"
);
}
#[test]
fn a_binary_matches_its_own_pdb() {
let Some(pdb_path) = own_pdb() else {
eprintln!("skipping: no PDB beside the test binary");
return;
};
let exe = std::env::current_exe().expect("current exe");
let Some(image) = image_debug_id(&exe) else {
panic!("the test binary has no CodeView debug directory");
};
let pdb = pdb_debug_id(&pdb_path).expect("the PDB has an identity");
assert_eq!(
image.guid, pdb.guid,
"the image and its own PDB disagree on the GUID; byte order is wrong"
);
assert!(
identity_matches(image, pdb),
"image {image:?} did not match its own pdb {pdb:?}"
);
}
#[test]
fn the_sibling_pdb_of_this_binary_is_accepted() {
if own_pdb().is_none() {
eprintln!("skipping: no PDB beside the test binary");
return;
}
let exe = std::env::current_exe().expect("current exe");
assert!(
pdb_path_for_with_search(&exe, &[]).is_some(),
"the binary's own sibling PDB should pass identity verification"
);
}
#[test]
fn a_sibling_pdb_from_a_different_build_is_rejected() {
let Some(real) = own_pdb() else {
eprintln!("skipping: no PDB beside the test binary");
return;
};
let dir = tempfile::tempdir().expect("tempdir");
let fake_exe = dir.path().join("other.exe");
std::fs::copy(std::env::current_exe().unwrap(), &fake_exe).expect("copy exe");
std::fs::write(
dir.path().join("other.pdb"),
&std::fs::read(&real).unwrap()[..64],
)
.expect("write pdb");
assert!(
pdb_path_for_with_search(&fake_exe, &[]).is_none(),
"a PDB whose identity cannot be verified must not be accepted"
);
}
#[test]
fn an_absent_symbol_path_yields_no_directories() {
assert!(parse_search_dirs(None).is_empty());
}
#[test]
fn empty_entries_are_dropped_rather_than_searching_the_cwd() {
let sep = if cfg!(windows) { ";" } else { ":" };
let raw = format!("{sep}{sep}");
assert!(
parse_search_dirs(Some(raw.into())).is_empty(),
"empty entries must not become a search of the working directory"
);
}
#[test]
fn directories_are_split_and_ordered() {
let sep = if cfg!(windows) { ";" } else { ":" };
let raw = format!("first{sep}second{sep}third");
let dirs = parse_search_dirs(Some(raw.into()));
assert_eq!(
dirs,
vec![
PathBuf::from("first"),
PathBuf::from("second"),
PathBuf::from("third")
],
"order matters: the search takes the first verified match"
);
}
#[test]
fn a_search_directory_supplies_a_missing_pdb() {
let Some(real) = own_pdb() else {
eprintln!("skipping: no PDB beside the test binary");
return;
};
let exe = std::env::current_exe().expect("current exe");
let dir = tempfile::tempdir().expect("tempdir");
let lonely_exe = dir.path().join("lonely.exe");
std::fs::copy(&exe, &lonely_exe).expect("copy exe");
let store = tempfile::tempdir().expect("store");
std::fs::copy(&real, store.path().join("lonely.pdb")).expect("copy pdb");
assert!(
pdb_path_for_with_search(&lonely_exe, &[]).is_none(),
"with no search path there is nothing beside the image to find"
);
let found = pdb_path_for_with_search(&lonely_exe, &[store.path().to_path_buf()])
.expect("the search directory should supply it");
assert_eq!(found, store.path().join("lonely.pdb"));
}
#[test]
fn discovery_uses_the_codeview_pdb_basename_not_the_image_stem() {
let Some(real) = own_pdb() else {
eprintln!("skipping: no PDB beside the test binary");
return;
};
let exe = std::env::current_exe().expect("current exe");
let dir = tempfile::tempdir().expect("tempdir");
let renamed_exe = dir.path().join("renamed-image.exe");
std::fs::copy(&exe, &renamed_exe).expect("copy exe");
let store = tempfile::tempdir().expect("store");
let recorded_name = image_debug_info(&renamed_exe).expect("CodeView").pdb_name;
assert_ne!(recorded_name, PathBuf::from("renamed-image.pdb"));
std::fs::copy(&real, store.path().join(&recorded_name)).expect("copy recorded PDB");
let module = ModuleRef {
name: "renamed-image.exe".into(),
path_hint: Some(renamed_exe.to_string_lossy().into_owned()),
..Default::default()
};
let config = DiscoveryConfig {
registered_symbol_paths: vec![store.path().to_string_lossy().into_owned()],
..Default::default()
};
assert!(matches!(
discover_module(&module, &config),
ModuleSymbols::Found {
source: DiscoverySource::Registration,
..
}
));
}
#[test]
fn a_search_directory_candidate_is_still_identity_checked() {
let Some(real) = own_pdb() else {
eprintln!("skipping: no PDB beside the test binary");
return;
};
let exe = std::env::current_exe().expect("current exe");
let dir = tempfile::tempdir().expect("tempdir");
let lonely_exe = dir.path().join("lonely.exe");
std::fs::copy(&exe, &lonely_exe).expect("copy exe");
let store = tempfile::tempdir().expect("store");
let bytes = std::fs::read(&real).expect("read pdb");
std::fs::write(store.path().join("lonely.pdb"), &bytes[..64]).expect("write");
assert!(
pdb_path_for_with_search(&lonely_exe, &[store.path().to_path_buf()]).is_none(),
"a candidate whose identity cannot be verified must not be accepted"
);
}
#[test]
fn a_bad_candidate_does_not_abort_the_search() {
let Some(real) = own_pdb() else {
eprintln!("skipping: no PDB beside the test binary");
return;
};
let exe = std::env::current_exe().expect("current exe");
let dir = tempfile::tempdir().expect("tempdir");
let lonely_exe = dir.path().join("lonely.exe");
std::fs::copy(&exe, &lonely_exe).expect("copy exe");
let bad = tempfile::tempdir().expect("bad");
let bytes = std::fs::read(&real).expect("read pdb");
std::fs::write(bad.path().join("lonely.pdb"), &bytes[..64]).expect("write bad");
let good = tempfile::tempdir().expect("good");
std::fs::copy(&real, good.path().join("lonely.pdb")).expect("copy good");
let found = pdb_path_for_with_search(
&lonely_exe,
&[bad.path().to_path_buf(), good.path().to_path_buf()],
)
.expect("the good candidate later in the path should be found");
assert_eq!(found, good.path().join("lonely.pdb"));
}
#[test]
fn the_image_directory_is_searched_first() {
let Some(real) = own_pdb() else {
eprintln!("skipping: no PDB beside the test binary");
return;
};
let exe = std::env::current_exe().expect("current exe");
let store = tempfile::tempdir().expect("store");
std::fs::copy(
&real,
store
.path()
.join(exe.with_extension("pdb").file_name().unwrap()),
)
.expect("copy pdb");
let found = pdb_path_for_with_search(&exe, &[store.path().to_path_buf()])
.expect("the sibling PDB should be found");
assert_eq!(found, exe.with_extension("pdb"), "the sibling must win");
}
#[test]
fn a_missing_pdb_yields_no_table() {
assert!(SymbolTable::from_pdb(Path::new("no-such-file.pdb")).is_none());
}
#[test]
fn a_corrupt_pdb_yields_no_table() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("broken.pdb");
std::fs::write(&path, [0xFFu8; 8192]).expect("write");
assert!(SymbolTable::from_pdb(&path).is_none());
}
#[test]
fn an_address_below_every_symbol_resolves_to_nothing() {
let table = SymbolTable {
entries: vec![(0x1000, "alpha".into()), (0x2000, "bravo".into())],
};
assert_eq!(
table.lookup(0x0FFF),
None,
"must not claim the first symbol"
);
}
#[test]
fn an_address_inside_a_function_resolves_to_it() {
let table = SymbolTable {
entries: vec![(0x1000, "alpha".into()), (0x2000, "bravo".into())],
};
assert_eq!(table.lookup(0x1000), Some("alpha"), "exact start");
assert_eq!(table.lookup(0x1234), Some("alpha"), "inside alpha");
assert_eq!(table.lookup(0x2000), Some("bravo"), "exact start of bravo");
assert_eq!(table.lookup(0x9999), Some("bravo"));
}
#[test]
fn an_address_too_large_for_an_rva_resolves_to_nothing() {
let table = SymbolTable {
entries: vec![(0x1000, "alpha".into())],
};
assert_eq!(table.lookup(u64::from(u32::MAX) + 1), None);
}
#[test]
fn a_real_pdb_yields_recognizable_symbol_names() {
let Some(pdb_path) = own_pdb() else {
eprintln!("skipping: no PDB beside the test binary");
return;
};
let Some(table) = SymbolTable::from_pdb(&pdb_path) else {
eprintln!("skipping: PDB had no public function symbols");
return;
};
assert!(
table
.entries
.iter()
.any(|(_, name)| name.contains("kernal_api")),
"no symbol mentioned this crate; the {} names read look wrong, e.g. {:?}",
table.len(),
table.entries.iter().take(3).collect::<Vec<_>>()
);
}
#[test]
fn every_symbol_resolves_to_itself_at_its_own_address() {
let Some(pdb_path) = own_pdb() else {
eprintln!("skipping: no PDB beside the test binary");
return;
};
let Some(table) = SymbolTable::from_pdb(&pdb_path) else {
eprintln!("skipping: PDB had no public function symbols");
return;
};
for (rva, expected) in table.entries.iter().step_by(97) {
assert_eq!(
table.lookup(u64::from(*rva)),
Some(expected.as_str()),
"symbol at {rva:#x} did not resolve to itself"
);
}
}
}