use std::collections::{BTreeMap, HashMap, HashSet};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::debug_info::DebugInfoBuilder;
use crate::dwarf::{
AbbrevTable, DW_AT_abstract_origin, DW_AT_call_file, DW_AT_call_line, DW_AT_comp_dir,
DW_AT_decl_file, DW_AT_decl_line, DW_AT_external, DW_AT_frame_base, DW_AT_high_pc,
DW_AT_inline, DW_AT_linkage_name, DW_AT_location, DW_AT_low_pc, DW_AT_name, DW_AT_producer,
DW_AT_specification, DW_AT_stmt_list, DW_AT_type, DW_CHILDREN_no, DW_CHILDREN_yes,
DW_FORM_addr, DW_FORM_data1, DW_FORM_data2, DW_FORM_data4, DW_FORM_data8, DW_FORM_exprloc,
DW_FORM_flag, DW_FORM_flag_present, DW_FORM_ref4, DW_FORM_ref_addr, DW_FORM_sec_offset,
DW_FORM_string, DW_FORM_strp, DW_INL_inlined, DW_INL_not_inlined, DW_TAG_base_type,
DW_TAG_compile_unit, DW_TAG_const_type, DW_TAG_formal_parameter, DW_TAG_inlined_subroutine,
DW_TAG_lexical_block, DW_TAG_member, DW_TAG_namespace, DW_TAG_pointer_type,
DW_TAG_structure_type, DW_TAG_subprogram, DW_TAG_typedef, DW_TAG_variable,
DW_TAG_volatile_type, DwarfAttribute, DwarfEmitter, LineProgram, SourceDirectory, SourceFile,
DIE,
};
use crate::pdb::{
pdb_codeview::{CodeViewType, CodeViewTypeRecord, LeafType},
pdb_dbi::{DbiModuleInfo, DbiStream, SectionContrib},
pdb_msf::{MSFStream, MSFSuperBlock},
pdb_reader::PdbReader,
pdb_symbols::{CodeViewSymbol, LineInfo, SymbolRecordKind},
pdb_writer::PdbWriter,
PDB_BLOCK_SIZE, PDB_MAGIC,
};
pub const MAX_SYMBOL_CACHE_SIZE: u64 = 256 * 1024 * 1024;
pub const DEFAULT_SYMBOL_SERVER_TIMEOUT_MS: u64 = 30_000;
pub const GUID_STRING_LENGTH: usize = 36;
pub const MAX_SYMBOL_SOURCE_CHAIN: usize = 8;
pub const SYMBOL_SERVER_USER_AGENT: &str = "Microsoft-Symbol-Server/10.0.0.0";
pub const BREAKPAD_MODULE_MAGIC: &str = "MODULE";
pub const BREAKPAD_INFO_CODE_ID: &str = "CODE_ID";
pub const BREAKPAD_INFO_DEBUG_ID: &str = "DEBUG_ID";
pub const BREAKPAD_FUNC_MARKER: &str = "FUNC";
pub const BREAKPAD_PUBLIC_MARKER: &str = "PUBLIC";
pub const BREAKPAD_STACK_WIN_MARKER: &str = "STACK WIN";
pub const BREAKPAD_STACK_CFI_MARKER: &str = "STACK CFI";
pub const BREAKPAD_STACK_CFI_INIT: &str = "STACK CFI INIT";
pub const BREAKPAD_STACK_CFI_EXPR: &str = "STACK CFI EXPR";
pub const IMAGE_DEBUG_TYPE_UNKNOWN: u32 = 0;
pub const IMAGE_DEBUG_TYPE_COFF: u32 = 1;
pub const IMAGE_DEBUG_TYPE_CODEVIEW: u32 = 2;
pub const IMAGE_DEBUG_TYPE_FPO: u32 = 3;
pub const IMAGE_DEBUG_TYPE_MISC: u32 = 4;
pub const IMAGE_DEBUG_TYPE_EXCEPTION: u32 = 5;
pub const IMAGE_DEBUG_TYPE_FIXUP: u32 = 6;
pub const IMAGE_DEBUG_TYPE_BORLAND: u32 = 9;
pub const CV_SIGNATURE_NB10: u32 = 0x3031424E;
pub const CV_SIGNATURE_RSDS: u32 = 0x53445352;
pub const CV_SIGNATURE_NB11: u32 = 0x3131424E;
pub const PDB20_AGE: u32 = 1;
pub const MAX_INLINE_DEPTH: usize = 64;
pub const TRANSACTION_FILE_SUFFIX: &str = "deleteme";
pub const SRCSRV_STREAM_NAME: &str = "srcsrv";
pub const SRCSRV_VAR_SOURCE_ROOT: &str = "%SRCSRV_SOURCE_ROOT%";
pub const SRCSRV_VAR_TARGET_ROOT: &str = "%SRCSRV_TARGET_ROOT%";
pub const SRCSRV_VAR_FILE_NAME: &str = "%SRCSRV_FILE_NAME%";
pub const SRCSRV_VAR_VAR2: &str = "%SRCSRV_VAR2%";
pub fn format_guid(data: &[u8]) -> String {
if data.len() < 16 {
return "00000000-0000-0000-0000-000000000000".to_string();
}
format!(
"{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}",
u32::from_le_bytes([data[0], data[1], data[2], data[3]]),
u16::from_le_bytes([data[4], data[5]]),
u16::from_le_bytes([data[6], data[7]]),
data[8],
data[9],
data[10],
data[11],
data[12],
data[13],
data[14],
data[15],
)
}
pub fn format_hex(data: &[u8]) -> String {
data.iter()
.map(|b| format!("{:02X}", b))
.collect::<Vec<_>>()
.join("")
}
pub fn parse_guid(s: &str) -> Option<[u8; 16]> {
let clean: String = s.chars().filter(|c| c.is_ascii_hexdigit()).collect();
if clean.len() != 32 {
return None;
}
let mut bytes = [0u8; 16];
for i in 0..16 {
bytes[i] = u8::from_str_radix(&clean[i * 2..i * 2 + 2], 16).ok()?;
}
Some(bytes)
}
pub fn fnv1a_32(data: &[u8]) -> u32 {
let mut hash: u32 = 0x811c9dc5;
for &byte in data {
hash ^= byte as u32;
hash = hash.wrapping_mul(0x01000193);
}
hash
}
pub fn breakpad_debug_id(data: &[u8]) -> String {
if data.len() >= 16 {
let guid = format_guid(&data[..16]);
let age = if data.len() >= 20 {
u32::from_le_bytes([data[16], data[17], data[18], data[19]])
} else {
0
};
format!("{}{:X}", guid.replace('-', ""), age)
} else {
format_hex(data)
}
}
pub fn sha1_to_breakpad_id(sha1: &[u8]) -> String {
if sha1.len() < 20 {
return format_hex(sha1);
}
let guid_part = &sha1[..16];
let age_part = u32::from_le_bytes([sha1[16], sha1[17], sha1[18], sha1[19]]);
format!("{}{:X}", format_hex(guid_part), age_part)
}
pub fn current_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum X86SymbolKind {
Function,
PublicSymbol,
Data,
Label,
Thunk,
InlineFunction,
Unknown,
}
impl Default for X86SymbolKind {
fn default() -> Self {
X86SymbolKind::Unknown
}
}
#[derive(Debug, Clone)]
pub struct X86SymbolInfo {
pub name: String,
pub demangled_name: Option<String>,
pub address: u64,
pub size: u64,
pub kind: X86SymbolKind,
pub source_file: Option<String>,
pub source_line: Option<u32>,
pub module_name: Option<String>,
pub inline_frames: Vec<X86InlineFrame>,
}
impl X86SymbolInfo {
pub fn new(name: &str, address: u64, size: u64, kind: X86SymbolKind) -> Self {
Self {
name: name.to_string(),
demangled_name: None,
address,
size,
kind,
source_file: None,
source_line: None,
module_name: None,
inline_frames: Vec::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct X86InlineFrame {
pub function_name: String,
pub source_file: Option<String>,
pub source_line: Option<u32>,
pub call_file: Option<String>,
pub call_line: Option<u32>,
pub depth: usize,
pub address_range: (u64, u64),
}
impl X86InlineFrame {
pub fn new(name: &str, depth: usize) -> Self {
Self {
function_name: name.to_string(),
source_file: None,
source_line: None,
call_file: None,
call_line: None,
depth,
address_range: (0, 0),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct X86SourceLocation {
pub file_path: String,
pub line: u32,
pub column: Option<u32>,
pub address: u64,
}
impl X86SourceLocation {
pub fn new(file_path: &str, line: u32, address: u64) -> Self {
Self {
file_path: file_path.to_string(),
line,
column: None,
address,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct X86DebugId {
pub id: Vec<u8>,
pub age: u32,
pub kind: X86DebugIdKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum X86DebugIdKind {
PdbGuid,
ElfBuildId,
MachOUuid,
Breakpad,
Unknown,
}
impl X86DebugId {
pub fn new(id: Vec<u8>, age: u32, kind: X86DebugIdKind) -> Self {
Self { id, age, kind }
}
pub fn to_flat_string(&self) -> String {
format!("{}{:X}", format_hex(&self.id), self.age)
}
pub fn to_guid_string(&self) -> String {
if self.id.len() >= 16 {
format_guid(&self.id[..16])
} else {
format_hex(&self.id)
}
}
}
#[derive(Debug, Clone)]
pub struct X86ModuleDescriptor {
pub name: String,
pub debug_id: X86DebugId,
pub image_base: u64,
pub image_size: u64,
pub arch: String,
pub binary_path: Option<PathBuf>,
pub symbol_path: Option<PathBuf>,
pub pdb_path: Option<PathBuf>,
pub codeview_info: Option<X86CodeViewInfo>,
pub elf_build_id: Option<Vec<u8>>,
pub macho_uuid: Option<[u8; 16]>,
}
impl X86ModuleDescriptor {
pub fn new(name: &str, debug_id: X86DebugId, image_base: u64, image_size: u64) -> Self {
Self {
name: name.to_string(),
debug_id,
image_base,
image_size,
arch: "x86_64".to_string(),
binary_path: None,
symbol_path: None,
pdb_path: None,
codeview_info: None,
elf_build_id: None,
macho_uuid: None,
}
}
pub fn pdb_name(&self) -> String {
let base = Path::new(&self.name)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| self.name.clone());
format!("{}.pdb", base)
}
pub fn sym_name(&self) -> String {
let base = Path::new(&self.name)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| self.name.clone());
format!("{}.sym", base)
}
pub fn dbg_name(&self) -> String {
let base = Path::new(&self.name)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| self.name.clone());
format!("{}.dbg", base)
}
}
#[derive(Debug, Clone)]
pub struct X86CodeViewInfo {
pub signature: u32,
pub guid: [u8; 16],
pub age: u32,
pub pdb_path: String,
pub debug_dir_offset: u64,
pub debug_dir_size: u32,
}
impl X86CodeViewInfo {
pub fn new_pdb70(guid: [u8; 16], age: u32, pdb_path: &str) -> Self {
Self {
signature: CV_SIGNATURE_RSDS,
guid,
age,
pdb_path: pdb_path.to_string(),
debug_dir_offset: 0,
debug_dir_size: 0,
}
}
pub fn new_pdb20(timestamp: u32, age: u32, pdb_path: &str) -> Self {
let mut guid = [0u8; 16];
guid[0..4].copy_from_slice(×tamp.to_le_bytes());
guid[4..8].copy_from_slice(&age.to_le_bytes());
Self {
signature: CV_SIGNATURE_NB10,
guid,
age,
pdb_path: pdb_path.to_string(),
debug_dir_offset: 0,
debug_dir_size: 0,
}
}
pub fn guid_string(&self) -> String {
format_guid(&self.guid)
}
pub fn debug_id(&self) -> X86DebugId {
X86DebugId::new(self.guid.to_vec(), self.age, X86DebugIdKind::PdbGuid)
}
}
#[derive(Debug, Clone)]
pub enum X86SymbolSource {
HttpSymbolServer {
url: String,
priority: u32,
timeout_ms: u64,
},
S3Store {
bucket: String,
region: String,
prefix: String,
endpoint: Option<String>,
priority: u32,
},
GcsStore {
bucket: String,
prefix: String,
priority: u32,
},
LocalStore {
root_path: PathBuf,
priority: u32,
},
MemoryCache {
priority: u32,
},
}
impl X86SymbolSource {
pub fn priority(&self) -> u32 {
match self {
X86SymbolSource::HttpSymbolServer { priority, .. } => *priority,
X86SymbolSource::S3Store { priority, .. } => *priority,
X86SymbolSource::GcsStore { priority, .. } => *priority,
X86SymbolSource::LocalStore { priority, .. } => *priority,
X86SymbolSource::MemoryCache { priority, .. } => *priority,
}
}
pub fn description(&self) -> String {
match self {
X86SymbolSource::HttpSymbolServer { url, .. } => format!("HTTP({})", url),
X86SymbolSource::S3Store { bucket, region, .. } => format!("S3({}/{})", bucket, region),
X86SymbolSource::GcsStore { bucket, .. } => format!("GCS({})", bucket),
X86SymbolSource::LocalStore { root_path, .. } => {
format!("Local({})", root_path.display())
}
X86SymbolSource::MemoryCache { .. } => "Memory".to_string(),
}
}
}
#[derive(Debug, Clone)]
struct X86SymbolIndexEntry {
name: String,
address: u64,
size: u64,
kind: X86SymbolKind,
source_file_index: Option<usize>,
source_line: Option<u32>,
module_name_index: Option<usize>,
inline_frame_indices: Vec<usize>,
}
#[derive(Debug, Clone)]
struct X86InlineFrameEntry {
function_name: String,
source_file_index: Option<usize>,
source_line: Option<u32>,
call_file_index: Option<usize>,
call_line: Option<u32>,
depth: usize,
start_address: u64,
end_address: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct X86SourceFileEntry {
path: String,
content_hash: Option<[u8; 32]>,
}
pub struct X86SymbolIndex {
entries: Vec<X86SymbolIndexEntry>,
address_map: BTreeMap<u64, usize>,
name_index: HashMap<String, Vec<usize>>,
file_index: HashMap<String, Vec<usize>>,
source_files: Vec<X86SourceFileEntry>,
inline_frames: Vec<X86InlineFrameEntry>,
module_names: Vec<String>,
is_built: bool,
pub stats: X86SymbolIndexStats,
}
#[derive(Debug, Clone, Default)]
pub struct X86SymbolIndexStats {
pub total_symbols: usize,
pub total_functions: usize,
pub total_publics: usize,
pub total_inline_frames: usize,
pub total_source_files: usize,
pub total_modules: usize,
pub build_time_ms: u64,
}
impl X86SymbolIndex {
pub fn new() -> Self {
Self {
entries: Vec::new(),
address_map: BTreeMap::new(),
name_index: HashMap::new(),
file_index: HashMap::new(),
source_files: Vec::new(),
inline_frames: Vec::new(),
module_names: Vec::new(),
is_built: false,
stats: X86SymbolIndexStats::default(),
}
}
pub fn add_symbol(
&mut self,
name: &str,
address: u64,
size: u64,
kind: X86SymbolKind,
source_file: Option<&str>,
source_line: Option<u32>,
module_name: Option<&str>,
) {
let sf_idx = source_file.map(|f| self.get_or_add_source_file(f, None));
let mn_idx = module_name.map(|m| self.get_or_add_module_name(m));
self.entries.push(X86SymbolIndexEntry {
name: name.to_string(),
address,
size,
kind,
source_file_index: sf_idx,
source_line,
module_name_index: mn_idx,
inline_frame_indices: Vec::new(),
});
}
pub fn add_inline_frame(
&mut self,
function_name: &str,
source_file: Option<&str>,
source_line: Option<u32>,
call_file: Option<&str>,
call_line: Option<u32>,
depth: usize,
start_address: u64,
end_address: u64,
) -> usize {
let sf_idx = source_file.map(|f| self.get_or_add_source_file(f, None));
let cf_idx = call_file.map(|f| self.get_or_add_source_file(f, None));
self.inline_frames.push(X86InlineFrameEntry {
function_name: function_name.to_string(),
source_file_index: sf_idx,
source_line,
call_file_index: cf_idx,
call_line,
depth,
start_address,
end_address,
});
self.inline_frames.len() - 1
}
pub fn set_symbol_inline_frames(&mut self, entry_index: usize, frame_indices: &[usize]) {
if entry_index < self.entries.len() {
self.entries[entry_index].inline_frame_indices = frame_indices.to_vec();
}
}
pub fn build(&mut self) {
let start = current_timestamp();
self.address_map.clear();
for (idx, entry) in self.entries.iter().enumerate() {
self.address_map.insert(entry.address, idx);
}
self.name_index.clear();
for (idx, entry) in self.entries.iter().enumerate() {
self.name_index
.entry(entry.name.to_lowercase())
.or_default()
.push(idx);
}
self.file_index.clear();
for (idx, entry) in self.entries.iter().enumerate() {
if let Some(sf_idx) = entry.source_file_index {
if sf_idx < self.source_files.len() {
let path = self.source_files[sf_idx].path.clone();
self.file_index.entry(path).or_default().push(idx);
}
}
}
self.is_built = true;
let end = current_timestamp();
self.stats.total_symbols = self.entries.len();
self.stats.total_functions = self
.entries
.iter()
.filter(|e| e.kind == X86SymbolKind::Function)
.count();
self.stats.total_publics = self
.entries
.iter()
.filter(|e| e.kind == X86SymbolKind::PublicSymbol)
.count();
self.stats.total_inline_frames = self.inline_frames.len();
self.stats.total_source_files = self.source_files.len();
self.stats.total_modules = self.module_names.len();
self.stats.build_time_ms = end.saturating_sub(start) * 1000;
}
pub fn lookup_by_address(&self, address: u64) -> Option<(usize, X86SymbolInfo)> {
if !self.is_built || self.entries.is_empty() {
return None;
}
for (&addr, &idx) in self.address_map.range(..=address).rev() {
let entry = &self.entries[idx];
let end = entry.address.saturating_add(entry.size);
if address >= entry.address && address < end {
let mut info = self.build_symbol_info(idx, entry);
if !entry.inline_frame_indices.is_empty() {
info.inline_frames =
self.resolve_inline_frames(&entry.inline_frame_indices, address);
}
return Some((idx, info));
}
break;
}
None
}
pub fn lookup_source_location(&self, address: u64) -> Option<X86SourceLocation> {
let (_idx, info) = self.lookup_by_address(address)?;
Some(X86SourceLocation {
file_path: info.source_file.unwrap_or_default(),
line: info.source_line.unwrap_or(0),
column: None,
address,
})
}
pub fn lookup_by_name(&self, name: &str) -> Vec<X86SymbolInfo> {
if !self.is_built {
return Vec::new();
}
let name_lower = name.to_lowercase();
let mut results = Vec::new();
if let Some(indices) = self.name_index.get(&name_lower) {
for &idx in indices {
results.push(self.build_symbol_info(idx, &self.entries[idx]));
}
}
for (key, indices) in &self.name_index {
if key.starts_with(&name_lower) && key != &name_lower {
for &idx in indices {
results.push(self.build_symbol_info(idx, &self.entries[idx]));
}
}
}
results
}
pub fn lookup_by_source_file(&self, file_path: &str) -> Vec<X86SymbolInfo> {
if !self.is_built {
return Vec::new();
}
let mut results = Vec::new();
if let Some(indices) = self.file_index.get(file_path) {
for &idx in indices {
results.push(self.build_symbol_info(idx, &self.entries[idx]));
}
}
results
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
self.address_map.clear();
self.name_index.clear();
self.file_index.clear();
self.source_files.clear();
self.inline_frames.clear();
self.module_names.clear();
self.is_built = false;
self.stats = X86SymbolIndexStats::default();
}
fn get_or_add_source_file(&mut self, path: &str, hash: Option<[u8; 32]>) -> usize {
if let Some(pos) = self.source_files.iter().position(|f| f.path == path) {
return pos;
}
self.source_files.push(X86SourceFileEntry {
path: path.to_string(),
content_hash: hash,
});
self.source_files.len() - 1
}
fn get_or_add_module_name(&mut self, name: &str) -> usize {
if let Some(pos) = self.module_names.iter().position(|m| m == name) {
return pos;
}
self.module_names.push(name.to_string());
self.module_names.len() - 1
}
fn build_symbol_info(&self, _idx: usize, entry: &X86SymbolIndexEntry) -> X86SymbolInfo {
X86SymbolInfo {
name: entry.name.clone(),
demangled_name: None,
address: entry.address,
size: entry.size,
kind: entry.kind,
source_file: entry
.source_file_index
.and_then(|i| self.source_files.get(i).map(|f| f.path.clone())),
source_line: entry.source_line,
module_name: entry
.module_name_index
.and_then(|i| self.module_names.get(i).cloned()),
inline_frames: Vec::new(),
}
}
fn resolve_inline_frames(&self, indices: &[usize], address: u64) -> Vec<X86InlineFrame> {
let mut frames: Vec<X86InlineFrame> = indices
.iter()
.filter_map(|&fi| {
let fe = self.inline_frames.get(fi)?;
if address >= fe.start_address && address < fe.end_address {
Some(X86InlineFrame {
function_name: fe.function_name.clone(),
source_file: fe
.source_file_index
.and_then(|i| self.source_files.get(i).map(|f| f.path.clone())),
source_line: fe.source_line,
call_file: fe
.call_file_index
.and_then(|i| self.source_files.get(i).map(|f| f.path.clone())),
call_line: fe.call_line,
depth: fe.depth,
address_range: (fe.start_address, fe.end_address),
})
} else {
None
}
})
.collect();
frames.sort_by_key(|f| f.depth);
frames
}
}
impl Default for X86SymbolIndex {
fn default() -> Self {
Self::new()
}
}
pub struct X86SymbolStore {
root_path: PathBuf,
memory_cache: HashMap<String, Vec<u8>>,
module_index: HashMap<String, X86ModuleDescriptor>,
build_id_index: HashMap<String, String>,
uuid_index: HashMap<String, String>,
max_cache_size: u64,
cache_usage: u64,
pending_transactions: HashMap<String, X86SymStoreTransaction>,
}
impl X86SymbolStore {
pub fn new(root_path: &Path) -> Self {
Self {
root_path: root_path.to_path_buf(),
memory_cache: HashMap::new(),
module_index: HashMap::new(),
build_id_index: HashMap::new(),
uuid_index: HashMap::new(),
max_cache_size: MAX_SYMBOL_CACHE_SIZE,
cache_usage: 0,
pending_transactions: HashMap::new(),
}
}
pub fn new_memory() -> Self {
Self::new(Path::new(":memory:"))
}
pub fn set_max_cache_size(&mut self, size: u64) {
self.max_cache_size = size;
}
pub fn symbol_server_path(module_name: &str, guid: &str, age: u32) -> PathBuf {
let name = if module_name.ends_with(".pdb") {
module_name.to_string()
} else {
format!("{}.pdb", module_name)
};
let guid_flat = guid.replace('-', "").to_uppercase();
let guid_with_age = format!("{}{:X}", guid_flat, age);
PathBuf::from(module_name).join(&guid_with_age).join(&name)
}
pub fn store_pdb_ms_layout(
&mut self,
module_name: &str,
guid: &str,
age: u32,
data: &[u8],
) -> std::io::Result<PathBuf> {
let rel_path = Self::symbol_server_path(module_name, guid, age);
let full_path = self.root_path.join(&rel_path);
self.ensure_parent_dir(&full_path)?;
self.write_file_atomic(&full_path, data)?;
let debug_id = X86DebugId::new(
parse_guid(guid).unwrap_or([0u8; 16]).to_vec(),
age,
X86DebugIdKind::PdbGuid,
);
let descriptor = X86ModuleDescriptor::new(module_name, debug_id, 0, data.len() as u64);
self.module_index
.insert(module_name.to_lowercase(), descriptor);
Ok(full_path)
}
pub fn breakpad_symbol_path(module_name: &str, debug_id: &str) -> PathBuf {
PathBuf::from(module_name)
.join(debug_id)
.join(format!("{}.sym", module_name))
}
pub fn store_breakpad_sym(
&mut self,
module_name: &str,
debug_id: &str,
data: &[u8],
) -> std::io::Result<PathBuf> {
let rel_path = Self::breakpad_symbol_path(module_name, debug_id);
let full_path = self.root_path.join(&rel_path);
self.ensure_parent_dir(&full_path)?;
self.write_file_atomic(&full_path, data)?;
Ok(full_path)
}
pub fn dwarf_debug_path(module_name: &str, build_id: &str) -> PathBuf {
PathBuf::from(module_name)
.join(build_id)
.join(format!("{}.dbg", module_name))
}
pub fn store_dwarf_debug(
&mut self,
module_name: &str,
build_id: &str,
data: &[u8],
) -> std::io::Result<PathBuf> {
let rel_path = Self::dwarf_debug_path(module_name, build_id);
let full_path = self.root_path.join(&rel_path);
self.ensure_parent_dir(&full_path)?;
self.write_file_atomic(&full_path, data)?;
Ok(full_path)
}
pub fn index_elf_build_id(&mut self, build_id: &[u8], module_name: &str) {
self.build_id_index
.insert(format_hex(build_id), module_name.to_string());
}
pub fn lookup_by_elf_build_id(&self, build_id: &[u8]) -> Option<&String> {
self.build_id_index.get(&format_hex(build_id))
}
pub fn parse_elf_build_id(data: &[u8]) -> Option<Vec<u8>> {
if data.len() < 64 {
return None;
}
let is_64bit = data[4] == 2;
let little_endian = data[5] == 1;
let read_u16 = |off: usize| -> u16 {
let b = [data[off], data[off + 1]];
if little_endian {
u16::from_le_bytes(b)
} else {
u16::from_be_bytes(b)
}
};
let read_u32 = |off: usize| -> u32 {
let b = [data[off], data[off + 1], data[off + 2], data[off + 3]];
if little_endian {
u32::from_le_bytes(b)
} else {
u32::from_be_bytes(b)
}
};
let read_u64 = |off: usize| -> u64 {
let b = [
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
data[off + 4],
data[off + 5],
data[off + 6],
data[off + 7],
];
if little_endian {
u64::from_le_bytes(b)
} else {
u64::from_be_bytes(b)
}
};
let shoff: u64 = if is_64bit {
read_u64(40)
} else {
read_u32(32) as u64
};
let shentsize: u16 = if is_64bit { read_u16(58) } else { read_u16(46) };
let shnum: u16 = if is_64bit { read_u16(60) } else { read_u16(48) };
let shstrndx: u16 = if is_64bit { read_u16(62) } else { read_u16(50) };
if shnum == 0 || shstrndx >= shnum {
return None;
}
let shstr_off: u64 = if is_64bit {
let idx = shstrndx as u64 * shentsize as u64;
read_u64(shoff as usize + idx as usize + 24) } else {
let idx = shstrndx as u64 * shentsize as u64;
read_u32(shoff as usize + idx as usize + 16) as u64 };
for i in 0..shnum as u64 {
let hdr_off = shoff as usize + (i * shentsize as u64) as usize;
if hdr_off + (if is_64bit { 64 } else { 40 }) > data.len() {
break;
}
let sh_name: u32 = read_u32(hdr_off);
let sh_type: u32 = if is_64bit {
read_u32(hdr_off + 4)
} else {
read_u32(hdr_off + 4)
};
let sh_offset: u64 = if is_64bit {
read_u64(hdr_off + 24)
} else {
read_u32(hdr_off + 16) as u64
};
let sh_size: u64 = if is_64bit {
read_u64(hdr_off + 32)
} else {
read_u32(hdr_off + 20) as u64
};
let name_off = (shstr_off + sh_name as u64) as usize;
let mut name = String::new();
for j in name_off..data.len() {
if data[j] == 0 {
break;
}
name.push(data[j] as char);
}
if sh_type == 7 && name == ".note.gnu.build-id" && sh_size > 16 && sh_offset > 0 {
let note_off = sh_offset as usize;
if note_off + 16 > data.len() {
continue;
}
let namesz = read_u32(note_off);
let descsz = read_u32(note_off + 4);
let ntype = read_u32(note_off + 8);
if ntype == 3 && descsz > 0 {
let desc_off = note_off + 12 + ((namesz + 3) & !3) as usize;
if desc_off + descsz as usize <= data.len() {
return Some(data[desc_off..desc_off + descsz as usize].to_vec());
}
}
}
}
None
}
pub fn index_pe_codeview(&mut self, info: &X86CodeViewInfo, module_name: &str) {
let key = format!("{}:{:X}", info.guid_string(), info.age);
self.build_id_index.insert(key, module_name.to_string());
}
pub fn parse_pe_debug_directory(data: &[u8], offset: usize) -> Option<X86CodeViewInfo> {
if offset + 28 > data.len() {
return None;
}
let base = offset;
let debug_type = u32::from_le_bytes([
data[base + 12],
data[base + 13],
data[base + 14],
data[base + 15],
]);
let size_of_data = u32::from_le_bytes([
data[base + 16],
data[base + 17],
data[base + 18],
data[base + 19],
]);
let pointer_to_raw = u32::from_le_bytes([
data[base + 24],
data[base + 25],
data[base + 26],
data[base + 27],
]);
if debug_type != IMAGE_DEBUG_TYPE_CODEVIEW {
return None;
}
let raw = pointer_to_raw as usize;
if raw + 4 > data.len() || raw + size_of_data as usize > data.len() {
return None;
}
let sig = u32::from_le_bytes([data[raw], data[raw + 1], data[raw + 2], data[raw + 3]]);
match sig {
CV_SIGNATURE_RSDS => {
if raw + 24 > data.len() {
return None;
}
let mut guid = [0u8; 16];
guid.copy_from_slice(&data[raw + 4..raw + 20]);
let age = u32::from_le_bytes([
data[raw + 20],
data[raw + 21],
data[raw + 22],
data[raw + 23],
]);
let pdb_path = Self::cstr_from(&data[raw + 24..raw + size_of_data as usize]);
Some(X86CodeViewInfo {
signature: sig,
guid,
age,
pdb_path,
debug_dir_offset: offset as u64,
debug_dir_size: size_of_data,
})
}
CV_SIGNATURE_NB10 => {
if raw + 12 > data.len() {
return None;
}
let ts = u32::from_le_bytes([
data[raw + 4],
data[raw + 5],
data[raw + 6],
data[raw + 7],
]);
let age = u32::from_le_bytes([
data[raw + 8],
data[raw + 9],
data[raw + 10],
data[raw + 11],
]);
let mut guid = [0u8; 16];
guid[0..4].copy_from_slice(&ts.to_le_bytes());
guid[4..8].copy_from_slice(&age.to_le_bytes());
let pdb_path = Self::cstr_from(&data[raw + 12..raw + size_of_data as usize]);
Some(X86CodeViewInfo {
signature: sig,
guid,
age,
pdb_path,
debug_dir_offset: offset as u64,
debug_dir_size: size_of_data,
})
}
_ => None,
}
}
pub fn index_macho_uuid(&mut self, uuid: &[u8; 16], module_name: &str) {
self.uuid_index
.insert(format_guid(uuid), module_name.to_string());
}
pub fn lookup_by_macho_uuid(&self, uuid: &[u8; 16]) -> Option<&String> {
self.uuid_index.get(&format_guid(uuid))
}
pub fn parse_macho_uuid(data: &[u8], lc_offset: usize) -> Option<[u8; 16]> {
const LC_UUID: u32 = 0x1B;
if lc_offset + 24 > data.len() {
return None;
}
let cmd = u32::from_le_bytes([
data[lc_offset],
data[lc_offset + 1],
data[lc_offset + 2],
data[lc_offset + 3],
]);
if cmd != LC_UUID {
return None;
}
let mut uuid = [0u8; 16];
uuid.copy_from_slice(&data[lc_offset + 8..lc_offset + 24]);
Some(uuid)
}
pub fn cache_put(&mut self, key: &str, data: Vec<u8>) {
if self.cache_usage + data.len() as u64 > self.max_cache_size {
self.cache_evict(data.len() as u64);
}
self.cache_usage += data.len() as u64;
self.memory_cache.insert(key.to_string(), data);
}
pub fn cache_get(&self, key: &str) -> Option<&Vec<u8>> {
self.memory_cache.get(key)
}
pub fn cache_remove(&mut self, key: &str) -> Option<Vec<u8>> {
if let Some(data) = self.memory_cache.remove(key) {
self.cache_usage = self.cache_usage.saturating_sub(data.len() as u64);
Some(data)
} else {
None
}
}
fn cache_evict(&mut self, needed: u64) {
if self.memory_cache.is_empty() {
return;
}
let keys: Vec<String> = self.memory_cache.keys().cloned().collect();
let mut freed: u64 = 0;
for key in &keys {
if freed >= needed {
break;
}
if let Some(data) = self.memory_cache.remove(key) {
freed += data.len() as u64;
self.cache_usage = self.cache_usage.saturating_sub(data.len() as u64);
}
}
}
pub fn begin_transaction(&mut self, transaction_id: &str) {
self.pending_transactions.insert(
transaction_id.to_string(),
X86SymStoreTransaction {
id: transaction_id.to_string(),
adds: Vec::new(),
deletes: Vec::new(),
state: X86TransactionState::Pending,
},
);
}
pub fn transaction_add_file(
&mut self,
transaction_id: &str,
path: PathBuf,
data: Vec<u8>,
) -> Result<(), String> {
let txn = self
.pending_transactions
.get_mut(transaction_id)
.ok_or_else(|| format!("Transaction {} not found", transaction_id))?;
if txn.state != X86TransactionState::Pending {
return Err("Transaction already committed or rolled back".to_string());
}
txn.adds.push((path, data));
Ok(())
}
pub fn transaction_delete_file(
&mut self,
transaction_id: &str,
path: PathBuf,
) -> Result<(), String> {
let txn = self
.pending_transactions
.get_mut(transaction_id)
.ok_or_else(|| format!("Transaction {} not found", transaction_id))?;
if txn.state != X86TransactionState::Pending {
return Err("Transaction already committed or rolled back".to_string());
}
txn.deletes.push(path);
Ok(())
}
pub fn commit_transaction(&mut self, transaction_id: &str) -> Result<(), String> {
let txn = self
.pending_transactions
.get_mut(transaction_id)
.ok_or_else(|| format!("Transaction {} not found", transaction_id))?;
if txn.state != X86TransactionState::Pending {
return Err("Transaction already committed or rolled back".to_string());
}
let mut temp_paths: Vec<(PathBuf, PathBuf)> = Vec::new();
for (path, data) in &txn.adds {
self.ensure_parent_dir(path).map_err(|e| e.to_string())?;
let temp_path = path.with_extension(TRANSACTION_FILE_SUFFIX);
self.write_file_atomic(&temp_path, data)
.map_err(|e| e.to_string())?;
temp_paths.push((path.clone(), temp_path));
}
for (target, temp) in &temp_paths {
if temp.exists() {
std::fs::rename(temp, target).map_err(|e| e.to_string())?;
}
}
for path in &txn.deletes {
if path.exists() {
let _ = std::fs::remove_file(path);
}
}
txn.state = X86TransactionState::Committed;
Ok(())
}
pub fn rollback_transaction(&mut self, transaction_id: &str) -> Result<(), String> {
let txn = self
.pending_transactions
.get_mut(transaction_id)
.ok_or_else(|| format!("Transaction {} not found", transaction_id))?;
if txn.state != X86TransactionState::Pending {
return Err("Transaction already committed or rolled back".to_string());
}
for (path, _) in &txn.adds {
let temp = path.with_extension(TRANSACTION_FILE_SUFFIX);
if temp.exists() {
let _ = std::fs::remove_file(&temp);
}
}
txn.state = X86TransactionState::RolledBack;
Ok(())
}
fn ensure_parent_dir(&self, path: &Path) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
Ok(())
}
fn write_file_atomic(&self, path: &Path, data: &[u8]) -> std::io::Result<()> {
let mut f = std::fs::File::create(path)?;
f.write_all(data)?;
f.sync_all()?;
Ok(())
}
fn cstr_from(data: &[u8]) -> String {
let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
String::from_utf8_lossy(&data[..end]).to_string()
}
}
#[derive(Debug, Clone)]
struct X86SymStoreTransaction {
id: String,
adds: Vec<(PathBuf, Vec<u8>)>,
deletes: Vec<PathBuf>,
state: X86TransactionState,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum X86TransactionState {
Pending,
Committed,
RolledBack,
}
pub struct X86SymbolServerProtocol {
sources: Vec<X86SymbolSource>,
store: X86SymbolStore,
cache_dir: Option<PathBuf>,
verify_signatures: bool,
timeout_ms: u64,
}
impl X86SymbolServerProtocol {
pub fn new(store: X86SymbolStore) -> Self {
Self {
sources: Vec::new(),
store,
cache_dir: None,
verify_signatures: false,
timeout_ms: DEFAULT_SYMBOL_SERVER_TIMEOUT_MS,
}
}
pub fn add_source(&mut self, source: X86SymbolSource) {
self.sources.push(source);
self.sources.sort_by_key(|s| s.priority());
if self.sources.len() > MAX_SYMBOL_SOURCE_CHAIN {
self.sources.truncate(MAX_SYMBOL_SOURCE_CHAIN);
}
}
pub fn remove_source(&mut self, desc: &str) {
self.sources.retain(|s| s.description() != desc);
}
pub fn set_cache_dir(&mut self, dir: &Path) {
self.cache_dir = Some(dir.to_path_buf());
}
pub fn set_verify_signatures(&mut self, verify: bool) {
self.verify_signatures = verify;
}
pub fn set_timeout(&mut self, ms: u64) {
self.timeout_ms = ms;
}
pub fn build_http_symbol_url(
server_url: &str,
module_name: &str,
guid: &str,
age: u32,
) -> String {
let pdb_name = if module_name.ends_with(".pdb") {
module_name.to_string()
} else {
format!("{}.pdb", module_name)
};
let guid_flat = guid.replace('-', "").to_uppercase();
let guid_with_age = format!("{}{:X}", guid_flat, age);
let base = server_url.trim_end_matches('/');
format!("{}/{}/{}/{}", base, pdb_name, guid_with_age, pdb_name)
}
pub fn fetch_http_symbol(
&self,
server_url: &str,
module_name: &str,
guid: &str,
age: u32,
) -> Option<Vec<u8>> {
let url = Self::build_http_symbol_url(server_url, module_name, guid, age);
if let Some(ref cache_dir) = self.cache_dir {
let cache_key = format!("http:{}/{}/{:X}", module_name, guid, age);
if let Some(data) = self.store.cache_get(&cache_key) {
return Some(data.clone());
}
let rel = X86SymbolStore::symbol_server_path(module_name, guid, age);
let cached = cache_dir.join(&rel);
if cached.exists() {
if let Ok(data) = std::fs::read(&cached) {
self.store.cache_put(&cache_key, data.clone());
return Some(data);
}
}
}
let _ = url; None
}
pub fn resolve_symbol_file(&self, module_name: &str, debug_id: &X86DebugId) -> Option<Vec<u8>> {
for source in &self.sources {
let result = match source {
X86SymbolSource::HttpSymbolServer {
url, timeout_ms, ..
} => {
let _ = timeout_ms;
self.fetch_http_symbol(
url,
module_name,
&debug_id.to_guid_string(),
debug_id.age,
)
}
X86SymbolSource::S3Store { bucket, prefix, .. } => {
let _ = (bucket, prefix);
self.try_local_cache(module_name, debug_id)
}
X86SymbolSource::GcsStore { bucket, prefix, .. } => {
let _ = (bucket, prefix);
self.try_local_cache(module_name, debug_id)
}
X86SymbolSource::LocalStore { root_path, .. } => {
self.try_local_store(root_path, module_name, debug_id)
}
X86SymbolSource::MemoryCache { .. } => {
let key = format!("{}:{}", module_name, debug_id.to_flat_string());
self.store.cache_get(&key).cloned()
}
};
if result.is_some() {
return result;
}
}
None
}
fn try_local_cache(&self, module_name: &str, debug_id: &X86DebugId) -> Option<Vec<u8>> {
if let Some(ref cache_dir) = self.cache_dir {
let rel = X86SymbolStore::symbol_server_path(
module_name,
&debug_id.to_guid_string(),
debug_id.age,
);
let cached = cache_dir.join(&rel);
if cached.exists() {
return std::fs::read(&cached).ok();
}
}
None
}
fn try_local_store(
&self,
root_path: &Path,
module_name: &str,
debug_id: &X86DebugId,
) -> Option<Vec<u8>> {
let rel = X86SymbolStore::symbol_server_path(
module_name,
&debug_id.to_guid_string(),
debug_id.age,
);
let path = root_path.join(&rel);
if path.exists() {
return std::fs::read(&path).ok();
}
let bp_rel = X86SymbolStore::breakpad_symbol_path(module_name, &debug_id.to_flat_string());
let bp_path = root_path.join(&bp_rel);
if bp_path.exists() {
return std::fs::read(&bp_path).ok();
}
let dbg_rel = X86SymbolStore::dwarf_debug_path(module_name, &debug_id.to_flat_string());
let dbg_path = root_path.join(&dbg_rel);
if dbg_path.exists() {
return std::fs::read(&dbg_path).ok();
}
None
}
pub fn list_sources(&self) -> &[X86SymbolSource] {
&self.sources
}
}
pub struct X86SymbolUploader {
store: X86SymbolStore,
protocol: X86SymbolServerProtocol,
compress: bool,
compression_level: u32,
strip_and_upload: bool,
}
impl X86SymbolUploader {
pub fn new(store: X86SymbolStore, protocol: X86SymbolServerProtocol) -> Self {
Self {
store,
protocol,
compress: true,
compression_level: 6,
strip_and_upload: false,
}
}
pub fn set_compress(&mut self, compress: bool, level: u32) {
self.compress = compress;
self.compression_level = level.min(9);
}
pub fn set_strip_and_upload(&mut self, enable: bool) {
self.strip_and_upload = enable;
}
pub fn upload_from_elf(
&mut self,
elf_data: &[u8],
module_name: &str,
) -> Result<PathBuf, String> {
let build_id = X86SymbolStore::parse_elf_build_id(elf_data)
.ok_or_else(|| "No ELF build ID found".to_string())?;
let bid_hex = format_hex(&build_id);
let debug_data = self.extract_dwarf_debug_sections(elf_data)?;
let final_data = if self.compress {
self.compress_data(&debug_data)
} else {
debug_data
};
self.store
.store_dwarf_debug(module_name, &bid_hex, &final_data)
.map_err(|e| e.to_string())
}
fn extract_dwarf_debug_sections(&self, elf_data: &[u8]) -> Result<Vec<u8>, String> {
let mut package = Vec::new();
package.extend_from_slice(b"DWARFDBG"); package.extend_from_slice(&1u32.to_le_bytes()); package.extend_from_slice(&(elf_data.len() as u64).to_le_bytes()); package.extend_from_slice(elf_data);
Ok(package)
}
pub fn upload_pdb(
&mut self,
pdb_data: &[u8],
module_name: &str,
codeview: &X86CodeViewInfo,
) -> Result<PathBuf, String> {
let final_data = if self.compress {
self.compress_data(pdb_data)
} else {
pdb_data.to_vec()
};
self.store
.store_pdb_ms_layout(
module_name,
&codeview.guid_string(),
codeview.age,
&final_data,
)
.map_err(|e| e.to_string())
}
pub fn upload_breakpad(
&mut self,
sym_data: &[u8],
module_name: &str,
debug_id: &str,
) -> Result<PathBuf, String> {
let final_data = if self.compress {
self.compress_data(sym_data)
} else {
sym_data.to_vec()
};
self.store
.store_breakpad_sym(module_name, debug_id, &final_data)
.map_err(|e| e.to_string())
}
pub fn strip_and_upload_binary(
&mut self,
binary_data: &[u8],
module_name: &str,
) -> Result<(Vec<u8>, PathBuf), String> {
let (stripped, debug_data) = self.strip_debug_info(binary_data)?;
let build_id = X86SymbolStore::parse_elf_build_id(binary_data)
.or_else(|| {
self.find_macho_uuid(binary_data).map(|u| u.to_vec())
})
.unwrap_or_else(|| {
let hash = fnv1a_32(binary_data);
hash.to_le_bytes().to_vec()
});
let bid_hex = format_hex(&build_id);
let sym_path = self
.store
.store_dwarf_debug(module_name, &bid_hex, &debug_data)
.map_err(|e| e.to_string())?;
Ok((stripped, sym_path))
}
fn strip_debug_info(&self, data: &[u8]) -> Result<(Vec<u8>, Vec<u8>), String> {
let half = data.len() / 2;
let stripped = data[..half].to_vec();
let debug = data[half..].to_vec();
Ok((stripped, debug))
}
fn find_macho_uuid(&self, data: &[u8]) -> Option<[u8; 16]> {
if data.len() < 28 {
return None;
}
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
if magic == 0xBEBAFECA {
return None; }
let is_64 = magic == 0xFEEDFACF || magic == 0xCFFAEDFE;
let is_32 = magic == 0xFEEDFACE || magic == 0xCEFAEDFE;
if !is_64 && !is_32 {
return None;
}
let is_be = magic == 0xFEEDFACE || magic == 0xFEEDFACF;
let ncmds: u32 = if is_be {
u32::from_be_bytes([data[16], data[17], data[18], data[19]])
} else {
u32::from_le_bytes([data[16], data[17], data[18], data[19]])
};
let hdr_size = if is_64 { 32 } else { 28 };
let mut off = hdr_size;
for _ in 0..ncmds {
if off + 8 > data.len() {
break;
}
let cmd: u32 = if is_be {
u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
} else {
u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
};
let cmdsize: u32 = if is_be {
u32::from_be_bytes([data[off + 4], data[off + 5], data[off + 6], data[off + 7]])
} else {
u32::from_le_bytes([data[off + 4], data[off + 5], data[off + 6], data[off + 7]])
};
const LC_UUID: u32 = 0x1B;
if cmd == LC_UUID && cmdsize >= 24 && off + 24 <= data.len() {
let mut uuid = [0u8; 16];
uuid.copy_from_slice(&data[off + 8..off + 24]);
return Some(uuid);
}
off += cmdsize as usize;
}
None
}
pub fn compress_data(&self, data: &[u8]) -> Vec<u8> {
if data.is_empty() {
return data.to_vec();
}
let mut compressed = Vec::new();
compressed.extend_from_slice(b"RLE1"); compressed.extend_from_slice(&(data.len() as u64).to_le_bytes()); let mut i = 0;
while i < data.len() {
let byte = data[i];
let mut run = 1;
while i + run < data.len() && data[i + run] == byte && run < 255 {
run += 1;
}
compressed.push(byte);
compressed.push(run as u8);
i += run;
}
compressed
}
pub fn decompress_data(&self, data: &[u8]) -> Option<Vec<u8>> {
if data.len() < 12 || &data[..4] != b"RLE1" {
return None;
}
let orig_size = u64::from_le_bytes([
data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
]) as usize;
let mut decompressed = Vec::with_capacity(orig_size);
let mut i = 12;
while i + 1 < data.len() {
let byte = data[i];
let run = data[i + 1] as usize;
for _ in 0..run {
decompressed.push(byte);
if decompressed.len() >= orig_size {
break;
}
}
i += 2;
if decompressed.len() >= orig_size {
break;
}
}
Some(decompressed)
}
}
pub struct X86SymbolResolver {
modules: Vec<X86ModuleDescriptor>,
module_indices: HashMap<String, X86SymbolIndex>,
protocol: X86SymbolServerProtocol,
breakpad_converter: X86BreakpadConverter,
auto_fetch: bool,
}
impl X86SymbolResolver {
pub fn new(protocol: X86SymbolServerProtocol) -> Self {
Self {
modules: Vec::new(),
module_indices: HashMap::new(),
protocol,
breakpad_converter: X86BreakpadConverter::new(),
auto_fetch: true,
}
}
pub fn register_module(&mut self, descriptor: X86ModuleDescriptor) {
let mut index = X86SymbolIndex::new();
if let Some(ref sym_path) = descriptor.symbol_path {
if let Ok(data) = std::fs::read(sym_path) {
self.index_symbol_data(&mut index, &data, &descriptor);
}
}
index.build();
let name = descriptor.name.to_lowercase();
self.module_indices.insert(name.clone(), index);
self.modules.push(descriptor);
}
fn index_symbol_data(
&self,
index: &mut X86SymbolIndex,
data: &[u8],
module: &X86ModuleDescriptor,
) {
if let Ok(text) = std::str::from_utf8(data) {
if text.contains("MODULE ") {
self.parse_breakpad_to_index(index, text, module);
return;
}
}
if data.starts_with(b"Microsoft C/C++") {
self.parse_pdb_to_index(index, data, module);
return;
}
if data.len() > 4 && &data[..4] == b"DWAR" {
self.parse_dwarf_to_index(index, data, module);
return;
}
if let Ok(text) = std::str::from_utf8(data) {
for line in text.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
if let Ok(addr) = u64::from_str_radix(parts[0], 16) {
let name = parts[1];
index.add_symbol(
name,
addr,
1,
X86SymbolKind::Function,
None,
None,
Some(&module.name),
);
}
}
}
}
}
fn parse_breakpad_to_index(
&self,
index: &mut X86SymbolIndex,
text: &str,
module: &X86ModuleDescriptor,
) {
for line in text.lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
continue;
}
match parts[0] {
"FUNC" | "PUBLIC" if parts.len() >= 3 => {
if let Ok(addr) = u64::from_str_radix(parts[1], 16) {
let size = if parts.len() >= 4 {
u64::from_str_radix(parts[2], 16).unwrap_or(1)
} else {
1
};
let kind = if parts[0] == "FUNC" {
X86SymbolKind::Function
} else {
X86SymbolKind::PublicSymbol
};
let name = if parts[0] == "FUNC" && parts.len() >= 5 {
parts[4].to_string()
} else if parts.len() >= 4 {
parts[3].to_string()
} else {
parts[2].to_string()
};
index.add_symbol(&name, addr, size, kind, None, None, Some(&module.name));
}
}
_ => {}
}
}
}
fn parse_pdb_to_index(
&self,
index: &mut X86SymbolIndex,
data: &[u8],
module: &X86ModuleDescriptor,
) {
let _ = (index, data, module);
}
fn parse_dwarf_to_index(
&self,
index: &mut X86SymbolIndex,
data: &[u8],
module: &X86ModuleDescriptor,
) {
let _ = (index, data, module);
}
pub fn resolve_function(&self, address: u64) -> Option<X86SymbolInfo> {
for module in &self.modules {
if address >= module.image_base
&& address < module.image_base.saturating_add(module.image_size)
{
let rel_addr = address - module.image_base;
let name = module.name.to_lowercase();
if let Some(index) = self.module_indices.get(&name) {
if let Some((_idx, mut info)) = index.lookup_by_address(rel_addr) {
info.address = address; info.module_name = Some(module.name.clone());
return Some(info);
}
}
}
}
if self.auto_fetch {
}
None
}
pub fn resolve_source_location(&self, address: u64) -> Option<X86SourceLocation> {
for module in &self.modules {
if address >= module.image_base
&& address < module.image_base.saturating_add(module.image_size)
{
let rel_addr = address - module.image_base;
let name = module.name.to_lowercase();
if let Some(index) = self.module_indices.get(&name) {
if let Some(loc) = index.lookup_source_location(rel_addr) {
return Some(X86SourceLocation { address, ..loc });
}
}
}
}
None
}
pub fn resolve_module(&self, address: u64) -> Option<&X86ModuleDescriptor> {
self.modules.iter().find(|m| {
address >= m.image_base && address < m.image_base.saturating_add(m.image_size)
})
}
pub fn resolve_inline_frames(&self, address: u64) -> Vec<X86InlineFrame> {
if let Some(info) = self.resolve_function(address) {
return info.inline_frames;
}
Vec::new()
}
pub fn resolve_by_name(&self, name: &str) -> Vec<X86SymbolInfo> {
let mut results = Vec::new();
for (module_name, index) in &self.module_indices {
for mut info in index.lookup_by_name(name) {
if let Some(module) = self
.modules
.iter()
.find(|m| m.name.to_lowercase() == *module_name)
{
info.address = module.image_base + info.address;
info.module_name = Some(module.name.clone());
}
results.push(info);
}
}
results
}
pub fn list_modules(&self) -> &[X86ModuleDescriptor] {
&self.modules
}
pub fn unregister_module(&mut self, name: &str) {
let lower = name.to_lowercase();
self.module_indices.remove(&lower);
self.modules.retain(|m| m.name.to_lowercase() != lower);
}
}
pub struct X86BreakpadConverter {
arch: String,
os: String,
include_cfi: bool,
include_publics: bool,
}
impl X86BreakpadConverter {
pub fn new() -> Self {
Self {
arch: "x86".to_string(),
os: "linux".to_string(),
include_cfi: true,
include_publics: true,
}
}
pub fn set_arch(&mut self, arch: &str) {
self.arch = arch.to_string();
}
pub fn set_os(&mut self, os: &str) {
self.os = os.to_string();
}
pub fn convert_dwarf_to_breakpad(
&self,
dwarf_data: &[u8],
module_name: &str,
debug_id: &str,
) -> String {
let mut output = String::new();
output.push_str(&format!(
"{} {} {} {} {}\n",
BREAKPAD_MODULE_MAGIC, self.os, self.arch, debug_id, module_name
));
output.push_str(&format!("INFO {} {}\n", BREAKPAD_INFO_CODE_ID, debug_id));
output.push_str(&format!("INFO {} {}\n", BREAKPAD_INFO_DEBUG_ID, debug_id));
let symbols = self.extract_dwarf_symbols(dwarf_data);
let mut file_id_map: HashMap<String, u32> = HashMap::new();
let mut next_file_id: u32 = 1;
for sym in &symbols {
if let Some(ref file) = sym.source_file {
if !file_id_map.contains_key(file) {
output.push_str(&format!("FILE {} {}\n", next_file_id, file));
file_id_map.insert(file.clone(), next_file_id);
next_file_id += 1;
}
}
}
for sym in &symbols {
let file_id = sym
.source_file
.as_ref()
.and_then(|f| file_id_map.get(f))
.copied()
.unwrap_or(0);
let line = sym.source_line.unwrap_or(0);
let func_line = format!(
"{} {:x} {:x} {} {}\n",
BREAKPAD_FUNC_MARKER, sym.address, sym.size, file_id, sym.name,
);
output.push_str(&func_line);
if sym.source_line.is_some() {
output.push_str(&format!(
"{:x} {} {} {}\n",
sym.address, sym.size, line, file_id,
));
}
}
if self.include_publics {
for sym in &symbols {
if sym.kind == X86SymbolKind::PublicSymbol || sym.kind == X86SymbolKind::Function {
output.push_str(&format!(
"{} {:x} 0 {}\n",
BREAKPAD_PUBLIC_MARKER, sym.address, sym.name,
));
}
}
}
if self.include_cfi {
output.push_str(&self.generate_cfi_records(&symbols));
}
output
}
fn extract_dwarf_symbols(&self, _dwarf_data: &[u8]) -> Vec<X86SymbolInfo> {
Vec::new()
}
pub fn convert_pdb_to_breakpad(
&self,
pdb_data: &[u8],
module_name: &str,
debug_id: &str,
) -> String {
let mut output = String::new();
output.push_str(&format!(
"{} {} {} {} {}\n",
BREAKPAD_MODULE_MAGIC, self.os, self.arch, debug_id, module_name
));
output.push_str(&format!("INFO {} {}\n", BREAKPAD_INFO_CODE_ID, debug_id));
output.push_str(&format!("INFO {} {}\n", BREAKPAD_INFO_DEBUG_ID, debug_id));
let symbols = self.extract_pdb_symbols(pdb_data);
let mut file_id_map: HashMap<String, u32> = HashMap::new();
let mut next_file_id: u32 = 1;
for sym in &symbols {
if let Some(ref file) = sym.source_file {
if !file_id_map.contains_key(file) {
output.push_str(&format!("FILE {} {}\n", next_file_id, file));
file_id_map.insert(file.clone(), next_file_id);
next_file_id += 1;
}
}
}
for sym in &symbols {
let file_id = sym
.source_file
.as_ref()
.and_then(|f| file_id_map.get(f))
.copied()
.unwrap_or(0);
let line = sym.source_line.unwrap_or(0);
output.push_str(&format!(
"{} {:x} {:x} {} {}\n",
BREAKPAD_FUNC_MARKER, sym.address, sym.size, file_id, sym.name,
));
output.push_str(&format!(
"{:x} {} {} {}\n",
sym.address, sym.size, line, file_id
));
}
if self.include_publics {
for sym in &symbols {
output.push_str(&format!(
"{} {:x} 0 {}\n",
BREAKPAD_PUBLIC_MARKER, sym.address, sym.name,
));
}
}
if self.include_cfi {
output.push_str(&self.generate_cfi_records(&symbols));
}
output
}
fn extract_pdb_symbols(&self, _pdb_data: &[u8]) -> Vec<X86SymbolInfo> {
Vec::new()
}
fn generate_cfi_records(&self, symbols: &[X86SymbolInfo]) -> String {
let mut output = String::new();
for sym in symbols {
if sym.kind == X86SymbolKind::Function && sym.size > 0 {
output.push_str(&format!(
"{} {:x} {:x} .cfa: $rsp 8 + .ra: .cfa -8 + ^\n",
BREAKPAD_STACK_CFI_INIT, sym.address, sym.size,
));
}
}
for sym in symbols {
if sym.kind == X86SymbolKind::Function && sym.size > 0 {
let frame_type = 0; let prologue_size = sym.size / 4;
let epilogue_size = sym.size / 8;
output.push_str(&format!(
"{} {} {:x} {:x} {:x} {:x} 0 {} 0 0 0 1\n",
BREAKPAD_STACK_WIN_MARKER,
frame_type,
sym.address,
sym.size,
prologue_size,
epilogue_size,
sym.size / 16, ));
}
}
output
}
}
impl Default for X86BreakpadConverter {
fn default() -> Self {
Self::new()
}
}
pub struct X86SourceServer {
variables: HashMap<String, String>,
vcs_provider: Option<X86VcsProvider>,
ini_entries: Vec<X86SrcSrvIniEntry>,
http_endpoints: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum X86VcsProvider {
Git {
repository_url: String,
branch: String,
},
Subversion {
repository_url: String,
revision: u64,
},
Perforce {
server: String,
client: String,
},
Tfs {
collection_url: String,
team_project: String,
},
}
#[derive(Debug, Clone)]
pub struct X86SrcSrvIniEntry {
pub key: String,
pub value: String,
}
impl X86SourceServer {
pub fn new() -> Self {
let mut variables = HashMap::new();
variables.insert(
"SRCSRV_SOURCE_ROOT".to_string(),
"https://symserver.example.com/sources".to_string(),
);
variables.insert("SRCSRV_TARGET_ROOT".to_string(), "c:\\target".to_string());
variables.insert("SRCSRV_FILE_NAME".to_string(), "%var2%".to_string());
Self {
variables,
vcs_provider: None,
ini_entries: Vec::new(),
http_endpoints: Vec::new(),
}
}
pub fn index_sources(&self, source_files: &[String], pdb_name: &str) -> Vec<u8> {
let mut srcsrv_data = String::new();
srcsrv_data.push_str("SRCSRV: ini ------------------------------------------------\r\n");
srcsrv_data.push_str(&format!("VERSION=1\r\n"));
srcsrv_data.push_str(&format!("VERCTRL={}\r\n", self.vcs_description()));
srcsrv_data.push_str(&format!("DATETIME={}\r\n", current_timestamp()));
srcsrv_data.push_str(&format!(
"SRCSRVTRG={}\r\n",
self.resolve_variable(SRCSRV_VAR_TARGET_ROOT)
));
srcsrv_data.push_str(&format!("SRCSRVCMD={}\r\n", self.build_retrieval_command()));
srcsrv_data.push_str("SRCSRV: source files ---------------------------------------\r\n");
for file in source_files {
let hash = self.compute_source_hash(file);
let escaped = file.replace('\\', "/");
srcsrv_data.push_str(&format!(
"{}{}*{}\r\n",
escaped,
self.expand_variables(file),
hash,
));
}
srcsrv_data.push_str("SRCSRV: end ------------------------------------------------\r\n");
srcsrv_data.into_bytes()
}
fn build_retrieval_command(&self) -> String {
if let Some(ref endpoint) = self.http_endpoints.first() {
format!(
"%fnchdirs%(%targtdir%)\"{}\" %srcsrvtrg%\\%var2%\\%var3%",
endpoint
)
} else if let Some(ref vcs) = self.vcs_provider {
match vcs {
X86VcsProvider::Git {
repository_url,
branch,
} => {
format!(
"git.exe clone --branch {} {} %srcsrvtrg%\\%var2%",
branch, repository_url
)
}
X86VcsProvider::Subversion {
repository_url,
revision,
} => {
format!(
"svn.exe export -r {} {}/%var2% %srcsrvtrg%\\%var2%\\%var3%",
revision, repository_url
)
}
X86VcsProvider::Perforce { server, client } => {
format!(
"p4.exe -p {} -c {} print -o %srcsrvtrg%\\%var2%\\%var3% %var2%",
server, client
)
}
X86VcsProvider::Tfs {
collection_url,
team_project,
} => {
format!(
"tf.exe get {}/{}%var2% /version:T",
collection_url, team_project
)
}
}
} else {
format!("%fnchdirs%(%targtdir%)")
}
}
pub fn generate_srcsrv_ini(&self) -> String {
let mut ini = String::new();
ini.push_str("[variables]\r\n");
for (key, value) in &self.variables {
ini.push_str(&format!("{}={}\r\n", key, value));
}
if let Some(ref vcs) = self.vcs_provider {
ini.push_str("[verctrl]\r\n");
match vcs {
X86VcsProvider::Git {
repository_url,
branch,
} => {
ini.push_str(&format!("provider=git\r\n"));
ini.push_str(&format!("url={}\r\n", repository_url));
ini.push_str(&format!("branch={}\r\n", branch));
}
X86VcsProvider::Subversion {
repository_url,
revision,
} => {
ini.push_str(&format!("provider=svn\r\n"));
ini.push_str(&format!("url={}\r\n", repository_url));
ini.push_str(&format!("revision={}\r\n", revision));
}
X86VcsProvider::Perforce { server, client } => {
ini.push_str(&format!("provider=p4\r\n"));
ini.push_str(&format!("server={}\r\n", server));
ini.push_str(&format!("client={}\r\n", client));
}
X86VcsProvider::Tfs {
collection_url,
team_project,
} => {
ini.push_str(&format!("provider=tfs\r\n"));
ini.push_str(&format!("collection={}\r\n", collection_url));
ini.push_str(&format!("project={}\r\n", team_project));
}
}
}
ini.push_str("[trusted_commands]\r\n");
for entry in &self.ini_entries {
ini.push_str(&format!("{}={}\r\n", entry.key, entry.value));
}
ini
}
pub fn retrieve_source(&self, file_path: &str) -> Option<Vec<u8>> {
for endpoint in &self.http_endpoints {
let url = format!("{}/{}", endpoint.trim_end_matches('/'), file_path);
if let Some(data) = self.fetch_http_source(&url) {
return Some(data);
}
}
if let Some(ref vcs) = self.vcs_provider {
if let Some(data) = self.retrieve_from_vcs(vcs, file_path) {
return Some(data);
}
}
None
}
fn retrieve_from_vcs(&self, vcs: &X86VcsProvider, file_path: &str) -> Option<Vec<u8>> {
match vcs {
X86VcsProvider::Git {
repository_url,
branch: _,
} => {
let _ = (repository_url, file_path);
None
}
X86VcsProvider::Subversion {
repository_url,
revision,
} => {
let _ = (repository_url, revision, file_path);
None
}
_ => None,
}
}
fn fetch_http_source(&self, _url: &str) -> Option<Vec<u8>> {
None
}
fn compute_source_hash(&self, file_path: &str) -> String {
format!("{:08X}", fnv1a_32(file_path.as_bytes()))
}
pub fn set_variable(&mut self, key: &str, value: &str) {
self.variables.insert(key.to_string(), value.to_string());
}
pub fn set_vcs_provider(&mut self, provider: X86VcsProvider) {
self.vcs_provider = Some(provider);
}
pub fn add_http_endpoint(&mut self, url: &str) {
self.http_endpoints.push(url.to_string());
}
pub fn add_ini_entry(&mut self, key: &str, value: &str) {
self.ini_entries.push(X86SrcSrvIniEntry {
key: key.to_string(),
value: value.to_string(),
});
}
fn vcs_description(&self) -> String {
match &self.vcs_provider {
Some(X86VcsProvider::Git { .. }) => "Git".to_string(),
Some(X86VcsProvider::Subversion { .. }) => "Subversion".to_string(),
Some(X86VcsProvider::Perforce { .. }) => "Perforce".to_string(),
Some(X86VcsProvider::Tfs { .. }) => "Team Foundation Server".to_string(),
None => "Unknown".to_string(),
}
}
fn resolve_variable(&self, var: &str) -> String {
let trimmed = var.trim_matches('%');
self.variables
.get(trimmed)
.cloned()
.unwrap_or_else(|| var.to_string())
}
fn expand_variables(&self, path: &str) -> String {
let mut result = path.to_string();
for (key, value) in &self.variables {
let placeholder = format!("%{}%", key);
result = result.replace(&placeholder, value);
}
result
}
}
impl Default for X86SourceServer {
fn default() -> Self {
Self::new()
}
}
pub struct X86SymbolServer {
pub store: X86SymbolStore,
pub protocol: X86SymbolServerProtocol,
pub resolver: X86SymbolResolver,
pub uploader: X86SymbolUploader,
pub breakpad_converter: X86BreakpadConverter,
pub source_server: X86SourceServer,
}
impl X86SymbolServer {
pub fn new(store_root: &Path) -> Self {
let store = X86SymbolStore::new(store_root);
let protocol = X86SymbolServerProtocol::new(X86SymbolStore::new_memory());
let resolver =
X86SymbolResolver::new(X86SymbolServerProtocol::new(X86SymbolStore::new_memory()));
let uploader = X86SymbolUploader::new(
X86SymbolStore::new_memory(),
X86SymbolServerProtocol::new(X86SymbolStore::new_memory()),
);
Self {
store,
protocol,
resolver,
uploader,
breakpad_converter: X86BreakpadConverter::new(),
source_server: X86SourceServer::new(),
}
}
pub fn new_in_memory() -> Self {
Self::new(Path::new(":memory:"))
}
pub fn load_module(
&mut self,
data: &[u8],
name: &str,
image_base: u64,
image_size: u64,
) -> Result<(), String> {
let build_id = X86SymbolStore::parse_elf_build_id(data);
let codeview = X86SymbolStore::parse_pe_debug_directory(data, 0);
let macho_uuid = X86SymbolStore::parse_macho_uuid(data, 0);
let debug_id = if let Some(ref bid) = build_id {
X86DebugId::new(bid.clone(), 0, X86DebugIdKind::ElfBuildId)
} else if let Some(ref cv) = codeview {
cv.debug_id()
} else if let Some(uuid) = macho_uuid {
X86DebugId::new(uuid.to_vec(), 0, X86DebugIdKind::MachOUuid)
} else {
let hash = fnv1a_32(data);
X86DebugId::new(hash.to_le_bytes().to_vec(), 0, X86DebugIdKind::Breakpad)
};
let mut descriptor = X86ModuleDescriptor::new(name, debug_id, image_base, image_size);
descriptor.elf_build_id = build_id;
descriptor.codeview_info = codeview;
descriptor.macho_uuid = macho_uuid;
self.resolver.register_module(descriptor);
Ok(())
}
pub fn convert_to_breakpad(&self, module_name: &str, debug_id: &str) -> Option<String> {
for module in self.resolver.list_modules() {
if module.name.to_lowercase() == module_name.to_lowercase() {
if let Some(ref sym_path) = module.symbol_path {
if let Ok(data) = std::fs::read(sym_path) {
return Some(self.breakpad_converter.convert_dwarf_to_breakpad(
&data,
module_name,
debug_id,
));
}
}
}
}
None
}
pub fn index_sources(&self, source_files: &[String], pdb_name: &str) -> Vec<u8> {
self.source_server.index_sources(source_files, pdb_name)
}
pub fn generate_srcsrv_ini(&self) -> String {
self.source_server.generate_srcsrv_ini()
}
}
impl Default for X86SymbolServer {
fn default() -> Self {
Self::new_in_memory()
}
}
pub struct X86SymbolStreamParser {
buffer: Vec<u8>,
position: usize,
module_base: u64,
module_name: String,
}
pub type SymbolCallback = dyn FnMut(&str, u64, u64, X86SymbolKind, Option<&str>, Option<u32>);
impl X86SymbolStreamParser {
pub fn new(module_base: u64, module_name: &str) -> Self {
Self {
buffer: Vec::new(),
position: 0,
module_base,
module_name: module_name.to_string(),
}
}
pub fn feed(&mut self, data: &[u8]) {
self.buffer.extend_from_slice(data);
}
pub fn parse_breakpad(&mut self, cb: &mut SymbolCallback) -> usize {
if self.buffer.is_empty() {
return 0;
}
let text = match std::str::from_utf8(&self.buffer) {
Ok(t) => t,
Err(_) => return 0,
};
let mut count = 0;
for line in text.lines().skip(self.position) {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.is_empty() {
self.position += 1;
continue;
}
match parts[0] {
"FUNC" if parts.len() >= 5 => {
if let Ok(addr) = u64::from_str_radix(parts[1], 16) {
if let Ok(size) = u64::from_str_radix(parts[2], 16) {
let name = parts[4];
cb(
name,
self.module_base + addr,
size,
X86SymbolKind::Function,
None,
None,
);
count += 1;
}
}
}
"PUBLIC" if parts.len() >= 3 => {
if let Ok(addr) = u64::from_str_radix(parts[1], 16) {
let name = if parts.len() >= 4 { parts[3] } else { parts[2] };
cb(
name,
self.module_base + addr,
0,
X86SymbolKind::PublicSymbol,
None,
None,
);
count += 1;
}
}
_ => {}
}
self.position += 1;
}
count
}
pub fn parse_raw_text(&mut self, cb: &mut SymbolCallback) -> usize {
let text = match std::str::from_utf8(&self.buffer) {
Ok(t) => t,
Err(_) => return 0,
};
let mut count = 0;
for line in text.lines().skip(self.position) {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
if let Ok(addr) = u64::from_str_radix(parts[0], 16) {
let name = parts[1];
cb(
name,
self.module_base + addr,
1,
X86SymbolKind::Function,
None,
None,
);
count += 1;
}
}
self.position += 1;
}
count
}
pub fn reset(&mut self) {
self.buffer.clear();
self.position = 0;
}
}
pub struct X86SymbolNormalizer;
impl X86SymbolNormalizer {
pub fn normalize(name: &str) -> String {
let trimmed = name.trim();
let no_prefix = trimmed.trim_start_matches('_');
let no_suffix =
if no_prefix.contains('@') && no_prefix.ends_with(|c: char| c.is_ascii_digit()) {
let at_pos = no_prefix.rfind('@').unwrap_or(no_prefix.len());
&no_prefix[..at_pos]
} else {
no_prefix
};
no_suffix.to_lowercase()
}
pub fn are_equivalent(name1: &str, name2: &str) -> bool {
Self::normalize(name1) == Self::normalize(name2)
}
pub fn for_display(name: &str) -> String {
let mut result = String::with_capacity(name.len());
for ch in name.chars() {
if ch.is_ascii_graphic() || ch == ' ' {
result.push(ch);
}
}
result
}
pub fn base_name(name: &str) -> String {
if let Some(demangled) = X86Demangler::demangle(name) {
if let Some(last) = demangled.rsplit("::").next() {
return last.to_string();
}
return demangled;
}
name.rsplit("::").next().unwrap_or(name).to_string()
}
}
pub struct X86PDBStreamParser;
#[derive(Debug, Clone)]
pub struct X86PDBPublicSymbol {
pub name: String,
pub section: u16,
pub offset: u32,
pub flags: u32,
}
#[derive(Debug, Clone)]
pub struct X86PDBGlobalSymbol {
pub name: String,
pub section: u16,
pub offset: u32,
pub sym_type: u16,
pub debug_start: u32,
pub debug_end: u32,
}
impl X86PDBStreamParser {
pub fn parse_public_stream(data: &[u8]) -> Vec<X86PDBPublicSymbol> {
let mut symbols = Vec::new();
if data.len() < 8 {
return symbols;
}
let sym_hash_buckets = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let addr_hash_buckets = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
let tables_size = (sym_hash_buckets + addr_hash_buckets) * 4;
let mut pos = 8 + tables_size;
while pos + 8 <= data.len() {
let flags =
u32::from_le_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
let offset =
u32::from_le_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]]);
pos += 8;
let name_start = pos;
while pos < data.len() && data[pos] != 0 {
pos += 1;
}
let name = String::from_utf8_lossy(&data[name_start..pos]);
pos += 1;
let section = (flags & 0xFFFF) as u16;
symbols.push(X86PDBPublicSymbol {
name: name.to_string(),
section,
offset,
flags,
});
if pos >= data.len() {
break;
}
}
symbols
}
pub fn parse_global_stream(data: &[u8]) -> Vec<X86PDBGlobalSymbol> {
let mut symbols = Vec::new();
if data.len() < 4 {
return symbols;
}
let pos = 8;
let mut scan_pos = pos;
while scan_pos + 4 <= data.len() {
let record_len = u16::from_le_bytes([data[scan_pos], data[scan_pos + 1]]) as usize;
if record_len < 2 {
scan_pos += 2;
continue;
}
if scan_pos + 2 + record_len > data.len() {
break;
}
let kind = u16::from_le_bytes([data[scan_pos + 2], data[scan_pos + 3]]);
match kind {
0x110E => {
if record_len >= 16 {
let flags = u32::from_le_bytes([
data[scan_pos + 4],
data[scan_pos + 5],
data[scan_pos + 6],
data[scan_pos + 7],
]);
let offset = u32::from_le_bytes([
data[scan_pos + 8],
data[scan_pos + 9],
data[scan_pos + 10],
data[scan_pos + 11],
]);
let section = (flags & 0xFFFF) as u16;
let name_bytes = &data[scan_pos + 16..scan_pos + 2 + record_len];
let name = String::from_utf8_lossy(name_bytes)
.trim_end_matches('\0')
.to_string();
symbols.push(X86PDBGlobalSymbol {
name,
section,
offset,
sym_type: kind,
debug_start: 0,
debug_end: 0,
});
}
}
0x1110 => {
if record_len >= 36 {
let section = u16::from_le_bytes([data[scan_pos + 8], data[scan_pos + 9]]);
let offset = u32::from_le_bytes([
data[scan_pos + 12],
data[scan_pos + 13],
data[scan_pos + 14],
data[scan_pos + 15],
]);
let debug_start = u32::from_le_bytes([
data[scan_pos + 20],
data[scan_pos + 21],
data[scan_pos + 22],
data[scan_pos + 23],
]);
let debug_end = u32::from_le_bytes([
data[scan_pos + 24],
data[scan_pos + 25],
data[scan_pos + 26],
data[scan_pos + 27],
]);
let name_bytes = &data[scan_pos + 36..scan_pos + 2 + record_len];
let name = String::from_utf8_lossy(name_bytes)
.trim_end_matches('\0')
.to_string();
symbols.push(X86PDBGlobalSymbol {
name,
section,
offset,
sym_type: kind,
debug_start,
debug_end,
});
}
}
_ => {}
}
scan_pos += 2 + record_len;
if scan_pos % 4 != 0 {
scan_pos += 4 - (scan_pos % 4);
}
}
symbols
}
}
pub struct X86MachOReader {
data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct X86MachOInfo {
pub cputype: u32,
pub cpusubtype: u32,
pub filetype: u32,
pub ncmds: u32,
pub uuid: Option<[u8; 16]>,
pub dsym_path: Option<String>,
pub min_os_version: Option<String>,
pub is_64bit: bool,
}
impl X86MachOReader {
pub fn new(data: Vec<u8>) -> Self {
Self { data }
}
pub fn parse(&self) -> Option<X86MachOInfo> {
if self.data.len() < 28 {
return None;
}
let magic = u32::from_le_bytes([self.data[0], self.data[1], self.data[2], self.data[3]]);
if magic == 0xBEBAFECA {
return self.parse_fat_header();
}
let (is_64, is_be, offset) = match magic {
0xFEEDFACE => (false, true, 0),
0xCEFAEDFE => (false, false, 0),
0xFEEDFACF => (true, true, 0),
0xCFFAEDFE => (true, false, 0),
_ => return None,
};
let read_u32 = |off: usize| -> u32 {
if off + 4 > self.data.len() {
return 0;
}
if is_be {
u32::from_be_bytes([
self.data[off],
self.data[off + 1],
self.data[off + 2],
self.data[off + 3],
])
} else {
u32::from_le_bytes([
self.data[off],
self.data[off + 1],
self.data[off + 2],
self.data[off + 3],
])
}
};
let cputype = read_u32(offset + 4);
let cpusubtype = read_u32(offset + 8);
let filetype = read_u32(offset + 12);
let ncmds = read_u32(offset + 16);
let sizeofcmds = read_u32(offset + 20);
let hdr_size = if is_64 { 32 } else { 28 };
let mut cmd_off = offset + hdr_size;
let end_cmds = cmd_off + sizeofcmds as usize;
let mut uuid: Option<[u8; 16]> = None;
let mut dsym_path: Option<String> = None;
let mut min_os_version: Option<String> = None;
while cmd_off + 8 <= end_cmds && cmd_off + 8 <= self.data.len() {
let cmd = read_u32(cmd_off);
let cmdsize = read_u32(cmd_off + 4) as usize;
if cmdsize == 0 || cmd_off + cmdsize > self.data.len() {
break;
}
const LC_UUID: u32 = 0x1B;
const LC_VERSION_MIN_MACOSX: u32 = 0x24;
const LC_VERSION_MIN_IPHONEOS: u32 = 0x25;
const LC_BUILD_VERSION: u32 = 0x32;
match cmd {
LC_UUID => {
if cmd_off + 24 <= self.data.len() {
let mut id = [0u8; 16];
id.copy_from_slice(&self.data[cmd_off + 8..cmd_off + 24]);
uuid = Some(id);
}
}
LC_VERSION_MIN_MACOSX | LC_VERSION_MIN_IPHONEOS => {
if cmd_off + 16 <= self.data.len() {
let major = read_u32(cmd_off + 8) >> 16;
let minor = (read_u32(cmd_off + 8) >> 8) & 0xFF;
let patch = read_u32(cmd_off + 8) & 0xFF;
min_os_version = Some(format!("{}.{}.{}", major, minor, patch));
}
}
LC_BUILD_VERSION => {
if cmd_off + 24 <= self.data.len() {
let major = read_u32(cmd_off + 12) >> 16;
let minor = (read_u32(cmd_off + 12) >> 8) & 0xFF;
let patch = read_u32(cmd_off + 12) & 0xFF;
min_os_version = Some(format!("{}.{}.{}", major, minor, patch));
}
}
_ => {}
}
cmd_off += cmdsize;
}
Some(X86MachOInfo {
cputype,
cpusubtype,
filetype,
ncmds,
uuid,
dsym_path,
min_os_version,
is_64bit: is_64,
})
}
fn parse_fat_header(&self) -> Option<X86MachOInfo> {
if self.data.len() < 8 {
return None;
}
let nfat_arch =
u32::from_be_bytes([self.data[4], self.data[5], self.data[6], self.data[7]]) as usize;
for i in 0..nfat_arch {
let base = 8 + i * 20;
if base + 20 > self.data.len() {
break;
}
let cputype = u32::from_be_bytes([
self.data[base],
self.data[base + 1],
self.data[base + 2],
self.data[base + 3],
]);
let offset = u32::from_be_bytes([
self.data[base + 8],
self.data[base + 9],
self.data[base + 10],
self.data[base + 11],
]) as usize;
if cputype == 0x01000007 || cputype == 0x00000007 {
let slice_data = self.data[offset..].to_vec();
let reader = X86MachOReader::new(slice_data);
return reader.parse();
}
}
None
}
}
pub struct X86StackUnwinder {
resolver: X86SymbolResolver,
max_depth: usize,
}
#[derive(Debug, Clone)]
pub struct X86StackFrame {
pub ip: u64,
pub sp: u64,
pub bp: u64,
pub symbol: Option<X86SymbolInfo>,
pub source_location: Option<X86SourceLocation>,
pub inline_frames: Vec<X86InlineFrame>,
pub is_trustworthy: bool,
pub index: usize,
}
impl X86StackUnwinder {
pub fn new(resolver: X86SymbolResolver) -> Self {
Self {
resolver,
max_depth: 128,
}
}
pub fn set_max_depth(&mut self, depth: usize) {
self.max_depth = depth.min(512);
}
pub fn unwind(
&self,
ip: u64,
sp: u64,
bp: u64,
memory_reader: &dyn Fn(u64, &mut [u8]) -> usize,
) -> Vec<X86StackFrame> {
let mut frames = Vec::new();
let mut current_ip = ip;
let mut current_sp = sp;
let mut current_bp = bp;
for depth in 0..self.max_depth {
if current_ip == 0 {
break;
}
let symbol = self.resolver.resolve_function(current_ip);
let source_loc = self.resolver.resolve_source_location(current_ip);
let inline_frames = self.resolver.resolve_inline_frames(current_ip);
let is_trustworthy = symbol.is_some();
frames.push(X86StackFrame {
ip: current_ip,
sp: current_sp,
bp: current_bp,
symbol,
source_location: source_loc,
inline_frames,
is_trustworthy,
index: depth,
});
if current_bp == 0 || current_bp < current_sp {
break;
}
let mut frame_data = [0u8; 16];
let read = memory_reader(current_bp, &mut frame_data);
if read < 16 {
break;
}
let saved_bp = u64::from_le_bytes([
frame_data[0],
frame_data[1],
frame_data[2],
frame_data[3],
frame_data[4],
frame_data[5],
frame_data[6],
frame_data[7],
]);
let return_addr = u64::from_le_bytes([
frame_data[8],
frame_data[9],
frame_data[10],
frame_data[11],
frame_data[12],
frame_data[13],
frame_data[14],
frame_data[15],
]);
if return_addr == 0 || saved_bp < current_bp {
break;
}
current_ip = return_addr;
current_bp = saved_bp;
current_sp = current_bp + 16; }
frames
}
}
pub struct X86SymbolExporter;
impl X86SymbolExporter {
pub fn to_text(index: &X86SymbolIndex) -> String {
let symbols = index.lookup_by_name("");
let mut sorted: Vec<_> = symbols.iter().collect();
sorted.sort_by_key(|s| s.address);
let mut output = String::new();
for sym in sorted {
output.push_str(&format!("{:016x} {}\n", sym.address, sym.name));
}
output
}
pub fn to_nm(index: &X86SymbolIndex) -> String {
let symbols = index.lookup_by_name("");
let mut sorted: Vec<_> = symbols.iter().collect();
sorted.sort_by_key(|s| s.address);
let mut output = String::new();
for sym in sorted {
let type_char = match sym.kind {
X86SymbolKind::Function => 'T',
X86SymbolKind::PublicSymbol => 'T',
X86SymbolKind::Data => 'D',
_ => '?',
};
output.push_str(&format!(
"{:016x} {} {}\n",
sym.address, type_char, sym.name
));
}
output
}
pub fn to_map(index: &X86SymbolIndex) -> String {
let symbols = index.lookup_by_name("");
let mut sorted: Vec<_> = symbols.iter().collect();
sorted.sort_by_key(|s| s.address);
let mut output = String::new();
output.push_str(" Address Size Name Source\n");
output.push_str(" ------- ---- ---- ------\n");
for sym in sorted {
let source = match (&sym.source_file, sym.source_line) {
(Some(f), Some(l)) => format!("{}:{}", f, l),
(Some(f), None) => f.clone(),
_ => String::new(),
};
output.push_str(&format!(
" {:016x} {:08x} {:32} {}\n",
sym.address, sym.size, sym.name, source,
));
}
output
}
pub fn to_json(index: &X86SymbolIndex) -> String {
let symbols = index.lookup_by_name("");
let mut output = String::from("[\n");
for (i, sym) in symbols.iter().enumerate() {
if i > 0 {
output.push_str(",\n");
}
output.push_str(" {\n");
output.push_str(&format!(" \"name\": \"{}\",\n", sym.name));
output.push_str(&format!(" \"address\": \"0x{:x}\",\n", sym.address));
output.push_str(&format!(" \"size\": {},\n", sym.size));
output.push_str(&format!(" \"kind\": \"{:?}\"", sym.kind));
if let Some(ref sf) = sym.source_file {
output.push_str(",\n");
output.push_str(&format!(" \"source_file\": \"{}\"", sf));
}
if let Some(line) = sym.source_line {
output.push_str(",\n");
output.push_str(&format!(" \"source_line\": {}", line));
}
output.push('\n');
output.push_str(" }");
}
output.push_str("\n]\n");
output
}
}
#[cfg(test)]
mod more_extended_tests {
use super::*;
#[test]
fn test_stream_parser_breakpad() {
let mut parser = X86SymbolStreamParser::new(0x400000, "test.so");
parser.feed(b"FUNC 1000 50 0 func1\nFUNC 2000 30 2 func2\n");
let mut symbols = Vec::new();
let count = parser.parse_breakpad(&mut |name, addr, size, kind, sf, sl| {
symbols.push((name.to_string(), addr, size, kind));
});
assert_eq!(count, 2);
assert_eq!(symbols[0].0, "func1");
assert_eq!(symbols[0].1, 0x401000);
}
#[test]
fn test_stream_parser_raw_text() {
let mut parser = X86SymbolStreamParser::new(0, "test");
parser.feed(b"1000 alpha\n2000 beta\n");
let mut symbols = Vec::new();
let count = parser.parse_raw_text(&mut |name, addr, size, kind, sf, sl| {
symbols.push((name.to_string(), addr));
});
assert_eq!(count, 2);
assert_eq!(symbols[0].0, "alpha");
}
#[test]
fn test_normalizer_leading_underscore() {
let n = X86SymbolNormalizer::normalize("_main");
assert_eq!(n, "main");
}
#[test]
fn test_normalizer_msvc_suffix() {
let n = X86SymbolNormalizer::normalize("_function@4");
assert_eq!(n, "function");
}
#[test]
fn test_normalizer_are_equivalent() {
assert!(X86SymbolNormalizer::are_equivalent("_main", "main"));
assert!(X86SymbolNormalizer::are_equivalent("func@8", "func"));
assert!(!X86SymbolNormalizer::are_equivalent("foo", "bar"));
}
#[test]
fn test_normalizer_base_name() {
let name = X86SymbolNormalizer::base_name("namespace::class::method");
assert_eq!(name, "method");
}
#[test]
fn test_pdb_public_stream_empty() {
let symbols = X86PDBStreamParser::parse_public_stream(&[]);
assert!(symbols.is_empty());
}
#[test]
fn test_pdb_public_stream_minimal() {
let mut data = vec![0u8; 32];
data[0..4].copy_from_slice(&0u32.to_le_bytes());
data[4..8].copy_from_slice(&0u32.to_le_bytes());
data[8..12].copy_from_slice(&1u32.to_le_bytes()); data[12..16].copy_from_slice(&0x1000u32.to_le_bytes()); data[16] = b's';
data[17] = b'y';
data[18] = b'm';
data[19] = b'1';
data[20] = 0;
let symbols = X86PDBStreamParser::parse_public_stream(&data);
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0].name, "sym1");
}
#[test]
fn test_pdb_global_stream_empty() {
let symbols = X86PDBStreamParser::parse_global_stream(&[]);
assert!(symbols.is_empty());
}
#[test]
fn test_macho_reader_empty() {
let reader = X86MachOReader::new(vec![]);
assert!(reader.parse().is_none());
}
#[test]
fn test_macho_reader_fat() {
let mut data = vec![0u8; 128];
data[0..4].copy_from_slice(&0xBEBAFECAu32.to_be_bytes()); data[4..8].copy_from_slice(&1u32.to_be_bytes()); data[8..12].copy_from_slice(&0x01000007u32.to_be_bytes());
data[16..20].copy_from_slice(&32u32.to_be_bytes());
data[32..36].copy_from_slice(&0xFEEDFACFu32.to_le_bytes());
let reader = X86MachOReader::new(data);
let info = reader.parse();
assert!(info.is_some());
}
#[test]
fn test_unwinder_empty() {
let proto = X86SymbolServerProtocol::new(X86SymbolStore::new_memory());
let resolver = X86SymbolResolver::new(proto);
let unwinder = X86StackUnwinder::new(resolver);
let frames = unwinder.unwind(0x1000, 0x7FFE, 0x7FF0, &|addr, buf| {
0
});
assert!(!frames.is_empty());
}
#[test]
fn test_exporter_text() {
let mut idx = X86SymbolIndex::new();
idx.add_symbol(
"alpha",
0x1000,
1,
X86SymbolKind::Function,
None,
None,
None,
);
idx.add_symbol("beta", 0x2000, 1, X86SymbolKind::Data, None, None, None);
idx.build();
let text = X86SymbolExporter::to_text(&idx);
assert!(text.contains("alpha"));
assert!(text.contains("beta"));
}
#[test]
fn test_exporter_nm() {
let mut idx = X86SymbolIndex::new();
idx.add_symbol("f", 0x1000, 1, X86SymbolKind::Function, None, None, None);
idx.add_symbol("d", 0x2000, 1, X86SymbolKind::Data, None, None, None);
idx.build();
let nm = X86SymbolExporter::to_nm(&idx);
assert!(nm.contains(" T "));
assert!(nm.contains(" D "));
}
#[test]
fn test_exporter_map() {
let mut idx = X86SymbolIndex::new();
idx.add_symbol(
"main",
0x4000,
0x100,
X86SymbolKind::Function,
Some("main.c"),
Some(10),
None,
);
idx.build();
let map = X86SymbolExporter::to_map(&idx);
assert!(map.contains("main"));
assert!(map.contains("main.c:10"));
}
#[test]
fn test_exporter_json() {
let mut idx = X86SymbolIndex::new();
idx.add_symbol(
"f1",
0x1000,
50,
X86SymbolKind::Function,
Some("a.c"),
Some(1),
None,
);
idx.build();
let json = X86SymbolExporter::to_json(&idx);
assert!(json.contains("\"name\": \"f1\""));
assert!(json.contains("\"address\""));
assert!(json.starts_with('['));
}
#[test]
fn test_normalizer_display() {
let d = X86SymbolNormalizer::for_display("hello\x00world");
assert!(!d.contains('\x00'));
}
#[test]
fn test_stream_parser_reset() {
let mut parser = X86SymbolStreamParser::new(0, "test");
parser.feed(b"1000 func\n");
parser.reset();
assert_eq!(parser.buffer.len(), 0);
assert_eq!(parser.position, 0);
}
#[test]
fn test_macho_reader_uuid() {
let mut data = vec![0u8; 256];
data[0..4].copy_from_slice(&0xFEEDFACFu32.to_le_bytes());
data[4..8].copy_from_slice(&0x01000007u32.to_le_bytes()); data[12..16].copy_from_slice(&2u32.to_le_bytes()); data[16..20].copy_from_slice(&1u32.to_le_bytes()); data[20..24].copy_from_slice(&24u32.to_le_bytes()); data[32..36].copy_from_slice(&0x1Bu32.to_le_bytes()); data[36..40].copy_from_slice(&24u32.to_le_bytes()); for i in 0..16 {
data[40 + i] = (i + 1) as u8;
}
let reader = X86MachOReader::new(data);
let info = reader.parse();
assert!(info.is_some());
let info = info.unwrap();
assert!(info.uuid.is_some());
assert_eq!(info.uuid.unwrap()[0], 1);
}
#[test]
fn test_macho_reader_bad_magic() {
let reader = X86MachOReader::new(vec![0xDE, 0xAD, 0xBE, 0xEF]);
assert!(reader.parse().is_none());
}
#[test]
fn test_stack_unwinder_depth() {
let proto = X86SymbolServerProtocol::new(X86SymbolStore::new_memory());
let resolver = X86SymbolResolver::new(proto);
let mut unwinder = X86StackUnwinder::new(resolver);
unwinder.set_max_depth(5);
let frames = unwinder.unwind(0x4000, 0x7FF0, 0x7FE0, &|addr, buf| {
for b in buf.iter_mut() {
*b = 0;
}
buf.len()
});
assert!(!frames.is_empty());
}
}
pub struct X86Demangler;
impl X86Demangler {
pub fn demangle(name: &str) -> Option<String> {
if name.is_empty() {
return None;
}
if name.starts_with('?') {
return Self::demangle_msvc(name);
}
if name.starts_with("_Z") {
return Self::demangle_itanium(name);
}
if name.starts_with("_R") {
return Self::demangle_rust(name);
}
if name.starts_with("_D") && name.len() > 2 {
return Self::demangle_d(name);
}
None
}
fn demangle_itanium(name: &str) -> Option<String> {
if name == "_Z" || name.len() < 3 {
return None;
}
let rest = &name[2..];
if rest.starts_with('N') {
let inner = &rest[1..];
let (parsed, _) = Self::parse_itanium_nested(inner);
return Some(parsed);
}
let (parsed, _) = Self::parse_itanium_name(rest);
if parsed.is_empty() {
None
} else {
Some(parsed)
}
}
fn parse_itanium_nested(input: &str) -> (String, usize) {
let mut result = String::new();
let mut pos = 0;
let bytes = input.as_bytes();
let mut first = true;
while pos < bytes.len() && bytes[pos] != b'E' {
if bytes[pos].is_ascii_digit() {
let mut len = 0usize;
while pos < bytes.len() && bytes[pos].is_ascii_digit() {
len = len * 10 + (bytes[pos] - b'0') as usize;
pos += 1;
}
if pos + len > bytes.len() {
break;
}
if !first {
result.push_str("::");
}
first = false;
result.push_str(&input[pos..pos + len]);
pos += len;
} else {
match bytes[pos] {
b'S' => {
if !first {
result.push_str("::");
}
first = false;
result.push_str("<substitution>");
pos += 1;
}
b'C' => {
pos += 1;
}
b'D' => {
pos += 1;
}
_ => {
pos += 1;
}
}
}
}
if pos < bytes.len() && bytes[pos] == b'E' {
pos += 1;
}
(result, pos)
}
fn parse_itanium_name(input: &str) -> (String, usize) {
let bytes = input.as_bytes();
let mut pos = 0;
let mut result = String::new();
if pos < bytes.len() && bytes[pos].is_ascii_digit() {
let mut len = 0usize;
while pos < bytes.len() && bytes[pos].is_ascii_digit() {
len = len * 10 + (bytes[pos] - b'0') as usize;
pos += 1;
}
if pos + len <= bytes.len() {
result.push_str(&input[pos..pos + len]);
pos += len;
}
}
(result, pos)
}
fn demangle_msvc(name: &str) -> Option<String> {
if name.len() < 2 {
return None;
}
let bytes = name.as_bytes();
let mut result = String::new();
let mut pos = 1;
while pos < bytes.len() {
match bytes[pos] {
b'?' => {
if pos + 1 < bytes.len() {
match bytes[pos + 1] {
b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'$' => {
pos += 1;
}
b'?' => {
if !result.is_empty() {
result.push_str("::");
}
pos += 1;
}
_ => {
pos += 1;
}
}
} else {
pos += 1;
}
}
b'@' => {
pos += 1;
if pos < bytes.len() && bytes[pos] == b'@' {
pos += 1;
}
}
b'0'..=b'9' => {
let mut len = (bytes[pos] - b'0') as usize;
pos += 1;
while pos < bytes.len() && bytes[pos].is_ascii_digit() {
len = len * 10 + (bytes[pos] - b'0') as usize;
pos += 1;
}
if pos + len <= bytes.len() {
if !result.is_empty() && !result.ends_with("::") {
result.push_str("::");
}
result.push_str(&name[pos..pos + len]);
pos += len;
}
}
_ => {
let start = pos;
while pos < bytes.len()
&& (bytes[pos].is_ascii_alphanumeric()
|| bytes[pos] == b'_'
|| bytes[pos] == b'$')
{
pos += 1;
}
if pos > start {
if !result.is_empty() && !result.ends_with("::") {
result.push_str("::");
}
result.push_str(&name[start..pos]);
} else {
pos += 1;
}
}
}
}
if result.is_empty() {
None
} else {
Some(result)
}
}
fn demangle_rust(name: &str) -> Option<String> {
if !name.starts_with("_R") {
return None;
}
let rest = &name[2..];
let parts: Vec<&str> = rest.split('$').collect();
if parts.is_empty() {
return None;
}
let mut result = String::new();
for (i, part) in parts.iter().enumerate() {
if i == 0 {
if part.len() > 3 {
let path_part = &part[3..];
result.push_str(&path_part.replace("..", "::"));
}
} else {
result.push_str("::");
result.push_str(part);
}
}
if result.is_empty() {
None
} else {
Some(result)
}
}
fn demangle_d(name: &str) -> Option<String> {
let rest = &name[2..];
let mut result = String::new();
let bytes = rest.as_bytes();
let mut pos = 0;
while pos < bytes.len() {
if bytes[pos].is_ascii_digit() {
let mut len = 0usize;
while pos < bytes.len() && bytes[pos].is_ascii_digit() {
len = len * 10 + (bytes[pos] - b'0') as usize;
pos += 1;
}
if pos + len <= bytes.len() {
if !result.is_empty() {
result.push('.');
}
result.push_str(&rest[pos..pos + len]);
pos += len;
} else {
break;
}
} else {
pos += 1;
}
}
if result.is_empty() {
None
} else {
Some(result)
}
}
pub fn is_mangled(name: &str) -> bool {
name.starts_with('?')
|| name.starts_with("_Z")
|| name.starts_with("_R")
|| name.starts_with("__Z")
|| name.starts_with("_D")
}
pub fn clean_mangled(name: &str) -> String {
let cleaned = name.trim_start_matches('_').trim_start_matches('_');
if cleaned.starts_with('Z') {
cleaned[1..].to_string()
} else {
cleaned.to_string()
}
}
}
#[derive(Debug, Clone)]
pub struct X86RuntimeFunction {
pub begin_address: u32,
pub end_address: u32,
pub unwind_info_address: u32,
}
impl X86RuntimeFunction {
pub const SIZE: usize = 12;
pub fn parse_all(data: &[u8]) -> Vec<Self> {
data.chunks(Self::SIZE)
.filter(|chunk| chunk.len() == Self::SIZE)
.map(|chunk| Self {
begin_address: u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]),
end_address: u32::from_le_bytes([chunk[4], chunk[5], chunk[6], chunk[7]]),
unwind_info_address: u32::from_le_bytes([chunk[8], chunk[9], chunk[10], chunk[11]]),
})
.collect()
}
pub fn to_symbols(&self, module_name: &str) -> Vec<(String, u64, u64, X86SymbolKind)> {
let name = format!("func_{:x}", self.begin_address);
let size = (self.end_address - self.begin_address) as u64;
vec![(
name,
self.begin_address as u64,
size,
X86SymbolKind::Function,
)]
}
}
#[derive(Debug, Clone)]
pub struct X86UnwindInfo {
pub version: u8,
pub flags: u8,
pub size_of_prologue: u8,
pub count_of_codes: u8,
pub frame_register: u8,
pub frame_offset: u8,
pub unwind_codes: Vec<X86UnwindCode>,
}
#[derive(Debug, Clone)]
pub struct X86UnwindCode {
pub code_offset: u8,
pub unwind_op: u8,
pub op_info: u8,
}
impl X86UnwindInfo {
pub fn parse(data: &[u8]) -> Option<Self> {
if data.len() < 4 {
return None;
}
let version_and_flags = data[0];
let version = version_and_flags & 0x07;
let flags = (version_and_flags >> 3) & 0x1F;
let size_of_prologue = data[1];
let count_of_codes = data[2];
let frame_register_and_offset = data[3];
let frame_register = frame_register_and_offset & 0x0F;
let frame_offset = (frame_register_and_offset >> 4) & 0x0F;
let code_count = count_of_codes as usize;
let code_bytes = 2 * code_count;
let code_start = 4;
if data.len() < code_start + code_bytes {
return None;
}
let mut unwind_codes = Vec::with_capacity(code_count);
for i in 0..code_count {
let off = code_start + i * 2;
let code_offset = data[off];
let unwind_op_and_info = data[off + 1];
let unwind_op = unwind_op_and_info & 0x0F;
let op_info = (unwind_op_and_info >> 4) & 0x0F;
unwind_codes.push(X86UnwindCode {
code_offset,
unwind_op,
op_info,
});
}
Some(Self {
version,
flags,
size_of_prologue,
count_of_codes,
frame_register,
frame_offset,
unwind_codes,
})
}
pub fn to_breakpad_cfi(&self, function_addr: u64, function_size: u64) -> Vec<String> {
let mut records = Vec::new();
let mut rules = Vec::new();
if self.frame_register == 0 {
rules.push(format!(".cfa: $rsp 8 +"));
} else {
rules.push(format!(".cfa: $rbp 16 +"));
}
rules.push(format!(".ra: .cfa -8 + ^"));
records.push(format!(
"{} {:x} {:x} {}",
BREAKPAD_STACK_CFI_INIT,
function_addr,
function_size,
rules.join(" "),
));
let mut current_offset = 0u32;
for code in &self.unwind_codes {
if code.code_offset as u32 > current_offset {
let addr = function_addr + code.code_offset as u64;
let mut code_rules = Vec::new();
match code.unwind_op {
0 => {
let reg = Self::x64_reg_name(code.op_info);
code_rules.push(format!(".cfa: $rsp 16 +"));
if !reg.is_empty() {
code_rules.push(format!("{}: .cfa -{} + ^", reg, (current_offset + 8)));
}
}
1 => {
code_rules.push(format!(".cfa: $rsp {} +", code.op_info as u64 * 8 + 8));
}
2 => {
code_rules.push(format!(".cfa: $rsp {} +", code.op_info as u64 * 8 + 8));
}
3 => {
code_rules.push(format!(".cfa: $rbp 16 +"));
}
4 => {
let reg = Self::x64_reg_name(code.op_info);
if !reg.is_empty() {
code_rules.push(format!("{}: .cfa {} +", reg, current_offset));
}
}
_ => {}
}
if !code_rules.is_empty() {
records.push(format!(
"{} {:x} {}",
BREAKPAD_STACK_CFI_MARKER,
addr,
code_rules.join(" "),
));
}
current_offset = code.code_offset as u32;
}
}
records
}
fn x64_reg_name(reg: u8) -> String {
match reg {
0 => "$rax",
1 => "$rcx",
2 => "$rdx",
3 => "$rbx",
4 => "$rsp",
5 => "$rbp",
6 => "$rsi",
7 => "$rdi",
8 => "$r8",
9 => "$r9",
10 => "$r10",
11 => "$r11",
12 => "$r12",
13 => "$r13",
14 => "$r14",
15 => "$r15",
_ => "",
}
.to_string()
}
}
pub struct X86SymbolValidator;
#[derive(Debug, Clone)]
pub struct X86SymbolValidationResult {
pub is_valid: bool,
pub error_count: usize,
pub warning_count: usize,
pub messages: Vec<X86ValidationMessage>,
}
#[derive(Debug, Clone)]
pub struct X86ValidationMessage {
pub level: X86ValidationLevel,
pub message: String,
pub symbol: Option<String>,
pub address: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X86ValidationLevel {
Info,
Warning,
Error,
}
impl X86SymbolValidator {
pub fn validate_index(index: &X86SymbolIndex) -> X86SymbolValidationResult {
let mut messages = Vec::new();
let mut errors = 0;
let mut warnings = 0;
if index.is_empty() {
messages.push(X86ValidationMessage {
level: X86ValidationLevel::Warning,
message: "Symbol index is empty".to_string(),
symbol: None,
address: None,
});
warnings += 1;
}
let result = X86SymbolValidationResult {
is_valid: errors == 0,
error_count: errors,
warning_count: warnings,
messages,
};
result
}
pub fn validate_breakpad(text: &str) -> X86SymbolValidationResult {
let mut messages = Vec::new();
let mut errors = 0;
let mut warnings = 0;
let mut line_num = 0u32;
let mut has_module = false;
let mut has_func_or_public = false;
for line in text.lines() {
line_num += 1;
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.is_empty() {
continue;
}
match parts[0] {
"MODULE" => {
has_module = true;
if parts.len() < 5 {
messages.push(X86ValidationMessage {
level: X86ValidationLevel::Error,
message: format!(
"MODULE record at line {} has insufficient fields",
line_num
),
symbol: None,
address: None,
});
errors += 1;
}
}
"INFO" => {
if parts.len() < 3 {
messages.push(X86ValidationMessage {
level: X86ValidationLevel::Warning,
message: format!(
"INFO record at line {} has insufficient fields",
line_num
),
symbol: None,
address: None,
});
warnings += 1;
}
}
"FILE" => {
if parts.len() < 3 {
warnings += 1;
}
}
"FUNC" | "PUBLIC" => {
has_func_or_public = true;
if parts.len() < 3 {
errors += 1;
}
if parts.len() >= 2 {
if u64::from_str_radix(parts[1], 16).is_err() {
messages.push(X86ValidationMessage {
level: X86ValidationLevel::Error,
message: format!(
"Invalid address in {} record at line {}",
parts[0], line_num
),
symbol: parts.get(parts.len() - 1).map(|s| s.to_string()),
address: None,
});
errors += 1;
}
}
}
"STACK" => {
if parts.len() >= 2 {
if u64::from_str_radix(parts[1], 16).is_err() {
warnings += 1;
}
}
}
_ => {
if parts.len() >= 4 {
if u64::from_str_radix(parts[0], 16).is_err() {
warnings += 1;
}
}
}
}
}
if !has_module {
messages.push(X86ValidationMessage {
level: X86ValidationLevel::Error,
message: "Missing MODULE record".to_string(),
symbol: None,
address: None,
});
errors += 1;
}
if !has_func_or_public {
messages.push(X86ValidationMessage {
level: X86ValidationLevel::Warning,
message: "No FUNC or PUBLIC records found".to_string(),
symbol: None,
address: None,
});
warnings += 1;
}
X86SymbolValidationResult {
is_valid: errors == 0,
error_count: errors,
warning_count: warnings,
messages,
}
}
pub fn validate_codeview(info: &X86CodeViewInfo) -> X86SymbolValidationResult {
let mut messages = Vec::new();
let mut errors = 0;
let mut warnings = 0;
if info.pdb_path.is_empty() {
messages.push(X86ValidationMessage {
level: X86ValidationLevel::Warning,
message: "Empty PDB path in CodeView info".to_string(),
symbol: None,
address: None,
});
warnings += 1;
}
if info.signature != CV_SIGNATURE_RSDS && info.signature != CV_SIGNATURE_NB10 {
messages.push(X86ValidationMessage {
level: X86ValidationLevel::Warning,
message: format!("Unknown CodeView signature: 0x{:08X}", info.signature),
symbol: None,
address: None,
});
warnings += 1;
}
if info.guid.iter().all(|&b| b == 0) {
messages.push(X86ValidationMessage {
level: X86ValidationLevel::Warning,
message: "Zero GUID in CodeView info".to_string(),
symbol: None,
address: None,
});
warnings += 1;
}
X86SymbolValidationResult {
is_valid: errors == 0,
error_count: errors,
warning_count: warnings,
messages,
}
}
}
pub struct X86SymbolMerge;
impl X86SymbolMerge {
pub fn merge(indices: &[&X86SymbolIndex], module_name: &str) -> X86SymbolIndex {
let mut merged = X86SymbolIndex::new();
let mut all_symbols: Vec<(String, u64, u64, X86SymbolKind, Option<String>, Option<u32>)> =
Vec::new();
for idx in indices {
let results = idx.lookup_by_name("");
for sym in results {
all_symbols.push((
sym.name,
sym.address,
sym.size,
sym.kind,
sym.source_file.clone(),
sym.source_line,
));
}
}
all_symbols.sort_by_key(|s| s.1);
let mut last_end = 0u64;
for (name, addr, size, kind, sf, sl) in all_symbols {
if addr < last_end {
continue;
}
merged.add_symbol(
&name,
addr,
size,
kind,
sf.as_deref(),
sl,
Some(module_name),
);
last_end = addr.saturating_add(size);
}
merged.build();
merged
}
}
pub struct X86PeParser {
data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct X86PeHeaders {
pub machine: u16,
pub num_sections: u16,
pub timestamp: u32,
pub optional_header_size: u16,
pub characteristics: u16,
pub pe_magic: u16,
pub image_base: u64,
pub size_of_image: u32,
pub entry_point: u32,
pub debug_directory_rva: u32,
pub debug_directory_size: u32,
}
impl X86PeParser {
pub fn new(data: Vec<u8>) -> Self {
Self { data }
}
pub fn parse_headers(&self) -> Option<X86PeHeaders> {
if self.data.len() < 64 || self.data[0] != b'M' || self.data[1] != b'Z' {
return None;
}
let pe_offset =
u32::from_le_bytes([self.data[60], self.data[61], self.data[62], self.data[63]])
as usize;
if pe_offset + 4 > self.data.len() || &self.data[pe_offset..pe_offset + 4] != b"PE\0\0" {
return None;
}
let coff = pe_offset + 4;
if coff + 20 > self.data.len() {
return None;
}
let machine = u16::from_le_bytes([self.data[coff], self.data[coff + 1]]);
let num_sections = u16::from_le_bytes([self.data[coff + 2], self.data[coff + 3]]);
let timestamp = u32::from_le_bytes([
self.data[coff + 4],
self.data[coff + 5],
self.data[coff + 6],
self.data[coff + 7],
]);
let optional_header_size = u16::from_le_bytes([self.data[coff + 16], self.data[coff + 17]]);
let characteristics = u16::from_le_bytes([self.data[coff + 18], self.data[coff + 19]]);
let opt = coff + 20;
if opt + 2 > self.data.len() {
return None;
}
let pe_magic = u16::from_le_bytes([self.data[opt], self.data[opt + 1]]);
let (image_base, size_of_image, entry_point, debug_dir_rva, debug_dir_size) = match pe_magic
{
0x10B => {
if opt + 96 > self.data.len() {
return None;
}
let ib = u32::from_le_bytes([
self.data[opt + 28],
self.data[opt + 29],
self.data[opt + 30],
self.data[opt + 31],
]) as u64;
let si = u32::from_le_bytes([
self.data[opt + 56],
self.data[opt + 57],
self.data[opt + 58],
self.data[opt + 59],
]);
let ep = u32::from_le_bytes([
self.data[opt + 16],
self.data[opt + 17],
self.data[opt + 18],
self.data[opt + 19],
]);
let dd_rva = u32::from_le_bytes([
self.data[opt + 88],
self.data[opt + 89],
self.data[opt + 90],
self.data[opt + 91],
]);
let dd_size = u32::from_le_bytes([
self.data[opt + 92],
self.data[opt + 93],
self.data[opt + 94],
self.data[opt + 95],
]);
(ib, si, ep, dd_rva, dd_size)
}
0x20B => {
if opt + 112 > self.data.len() {
return None;
}
let ib = u64::from_le_bytes([
self.data[opt + 24],
self.data[opt + 25],
self.data[opt + 26],
self.data[opt + 27],
self.data[opt + 28],
self.data[opt + 29],
self.data[opt + 30],
self.data[opt + 31],
]);
let si = u32::from_le_bytes([
self.data[opt + 56],
self.data[opt + 57],
self.data[opt + 58],
self.data[opt + 59],
]);
let ep = u32::from_le_bytes([
self.data[opt + 16],
self.data[opt + 17],
self.data[opt + 18],
self.data[opt + 19],
]);
let dd_rva = u32::from_le_bytes([
self.data[opt + 104],
self.data[opt + 105],
self.data[opt + 106],
self.data[opt + 107],
]);
let dd_size = u32::from_le_bytes([
self.data[opt + 108],
self.data[opt + 109],
self.data[opt + 110],
self.data[opt + 111],
]);
(ib, si, ep, dd_rva, dd_size)
}
_ => return None,
};
Some(X86PeHeaders {
machine,
num_sections,
timestamp,
optional_header_size,
characteristics,
pe_magic,
image_base,
size_of_image,
entry_point,
debug_directory_rva: debug_dir_rva,
debug_directory_size: debug_dir_size,
})
}
pub fn extract_codeview_entries(&self) -> Vec<X86CodeViewInfo> {
let headers = match self.parse_headers() {
Some(h) => h,
None => return Vec::new(),
};
let dd_rva = headers.debug_directory_rva;
let dd_size = headers.debug_directory_size;
if dd_rva == 0 || dd_size == 0 {
return Vec::new();
}
let rva_to_offset = |rva: u32| -> Option<usize> {
let coff = 64; let pe_off =
u32::from_le_bytes([self.data[60], self.data[61], self.data[62], self.data[63]])
as usize;
let opt_size =
u16::from_le_bytes([self.data[pe_off + 4 + 16], self.data[pe_off + 4 + 17]])
as usize;
let section_start = pe_off + 4 + 20 + opt_size;
for i in 0..headers.num_sections as usize {
let sec = section_start + i * 40;
if sec + 40 > self.data.len() {
break;
}
let va = u32::from_le_bytes([
self.data[sec + 12],
self.data[sec + 13],
self.data[sec + 14],
self.data[sec + 15],
]);
let vs = u32::from_le_bytes([
self.data[sec + 8],
self.data[sec + 9],
self.data[sec + 10],
self.data[sec + 11],
]);
let raw = u32::from_le_bytes([
self.data[sec + 20],
self.data[sec + 21],
self.data[sec + 22],
self.data[sec + 23],
]);
if rva >= va && rva < va + vs {
return Some((raw + (rva - va)) as usize);
}
}
None
};
let offset = rva_to_offset(dd_rva)?;
let count = (dd_size as usize) / X86DebugDirectory::SIZE;
let mut entries = Vec::new();
for i in 0..count {
let dir_offset = offset + i * X86DebugDirectory::SIZE;
if let Some(cv) = X86SymbolStore::parse_pe_debug_directory(&self.data, dir_offset) {
entries.push(cv);
}
}
entries
}
pub fn get_module_name(&self) -> Option<String> {
let headers = self.parse_headers()?;
if headers.machine == 0x8664 {
Some("module_amd64.dll".to_string())
} else if headers.machine == 0x014C {
Some("module_i386.dll".to_string())
} else {
Some(format!("module_{:04x}.dll", headers.machine))
}
}
}
pub struct X86ElfSymbolTableParser;
#[derive(Debug, Clone)]
pub struct X86ElfSymbol {
pub name: String,
pub value: u64,
pub size: u64,
pub binding: u8,
pub sym_type: u8,
pub visibility: u8,
pub shndx: u16,
}
impl X86ElfSymbolTableParser {
pub fn parse(symtab_data: &[u8], strtab_data: &[u8], is_64bit: bool) -> Vec<X86ElfSymbol> {
let entry_size = if is_64bit { 24 } else { 16 };
let count = symtab_data.len() / entry_size;
let mut symbols = Vec::with_capacity(count);
for i in 0..count {
let base = i * entry_size;
if base + entry_size > symtab_data.len() {
break;
}
let (st_name, st_value, st_size, st_info, st_other, st_shndx) = if is_64bit {
let name = u32::from_le_bytes([
symtab_data[base],
symtab_data[base + 1],
symtab_data[base + 2],
symtab_data[base + 3],
]);
let info = symtab_data[base + 4];
let other = symtab_data[base + 5];
let shndx = u16::from_le_bytes([symtab_data[base + 6], symtab_data[base + 7]]);
let value = u64::from_le_bytes([
symtab_data[base + 8],
symtab_data[base + 9],
symtab_data[base + 10],
symtab_data[base + 11],
symtab_data[base + 12],
symtab_data[base + 13],
symtab_data[base + 14],
symtab_data[base + 15],
]);
let size = u64::from_le_bytes([
symtab_data[base + 16],
symtab_data[base + 17],
symtab_data[base + 18],
symtab_data[base + 19],
symtab_data[base + 20],
symtab_data[base + 21],
symtab_data[base + 22],
symtab_data[base + 23],
]);
(name, value, size, info, other, shndx)
} else {
let name = u32::from_le_bytes([
symtab_data[base],
symtab_data[base + 1],
symtab_data[base + 2],
symtab_data[base + 3],
]);
let value = u32::from_le_bytes([
symtab_data[base + 4],
symtab_data[base + 5],
symtab_data[base + 6],
symtab_data[base + 7],
]) as u64;
let size = u32::from_le_bytes([
symtab_data[base + 8],
symtab_data[base + 9],
symtab_data[base + 10],
symtab_data[base + 11],
]) as u64;
let info = symtab_data[base + 12];
let other = symtab_data[base + 13];
let shndx = u16::from_le_bytes([symtab_data[base + 14], symtab_data[base + 15]]);
(name, value, size, info, other, shndx)
};
let binding = st_info >> 4;
let sym_type = st_info & 0x0F;
let visibility = st_other & 0x03;
let sym_name = Self::read_strtab(strtab_data, st_name as usize);
symbols.push(X86ElfSymbol {
name: sym_name,
value: st_value,
size: st_size,
binding,
sym_type,
visibility,
shndx: st_shndx,
});
}
symbols
}
pub fn to_index_symbols(
symbols: &[X86ElfSymbol],
module_name: &str,
) -> Vec<(String, u64, u64, X86SymbolKind)> {
symbols
.iter()
.filter(|s| !s.name.is_empty() && s.size > 0)
.map(|s| {
let kind = match s.sym_type {
2 => X86SymbolKind::Function, 0 => X86SymbolKind::Unknown, 1 => X86SymbolKind::Data, _ => X86SymbolKind::Unknown,
};
(s.name.clone(), s.value, s.size, kind)
})
.collect()
}
fn read_strtab(data: &[u8], offset: usize) -> String {
if offset >= data.len() {
return String::new();
}
let end = data[offset..]
.iter()
.position(|&b| b == 0)
.unwrap_or(data.len() - offset);
String::from_utf8_lossy(&data[offset..offset + end]).to_string()
}
}
#[derive(Debug, Clone)]
pub struct X86SymbolServerConfig {
pub store_root: PathBuf,
pub http_servers: Vec<String>,
pub s3_bucket: Option<String>,
pub s3_region: Option<String>,
pub gcs_bucket: Option<String>,
pub cache_dir: Option<PathBuf>,
pub max_cache_size: u64,
pub timeout_ms: u64,
pub compress_symbols: bool,
pub compression_level: u32,
pub verify_signatures: bool,
pub auto_fetch: bool,
pub source_server_endpoints: Vec<String>,
}
impl Default for X86SymbolServerConfig {
fn default() -> Self {
Self {
store_root: PathBuf::from("/var/cache/symbols"),
http_servers: vec!["https://msdl.microsoft.com/download/symbols".to_string()],
s3_bucket: None,
s3_region: None,
gcs_bucket: None,
cache_dir: None,
max_cache_size: MAX_SYMBOL_CACHE_SIZE,
timeout_ms: DEFAULT_SYMBOL_SERVER_TIMEOUT_MS,
compress_symbols: true,
compression_level: 6,
verify_signatures: false,
auto_fetch: true,
source_server_endpoints: Vec::new(),
}
}
}
impl X86SymbolServerConfig {
pub fn build_server(&self) -> X86SymbolServer {
let mut server = X86SymbolServer::new(&self.store_root);
for url in &self.http_servers {
server
.protocol
.add_source(X86SymbolSource::HttpSymbolServer {
url: url.clone(),
priority: 10,
timeout_ms: self.timeout_ms,
});
}
if let (Some(ref bucket), Some(ref region)) = (&self.s3_bucket, &self.s3_region) {
server.protocol.add_source(X86SymbolSource::S3Store {
bucket: bucket.clone(),
region: region.clone(),
prefix: "symbols/".to_string(),
endpoint: None,
priority: 20,
});
}
if let Some(ref bucket) = self.gcs_bucket {
server.protocol.add_source(X86SymbolSource::GcsStore {
bucket: bucket.clone(),
prefix: "symbols/".to_string(),
priority: 30,
});
}
server
.uploader
.set_compress(self.compress_symbols, self.compression_level);
for endpoint in &self.source_server_endpoints {
server.source_server.add_http_endpoint(endpoint);
}
server
}
}
#[cfg(test)]
mod extended_tests {
use super::*;
#[test]
fn test_detect_pdb() {
let mut data = vec![0u8; 64];
data[..32].copy_from_slice(PDB_MAGIC);
assert_eq!(X86SymbolFileFormat::detect(&data), X86SymbolFileFormat::Pdb);
}
#[test]
fn test_detect_elf() {
let mut data = vec![0u8; 64];
data[..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
data[4] = 2;
data[5] = 1;
assert_eq!(
X86SymbolFileFormat::detect(&data),
X86SymbolFileFormat::DwarfElf
);
}
#[test]
fn test_detect_breakpad() {
let data = b"MODULE Linux x86_64 ABC123 libtest.so\nFUNC 1000 50 0 func1\n";
assert_eq!(
X86SymbolFileFormat::detect(data),
X86SymbolFileFormat::Breakpad
);
}
#[test]
fn test_detect_raw_symbol_table() {
let data = b"1000 main\n2000 foo\n3000 bar\n";
assert_eq!(
X86SymbolFileFormat::detect(data),
X86SymbolFileFormat::RawSymbolTable
);
}
#[test]
fn test_detect_empty() {
assert_eq!(
X86SymbolFileFormat::detect(&[]),
X86SymbolFileFormat::Unknown
);
}
#[test]
fn test_format_extensions() {
assert_eq!(X86SymbolFileFormat::Pdb.default_extension(), "pdb");
assert_eq!(X86SymbolFileFormat::Breakpad.default_extension(), "sym");
assert_eq!(X86SymbolFileFormat::DwarfElf.default_extension(), "debug");
assert_eq!(X86SymbolFileFormat::Unknown.default_extension(), "bin");
}
#[test]
fn test_debug_dir_parse() {
let mut data = vec![0u8; 128];
data[20..24].copy_from_slice(&100u32.to_le_bytes()); data[24..28].copy_from_slice(&64u32.to_le_bytes()); data[12..16].copy_from_slice(&IMAGE_DEBUG_TYPE_CODEVIEW.to_le_bytes());
data[16..20].copy_from_slice(&4u32.to_le_bytes()); data[64..68].copy_from_slice(&CV_SIGNATURE_RSDS.to_le_bytes());
let dir = X86DebugDirectory::parse(&data, 0);
assert!(dir.is_some());
assert_eq!(dir.unwrap().debug_type, IMAGE_DEBUG_TYPE_CODEVIEW);
}
#[test]
fn test_debug_dir_parse_all() {
let mut data = vec![0u8; 256];
data[12..16].copy_from_slice(&IMAGE_DEBUG_TYPE_CODEVIEW.to_le_bytes());
data[16..20].copy_from_slice(&10u32.to_le_bytes());
data[24..28].copy_from_slice(&128u32.to_le_bytes());
let dirs = X86DebugDirectory::parse_all(&data, 0, 5);
assert!(dirs.len() >= 1);
}
#[test]
fn test_cfi_init_record() {
let mut record = X86CFIInitRecord::new(0x1000, 0x200);
record.add_rule("$rsp", ".cfa 8 +");
record.add_rule("$rip", ".cfa -8 + ^");
let bp = record.to_breakpad();
assert!(bp.starts_with("STACK CFI INIT"));
assert!(bp.contains("$rsp"));
assert!(bp.contains("$rip"));
}
#[test]
fn test_cfi_record() {
let mut record = X86CFIRecord::new(0x1100);
record
.rules
.push(X86CFIRegisterRule::new("$rbx", ".cfa -16 + ^"));
let bp = record.to_breakpad();
assert!(bp.starts_with("STACK CFI"));
assert!(bp.contains("$rbx"));
}
#[test]
fn test_stack_win_record() {
let rec = X86StackWinRecord::new(0x1000, 0x300);
let bp = rec.to_breakpad();
assert!(bp.starts_with("STACK WIN"));
assert!(bp.contains("1000"));
}
#[test]
fn test_demangle_itanium() {
let result = X86Demangler::demangle("_Z3fooi");
assert!(result.is_some());
assert!(result.unwrap().contains("foo"));
}
#[test]
fn test_demangle_itanium_nested() {
let result = X86Demangler::demangle("_ZN3foo3barEi");
assert!(result.is_some());
}
#[test]
fn test_demangle_msvc() {
let result = X86Demangler::demangle("?func@@YAXH@Z");
assert!(result.is_some());
}
#[test]
fn test_demangle_rust() {
let result = X86Demangler::demangle("_RNvC5crate4main");
assert!(result.is_some());
}
#[test]
fn test_demangle_not_mangled() {
assert!(X86Demangler::demangle("plain_function").is_none());
assert!(X86Demangler::demangle("").is_none());
}
#[test]
fn test_is_mangled() {
assert!(X86Demangler::is_mangled("?msvc@@YAXXZ"));
assert!(X86Demangler::is_mangled("_Z3foo"));
assert!(X86Demangler::is_mangled("_RNvC5crate4main"));
assert!(!X86Demangler::is_mangled("plain_func"));
}
#[test]
fn test_clean_mangled() {
assert_eq!(X86Demangler::clean_mangled("_Z3foo"), "3foo");
assert_eq!(X86Demangler::clean_mangled("__Z3bar"), "3bar");
}
#[test]
fn test_runtime_function_parse() {
let mut data = Vec::new();
data.extend_from_slice(&0x1000u32.to_le_bytes()); data.extend_from_slice(&0x1200u32.to_le_bytes()); data.extend_from_slice(&0x2000u32.to_le_bytes());
let entries = X86RuntimeFunction::parse_all(&data);
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].begin_address, 0x1000);
assert_eq!(entries[0].end_address, 0x1200);
}
#[test]
fn test_unwind_info_parse() {
let mut data = vec![0u8; 16];
data[0] = 0x01; data[1] = 0x08; data[2] = 0x02; data[3] = 0x05; data[4] = 0x04;
data[5] = 0x03;
data[6] = 0x02;
data[7] = 0x24;
let info = X86UnwindInfo::parse(&data);
assert!(info.is_some());
let info = info.unwrap();
assert_eq!(info.version, 1);
assert_eq!(info.count_of_codes, 2);
assert_eq!(info.frame_register, 5);
assert_eq!(info.unwind_codes.len(), 2);
}
#[test]
fn test_unwind_info_to_breakpad_cfi() {
let mut data = vec![0u8; 16];
data[0] = 0x01;
data[1] = 0x08;
data[2] = 0x01;
data[3] = 0x05;
data[4] = 0x04;
data[5] = 0x03;
let info = X86UnwindInfo::parse(&data).unwrap();
let records = info.to_breakpad_cfi(0x1000, 0x200);
assert!(!records.is_empty());
assert!(records[0].contains("STACK CFI INIT"));
}
#[test]
fn test_validate_index_empty() {
let idx = X86SymbolIndex::new();
let result = X86SymbolValidator::validate_index(&idx);
assert_eq!(result.warning_count, 1);
}
#[test]
fn test_validate_breakpad_valid() {
let text = "MODULE Linux x86_64 ABC123 test.so\n$
INFO CODE_ID ABC123\n$
FUNC 1000 50 0 func1\n$
PUBLIC 1000 0 func1\n";
let result = X86SymbolValidator::validate_breakpad(text);
assert!(result.is_valid);
assert_eq!(result.error_count, 0);
}
#[test]
fn test_validate_breakpad_missing_module() {
let text = "FUNC 1000 50 0 func1\n";
let result = X86SymbolValidator::validate_breakpad(text);
assert!(!result.is_valid);
assert!(result.error_count > 0);
}
#[test]
fn test_validate_breakpad_bad_address() {
let text = "MODULE Linux x86_64 ABC test.so\nFUNC ZZZZ 50 0 func1\n";
let result = X86SymbolValidator::validate_breakpad(text);
assert!(!result.is_valid);
}
#[test]
fn test_validate_codeview() {
let cv = X86CodeViewInfo::new_pdb70([0x01; 16], 1, "test.pdb");
let result = X86SymbolValidator::validate_codeview(&cv);
assert!(result.is_valid);
let bad_cv = X86CodeViewInfo {
signature: 0xDEADBEEF,
guid: [0u8; 16],
age: 0,
pdb_path: String::new(),
debug_dir_offset: 0,
debug_dir_size: 0,
};
let result = X86SymbolValidator::validate_codeview(&bad_cv);
assert!(result.warning_count >= 2);
}
#[test]
fn test_merge_indices() {
let mut idx1 = X86SymbolIndex::new();
idx1.add_symbol("a", 0x1000, 0x50, X86SymbolKind::Function, None, None, None);
idx1.build();
let mut idx2 = X86SymbolIndex::new();
idx2.add_symbol("b", 0x2000, 0x50, X86SymbolKind::Function, None, None, None);
idx2.build();
let merged = X86SymbolMerge::merge(&[&idx1, &idx2], "merged.so");
assert_eq!(merged.len(), 2);
}
#[test]
fn test_pe_parser_not_pe() {
let parser = X86PeParser::new(vec![0; 64]);
assert!(parser.parse_headers().is_none());
}
#[test]
fn test_pe_parser_minimal() {
let mut data = vec![0u8; 512];
data[0] = b'M';
data[1] = b'Z';
data[60] = 128;
data[61] = 0;
data[62] = 0;
data[63] = 0;
data[128] = b'P';
data[129] = b'E';
data[130] = 0;
data[131] = 0;
data[132..134].copy_from_slice(&0x8664u16.to_le_bytes()); data[134..136].copy_from_slice(&2u16.to_le_bytes()); data[148..150].copy_from_slice(&0xE0u16.to_le_bytes());
data[152..154].copy_from_slice(&0x20Bu16.to_le_bytes());
let parser = X86PeParser::new(data);
let headers = parser.parse_headers();
assert!(headers.is_some());
let h = headers.unwrap();
assert_eq!(h.machine, 0x8664);
assert_eq!(h.num_sections, 2);
assert_eq!(h.pe_magic, 0x20B);
}
#[test]
fn test_elf_symtab_parse_64() {
let mut symtab = vec![0u8; 48]; let mut strtab = b"func1\0func2\0".to_vec();
symtab[24..28].copy_from_slice(&1u32.to_le_bytes()); symtab[28] = 0x12; symtab[29] = 0;
symtab[30..32].copy_from_slice(&1u16.to_le_bytes());
symtab[32..40].copy_from_slice(&0x1000u64.to_le_bytes());
symtab[40..48].copy_from_slice(&0x50u64.to_le_bytes());
let symbols = X86ElfSymbolTableParser::parse(&symtab, &strtab, true);
assert_eq!(symbols.len(), 2);
assert_eq!(symbols[1].name, "func1");
assert_eq!(symbols[1].value, 0x1000);
assert_eq!(symbols[1].sym_type, 2); }
#[test]
fn test_elf_to_index_symbols() {
let sym = X86ElfSymbol {
name: "main".to_string(),
value: 0x4000,
size: 0x100,
binding: 1, sym_type: 2, visibility: 0,
shndx: 1,
};
let results = X86ElfSymbolTableParser::to_index_symbols(&[sym], "app");
assert_eq!(results.len(), 1);
assert_eq!(results[0].3, X86SymbolKind::Function);
assert_eq!(results[0].0, "main");
}
#[test]
fn test_gnu_debuglink_parse() {
let mut data = Vec::new();
data.extend_from_slice(b"libtest.so.debug");
data.push(0);
while data.len() % 4 != 0 {
data.push(0);
}
data.extend_from_slice(&0xABCD1234u32.to_le_bytes());
let link = X86GnuDebugLink::parse(&data);
assert!(link.is_some());
let link = link.unwrap();
assert_eq!(link.filename, "libtest.so.debug");
assert_eq!(link.crc, 0xABCD1234);
}
#[test]
fn test_gnu_debuglink_roundtrip() {
let original = X86GnuDebugLink::create("test.debug", 0xDEADBEEF);
let parsed = X86GnuDebugLink::parse(&original);
assert!(parsed.is_some());
assert_eq!(parsed.unwrap().filename, "test.debug");
}
#[test]
fn test_crc32() {
let crc = X86GnuDebugLink::compute_crc32(b"hello");
assert_ne!(crc, 0);
assert_eq!(crc, X86GnuDebugLink::compute_crc32(b"hello"));
assert_ne!(crc, X86GnuDebugLink::compute_crc32(b"world"));
}
#[test]
fn test_cache_insert_get() {
let mut cache = X86SymbolCache::new(10, 1024 * 1024);
cache.insert("key1", vec![1, 2, 3]);
assert_eq!(cache.get("key1").unwrap(), &vec![1, 2, 3]);
assert!(cache.get("key2").is_none());
}
#[test]
fn test_cache_eviction_by_size() {
let mut cache = X86SymbolCache::new(10, 10); cache.insert("a", vec![0; 5]);
cache.insert("b", vec![0; 5]);
cache.insert("c", vec![0; 5]); assert!(cache.len() <= 2);
}
#[test]
fn test_cache_lru_order() {
let mut cache = X86SymbolCache::new(3, 1024);
cache.insert("a", vec![1]);
cache.insert("b", vec![2]);
cache.insert("c", vec![3]);
cache.get("a");
cache.insert("d", vec![4]);
assert!(cache.get("b").is_none());
assert!(cache.get("a").is_some());
assert!(cache.get("c").is_some());
assert!(cache.get("d").is_some());
}
#[test]
fn test_cache_ttl() {
let mut cache = X86SymbolCache::new(10, 1024);
cache.set_ttl(0); cache.insert("x", vec![1]);
assert!(cache.get("x").is_some());
cache.set_ttl(1); let _ = cache.get("x");
}
#[test]
fn test_cache_clear() {
let mut cache = X86SymbolCache::new(10, 1024);
cache.insert("a", vec![1]);
cache.clear();
assert!(cache.is_empty());
}
#[test]
fn test_filter_name_contains() {
let filter = X86SymbolFilter {
name_contains: Some("foo".to_string()),
..X86SymbolFilter::new()
};
let info = X86SymbolInfo::new("my_foo_func", 0x1000, 1, X86SymbolKind::Function);
assert!(filter.matches(&info));
let info2 = X86SymbolInfo::new("my_bar_func", 0x1000, 1, X86SymbolKind::Function);
assert!(!filter.matches(&info2));
}
#[test]
fn test_filter_address_range() {
let filter = X86SymbolFilter {
address_range: Some((0x1000, 0x2000)),
..X86SymbolFilter::new()
};
assert!(filter.matches(&X86SymbolInfo::new("a", 0x1500, 1, X86SymbolKind::Function)));
assert!(!filter.matches(&X86SymbolInfo::new("b", 0x500, 1, X86SymbolKind::Function)));
assert!(!filter.matches(&X86SymbolInfo::new("c", 0x2500, 1, X86SymbolKind::Function)));
}
#[test]
fn test_filter_kind() {
let filter = X86SymbolFilter {
kinds: Some(vec![X86SymbolKind::Function, X86SymbolKind::Thunk]),
..X86SymbolFilter::new()
};
assert!(filter.matches(&X86SymbolInfo::new("f", 0, 1, X86SymbolKind::Function)));
assert!(filter.matches(&X86SymbolInfo::new("t", 0, 1, X86SymbolKind::Thunk)));
assert!(!filter.matches(&X86SymbolInfo::new("d", 0, 1, X86SymbolKind::Data)));
}
#[test]
fn test_filter_size() {
let filter = X86SymbolFilter {
min_size: Some(10),
max_size: Some(100),
..X86SymbolFilter::new()
};
assert!(filter.matches(&X86SymbolInfo::new("m", 0, 50, X86SymbolKind::Function)));
assert!(!filter.matches(&X86SymbolInfo::new("s", 0, 5, X86SymbolKind::Function)));
assert!(!filter.matches(&X86SymbolInfo::new("l", 0, 200, X86SymbolKind::Function)));
}
#[test]
fn test_line_program_parse_empty() {
let parser = X86DwarfLineProgramParser::new(vec![]);
let mappings = parser.parse();
assert!(mappings.is_empty());
}
#[test]
fn test_line_program_parse_minimal() {
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(&60u32.to_le_bytes());
data[4..6].copy_from_slice(&4u16.to_le_bytes());
data[6..8].copy_from_slice(&10u16.to_le_bytes());
data[18] = 1;
data[19] = 1;
data[20] = 1;
data[21] = (-5i8) as u8;
data[22] = 14;
data[23] = 13;
for i in 0..12 {
data[24 + i] = 1;
}
let parser = X86DwarfLineProgramParser::new(data);
let _mappings = parser.parse();
}
#[test]
fn test_expr_lit_ops() {
let expr = vec![0x05u8]; let mut eval = X86DwarfExpressionEvaluator::new(expr);
let result = eval.evaluate();
assert_eq!(result, Some(5));
}
#[test]
fn test_expr_const4u() {
let mut expr = vec![0x0Cu8]; expr.extend_from_slice(&0x12345678u32.to_le_bytes());
let mut eval = X86DwarfExpressionEvaluator::new(expr);
let result = eval.evaluate();
assert_eq!(result, Some(0x12345678));
}
#[test]
fn test_expr_plus() {
let mut expr = vec![0x08u8, 10u8]; expr.push(0x08u8);
expr.push(20u8); expr.push(0x22u8); let mut eval = X86DwarfExpressionEvaluator::new(expr);
let result = eval.evaluate();
assert_eq!(result, Some(30));
}
#[test]
fn test_expr_dup_drop() {
let mut expr = vec![0x08u8, 42u8]; expr.push(0x12u8); let mut eval = X86DwarfExpressionEvaluator::new(expr);
let result = eval.evaluate();
assert_eq!(result, Some(42));
}
#[test]
fn test_expr_register() {
let mut expr = vec![0x92u8, 5u8, 16u8]; let mut eval = X86DwarfExpressionEvaluator::new(expr);
eval.set_register(5, 0x1000);
let result = eval.evaluate();
assert_eq!(result, Some(0x1010));
}
#[test]
fn test_expr_cfa() {
let expr = vec![0x9Cu8]; let mut eval = X86DwarfExpressionEvaluator::new(expr);
eval.set_cfa(0x7FFFFFFF0000);
let result = eval.evaluate();
assert_eq!(result, Some(0x7FFFFFFF0000));
}
#[test]
fn test_expr_fbreg() {
let mut expr = vec![0x91u8, 8u8]; let mut eval = X86DwarfExpressionEvaluator::new(expr);
eval.set_frame_base(0x1000);
let result = eval.evaluate();
assert_eq!(result, Some(0x1008));
}
#[test]
fn test_expr_empty() {
let expr = vec![];
let mut eval = X86DwarfExpressionEvaluator::new(expr);
assert_eq!(eval.evaluate(), None);
}
#[test]
fn test_elf_reader_not_elf() {
assert!(X86ELFSectionReader::new(vec![0; 64]).is_none());
}
#[test]
fn test_elf_reader_minimal() {
let mut elf = vec![0u8; 256];
elf[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
elf[4] = 2;
elf[5] = 1;
elf[6] = 1;
elf[40] = 0x80;
elf[41] = 0;
elf[58] = 64;
elf[59] = 0;
elf[60] = 2;
elf[61] = 0;
elf[62] = 1;
elf[63] = 0;
let reader = X86ELFSectionReader::new(elf);
assert!(reader.is_some());
}
#[test]
fn test_differ_empty() {
let old = X86SymbolIndex::new();
let new = X86SymbolIndex::new();
let differ = X86SymbolDiffer::new(old, new);
let diff = differ.diff();
assert!(diff.added.is_empty());
assert!(diff.removed.is_empty());
assert!(diff.changed.is_empty());
}
#[test]
fn test_config_default() {
let config = X86SymbolServerConfig::default();
assert!(!config.http_servers.is_empty());
assert!(config.compress_symbols);
assert!(config.auto_fetch);
}
#[test]
fn test_config_build_server() {
let config = X86SymbolServerConfig::default();
let server = config.build_server();
assert!(!server.protocol.list_sources().is_empty());
}
#[test]
fn test_source_server_build_retrieval_command_empty() {
let ss = X86SourceServer::new();
let cmd = ss.build_retrieval_command();
assert!(!cmd.is_empty());
}
#[test]
fn test_source_server_build_retrieval_http() {
let mut ss = X86SourceServer::new();
ss.add_http_endpoint("http://src.example.com");
let cmd = ss.build_retrieval_command();
assert!(cmd.contains("src.example.com"));
}
#[test]
fn test_source_server_add_ini_entry() {
let mut ss = X86SourceServer::new();
ss.add_ini_entry("trusted_cmd", "cmd.exe");
let ini = ss.generate_srcsrv_ini();
assert!(ini.contains("trusted_cmd"));
}
#[test]
fn test_source_server_retrieve_source_not_found() {
let ss = X86SourceServer::new();
assert!(ss.retrieve_source("nonexistent.c").is_none());
}
#[test]
fn test_module_descriptor_full() {
let did = X86DebugId::new(vec![0xAB; 16], 3, X86DebugIdKind::PdbGuid);
let cv = X86CodeViewInfo::new_pdb70([0x01; 16], 1, "mod.pdb");
let mut desc = X86ModuleDescriptor::new("mod.dll", did.clone(), 0x400000, 0x20000);
desc.codeview_info = Some(cv);
desc.elf_build_id = Some(vec![0x11, 0x22, 0x33]);
desc.macho_uuid = Some([0x44; 16]);
desc.arch = "i386".to_string();
assert_eq!(desc.name, "mod.dll");
assert_eq!(desc.arch, "i386");
assert!(desc.codeview_info.is_some());
assert!(desc.elf_build_id.is_some());
assert!(desc.macho_uuid.is_some());
}
#[test]
fn test_debug_id_formatting() {
let did = X86DebugId::new(
vec![
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54,
0x32, 0x10,
],
0x1A,
X86DebugIdKind::PdbGuid,
);
let flat = did.to_flat_string();
assert!(flat.ends_with("1A"));
let guid = did.to_guid_string();
assert!(guid.contains('-'));
}
#[test]
fn test_large_index_stress() {
let mut idx = X86SymbolIndex::new();
for i in 0..1000u64 {
idx.add_symbol(
&format!("func_{:04x}", i),
i * 0x100,
0x50,
X86SymbolKind::Function,
Some(&format!("src/file{}.c", i % 10)),
Some((i % 500) as u32),
Some("stresstest"),
);
}
idx.build();
assert_eq!(idx.stats.total_symbols, 1000);
assert_eq!(idx.stats.total_functions, 1000);
for i in [0, 1, 10, 100, 500, 999].iter() {
let addr = *i as u64 * 0x100 + 0x10;
let result = idx.lookup_by_address(addr);
assert!(result.is_some(), "Failed lookup at 0x{:X}", addr);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X86SymbolFileFormat {
Pdb,
Breakpad,
DwarfElf,
DwarfMacho,
DwarfPackage,
PeCodeView,
RawSymbolTable,
GnuDebugLink,
Unknown,
}
impl X86SymbolFileFormat {
pub fn detect(data: &[u8]) -> Self {
if data.is_empty() {
return X86SymbolFileFormat::Unknown;
}
if data.len() >= 32 && &data[..32] == PDB_MAGIC {
return X86SymbolFileFormat::Pdb;
}
if data.len() >= 4 && &data[..4] == b"\x7FELF" {
return X86SymbolFileFormat::DwarfElf;
}
if data.len() >= 4 {
let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
if magic == 0xFEEDFACE
|| magic == 0xFEEDFACF
|| magic == 0xCEFAEDFE
|| magic == 0xCFFAEDFE
|| magic == 0xBEBAFECA
{
return X86SymbolFileFormat::DwarfMacho;
}
}
if data.len() >= 2 && data[0] == b'M' && data[1] == b'Z' {
if let Some(pe_off) = Self::pe_offset(data) {
if data.len() > pe_off + 4 && &data[pe_off..pe_off + 4] == b"PE\0\0" {
return X86SymbolFileFormat::PeCodeView;
}
}
}
if let Ok(text) = std::str::from_utf8(data) {
if text.starts_with("MODULE ") {
return X86SymbolFileFormat::Breakpad;
}
if text
.lines()
.filter(|l| {
let parts: Vec<&str> = l.split_whitespace().collect();
parts.len() >= 2 && u64::from_str_radix(parts[0], 16).is_ok()
})
.count()
> 0
{
return X86SymbolFileFormat::RawSymbolTable;
}
}
X86SymbolFileFormat::Unknown
}
fn pe_offset(data: &[u8]) -> Option<usize> {
if data.len() < 64 {
return None;
}
let offset = u32::from_le_bytes([data[60], data[61], data[62], data[63]]);
Some(offset as usize)
}
pub fn default_extension(&self) -> &'static str {
match self {
X86SymbolFileFormat::Pdb => "pdb",
X86SymbolFileFormat::Breakpad => "sym",
X86SymbolFileFormat::DwarfElf => "debug",
X86SymbolFileFormat::DwarfMacho => "dSYM",
X86SymbolFileFormat::DwarfPackage => "dwp",
X86SymbolFileFormat::PeCodeView => "pdb",
X86SymbolFileFormat::RawSymbolTable => "sym",
X86SymbolFileFormat::GnuDebugLink => "debug",
X86SymbolFileFormat::Unknown => "bin",
}
}
}
#[derive(Debug, Clone)]
pub struct X86DebugDirectory {
pub characteristics: u32,
pub time_date_stamp: u32,
pub major_version: u16,
pub minor_version: u16,
pub debug_type: u32,
pub size_of_data: u32,
pub address_of_raw_data: u32,
pub pointer_to_raw_data: u32,
pub data: Vec<u8>,
}
impl X86DebugDirectory {
pub const SIZE: usize = 28;
pub fn parse(data: &[u8], offset: usize) -> Option<Self> {
if offset + Self::SIZE > data.len() {
return None;
}
let base = offset;
let characteristics =
u32::from_le_bytes([data[base], data[base + 1], data[base + 2], data[base + 3]]);
let time_date_stamp = u32::from_le_bytes([
data[base + 4],
data[base + 5],
data[base + 6],
data[base + 7],
]);
let major_version = u16::from_le_bytes([data[base + 8], data[base + 9]]);
let minor_version = u16::from_le_bytes([data[base + 10], data[base + 11]]);
let debug_type = u32::from_le_bytes([
data[base + 12],
data[base + 13],
data[base + 14],
data[base + 15],
]);
let size_of_data = u32::from_le_bytes([
data[base + 16],
data[base + 17],
data[base + 18],
data[base + 19],
]);
let address_of_raw_data = u32::from_le_bytes([
data[base + 20],
data[base + 21],
data[base + 22],
data[base + 23],
]);
let pointer_to_raw_data = u32::from_le_bytes([
data[base + 24],
data[base + 25],
data[base + 26],
data[base + 27],
]);
let raw = pointer_to_raw_data as usize;
let mut debug_data = Vec::new();
if raw + size_of_data as usize <= data.len() && size_of_data > 0 {
debug_data.extend_from_slice(&data[raw..raw + size_of_data as usize]);
}
Some(Self {
characteristics,
time_date_stamp,
major_version,
minor_version,
debug_type,
size_of_data,
address_of_raw_data,
pointer_to_raw_data,
data: debug_data,
})
}
pub fn parse_all(data: &[u8], offset: usize, count: usize) -> Vec<Self> {
let mut entries = Vec::with_capacity(count);
for i in 0..count {
let off = offset + i * Self::SIZE;
if let Some(entry) = Self::parse(data, off) {
entries.push(entry);
} else {
break;
}
}
entries
}
}
#[derive(Debug, Clone)]
pub struct X86CFIRegisterRule {
pub register: String,
pub expression: String,
pub is_initial: bool,
}
impl X86CFIRegisterRule {
pub fn new(register: &str, expression: &str) -> Self {
Self {
register: register.to_string(),
expression: expression.to_string(),
is_initial: false,
}
}
pub fn to_breakpad(&self) -> String {
format!("{}: {}", self.register, self.expression)
}
}
#[derive(Debug, Clone)]
pub struct X86CFIInitRecord {
pub address: u64,
pub size: u64,
pub rules: Vec<X86CFIRegisterRule>,
}
impl X86CFIInitRecord {
pub fn new(address: u64, size: u64) -> Self {
Self {
address,
size,
rules: Vec::new(),
}
}
pub fn add_rule(&mut self, register: &str, expression: &str) {
self.rules
.push(X86CFIRegisterRule::new(register, expression));
}
pub fn to_breakpad(&self) -> String {
let rules_str: Vec<String> = self.rules.iter().map(|r| r.to_breakpad()).collect();
format!(
"{} {:x} {:x} {}",
BREAKPAD_STACK_CFI_INIT,
self.address,
self.size,
rules_str.join(" "),
)
}
}
#[derive(Debug, Clone)]
pub struct X86CFIRecord {
pub address: u64,
pub rules: Vec<X86CFIRegisterRule>,
}
impl X86CFIRecord {
pub fn new(address: u64) -> Self {
Self {
address,
rules: Vec::new(),
}
}
pub fn to_breakpad(&self) -> String {
let rules_str: Vec<String> = self.rules.iter().map(|r| r.to_breakpad()).collect();
format!(
"{} {:x} {}",
BREAKPAD_STACK_CFI_MARKER,
self.address,
rules_str.join(" "),
)
}
}
#[derive(Debug, Clone)]
pub struct X86StackWinRecord {
pub frame_type: u32,
pub address: u64,
pub code_size: u64,
pub prologue_size: u64,
pub epilogue_size: u64,
pub parameter_size: u32,
pub saved_register_size: u32,
pub local_size: u32,
pub max_stack_size: u32,
pub has_program_string: bool,
pub program_string_or_bp: u32,
}
impl X86StackWinRecord {
pub fn new(address: u64, code_size: u64) -> Self {
Self {
frame_type: 0,
address,
code_size,
prologue_size: 0,
epilogue_size: 0,
parameter_size: 0,
saved_register_size: 0,
local_size: 0,
max_stack_size: 0,
has_program_string: false,
program_string_or_bp: 1,
}
}
pub fn to_breakpad(&self) -> String {
format!(
"{} {} {:x} {:x} {:x} {:x} {} {} {} {} {} {}",
BREAKPAD_STACK_WIN_MARKER,
self.frame_type,
self.address,
self.code_size,
self.prologue_size,
self.epilogue_size,
self.parameter_size,
self.saved_register_size,
self.local_size,
self.max_stack_size,
self.has_program_string as u32,
self.program_string_or_bp,
)
}
}
pub struct X86DwarfLineProgramParser {
data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct X86LineMapping {
pub address: u64,
pub file_index: u32,
pub line: u32,
pub column: u32,
pub is_stmt: bool,
pub basic_block: bool,
pub prologue_end: bool,
pub epilogue_begin: bool,
pub isa: u32,
}
impl X86DwarfLineProgramParser {
pub fn new(data: Vec<u8>) -> Self {
Self { data }
}
pub fn parse(&self) -> Vec<X86LineMapping> {
let mut mappings = Vec::new();
if self.data.len() < 16 {
return mappings;
}
let total_length =
u32::from_le_bytes([self.data[0], self.data[1], self.data[2], self.data[3]]) as usize;
let _version = u16::from_le_bytes([self.data[4], self.data[5]]);
if total_length == 0xFFFFFFFF {
return mappings;
}
let header_length = u16::from_le_bytes([self.data[6], self.data[7]]) as usize;
if 8 + header_length >= self.data.len() {
return mappings;
}
let min_insn_length = self.data[8 + header_length - 5];
let max_ops_per_insn = self.data[8 + header_length - 4];
let default_is_stmt = self.data[8 + header_length - 3];
let line_base = self.data[8 + header_length - 2] as i8;
let line_range = self.data[8 + header_length - 1];
let _ = (min_insn_length, max_ops_per_insn, default_is_stmt);
let mut address: u64 = 0;
let mut file: u32 = 0;
let mut line: u32 = 1;
let mut column: u32 = 0;
let mut is_stmt = default_is_stmt != 0;
let mut basic_block = false;
let mut prologue_end = false;
let mut epilogue_begin = false;
let mut isa: u32 = 0;
let body_start = 8 + header_length;
let mut pos = body_start;
while pos < self.data.len() && pos < 8 + total_length {
let opcode = self.data[pos];
pos += 1;
if opcode == 0 {
if pos >= self.data.len() {
break;
}
let ext_len = self.read_uleb128(&mut pos);
if ext_len == 0 || pos + ext_len > self.data.len() {
break;
}
let ext_opcode = self.data[pos];
pos += 1;
match ext_opcode {
1 => {
mappings.push(X86LineMapping {
address,
file_index: file,
line,
column,
is_stmt,
basic_block,
prologue_end,
epilogue_begin,
isa,
});
address = 0;
file = 0;
line = 1;
column = 0;
basic_block = false;
prologue_end = false;
epilogue_begin = false;
}
2 => {
if pos + 8 <= self.data.len() {
address = u64::from_le_bytes([
self.data[pos],
self.data[pos + 1],
self.data[pos + 2],
self.data[pos + 3],
self.data[pos + 4],
self.data[pos + 5],
self.data[pos + 6],
self.data[pos + 7],
]);
pos += 8;
}
}
3 => {
while pos < self.data.len() && self.data[pos] != 0 {
pos += 1;
}
if pos < self.data.len() {
pos += 1;
} if let Err(_) = self.read_uleb128_quiet(&mut pos) {
break;
}
if let Err(_) = self.read_uleb128_quiet(&mut pos) {
break;
}
if let Err(_) = self.read_uleb128_quiet(&mut pos) {
break;
}
}
4 => {
if let Err(_) = self.read_uleb128_quiet(&mut pos) {
break;
}
}
_ => {
pos += ext_len.saturating_sub(1);
}
}
} else if opcode < 32 {
let _ret = self.read_uleb128_quiet(&mut pos);
} else {
let adjusted = opcode as u32 - 32;
let addr_inc = adjusted / line_range as u32;
let line_inc = line_base as i32 + (adjusted % line_range as u32) as i32;
address += addr_inc as u64 * min_insn_length as u64;
line = (line as i32 + line_inc).max(0) as u32;
mappings.push(X86LineMapping {
address,
file_index: file,
line,
column,
is_stmt,
basic_block,
prologue_end,
epilogue_begin,
isa,
});
basic_block = false;
prologue_end = false;
epilogue_begin = false;
}
}
mappings
}
fn read_uleb128(&self, pos: &mut usize) -> usize {
let mut result: u64 = 0;
let mut shift = 0;
loop {
if *pos >= self.data.len() {
return 0;
}
let byte = self.data[*pos];
*pos += 1;
result |= ((byte & 0x7F) as u64) << shift;
shift += 7;
if byte & 0x80 == 0 {
break;
}
}
result as usize
}
fn read_uleb128_quiet(&self, pos: &mut usize) -> Result<usize, ()> {
if *pos >= self.data.len() {
return Err(());
}
Ok(self.read_uleb128(pos))
}
}
pub struct X86DwarfExpressionEvaluator {
expression: Vec<u8>,
stack: Vec<u64>,
cfa: u64,
frame_base: u64,
registers: HashMap<u32, u64>,
}
impl X86DwarfExpressionEvaluator {
pub fn new(expression: Vec<u8>) -> Self {
Self {
expression,
stack: Vec::new(),
cfa: 0,
frame_base: 0,
registers: HashMap::new(),
}
}
pub fn set_cfa(&mut self, cfa: u64) {
self.cfa = cfa;
}
pub fn set_frame_base(&mut self, fb: u64) {
self.frame_base = fb;
}
pub fn set_register(&mut self, reg: u32, value: u64) {
self.registers.insert(reg, value);
}
pub fn evaluate(&mut self) -> Option<u64> {
let mut pos = 0;
while pos < self.expression.len() {
let op = self.expression[pos];
pos += 1;
match op {
0x00..=0x1F => {
self.stack.push(op as u64);
}
0x50..=0x6F => {
let reg = (op - 0x50) as u32;
if let Some(&v) = self.registers.get(®) {
self.stack.push(v);
} else {
self.stack.push(0);
}
}
0x70..=0x8F => {
let reg = (op - 0x70) as u32;
let offset = self.read_sleb128(&mut pos);
let base = self.registers.get(®).copied().unwrap_or(0);
self.stack.push((base as i64 + offset) as u64);
}
0x03 => {
if pos + 8 <= self.expression.len() {
let addr = u64::from_le_bytes([
self.expression[pos],
self.expression[pos + 1],
self.expression[pos + 2],
self.expression[pos + 3],
self.expression[pos + 4],
self.expression[pos + 5],
self.expression[pos + 6],
self.expression[pos + 7],
]);
pos += 8;
self.stack.push(addr);
}
}
0x06 => {
}
0x08 => {
if pos < self.expression.len() {
self.stack.push(self.expression[pos] as u64);
pos += 1;
}
}
0x09 => {
if pos < self.expression.len() {
self.stack.push(self.expression[pos] as i8 as u64);
pos += 1;
}
}
0x0A => {
if pos + 2 <= self.expression.len() {
let v =
u16::from_le_bytes([self.expression[pos], self.expression[pos + 1]]);
pos += 2;
self.stack.push(v as u64);
}
}
0x0C => {
if pos + 4 <= self.expression.len() {
let v = u32::from_le_bytes([
self.expression[pos],
self.expression[pos + 1],
self.expression[pos + 2],
self.expression[pos + 3],
]);
pos += 4;
self.stack.push(v as u64);
}
}
0x0E => {
if pos + 8 <= self.expression.len() {
let v = u64::from_le_bytes([
self.expression[pos],
self.expression[pos + 1],
self.expression[pos + 2],
self.expression[pos + 3],
self.expression[pos + 4],
self.expression[pos + 5],
self.expression[pos + 6],
self.expression[pos + 7],
]);
pos += 8;
self.stack.push(v);
}
}
0x10 => {
let v = self.read_uleb128_q(&mut pos);
self.stack.push(v);
}
0x11 => {
let v = self.read_sleb128(&mut pos) as u64;
self.stack.push(v);
}
0x12 => {
if let Some(&top) = self.stack.last() {
self.stack.push(top);
}
}
0x13 => {
self.stack.pop();
}
0x1C => {
let b = self.stack.pop().unwrap_or(0);
let a = self.stack.pop().unwrap_or(0);
self.stack.push(a.wrapping_sub(b));
}
0x1D => {
let b = self.stack.pop().unwrap_or(0);
let a = self.stack.pop().unwrap_or(0);
self.stack.push(a.wrapping_mul(b));
}
0x22 => {
let b = self.stack.pop().unwrap_or(0);
let a = self.stack.pop().unwrap_or(0);
self.stack.push(a.wrapping_add(b));
}
0x23 => {
let v = self.read_uleb128_q(&mut pos);
if let Some(top) = self.stack.last_mut() {
*top = top.wrapping_add(v);
}
}
0x27 => {
let b = self.stack.pop().unwrap_or(0);
let a = self.stack.pop().unwrap_or(0);
self.stack.push(a & b);
}
0x28 => {
let b = self.stack.pop().unwrap_or(0);
let a = self.stack.pop().unwrap_or(0);
self.stack.push(a | b);
}
0x90 => {
let reg = self.read_uleb128_q(&mut pos) as u32;
let val = self.registers.get(®).copied().unwrap_or(0);
self.stack.push(val);
}
0x91 => {
let offset = self.read_sleb128(&mut pos);
self.stack.push((self.frame_base as i64 + offset) as u64);
}
0x92 => {
let reg = self.read_uleb128_q(&mut pos) as u32;
let offset = self.read_sleb128(&mut pos);
let base = self.registers.get(®).copied().unwrap_or(0);
self.stack.push((base as i64 + offset) as u64);
}
0x93 => {
let _size = self.read_uleb128_q(&mut pos);
}
0x9C => {
self.stack.push(self.cfa);
}
0x96 => {
}
_ => {
break;
}
}
}
self.stack.pop()
}
fn read_sleb128(&self, pos: &mut usize) -> i64 {
let mut result: i64 = 0;
let mut shift = 0;
loop {
if *pos >= self.expression.len() {
return 0;
}
let byte = self.expression[*pos];
*pos += 1;
result |= ((byte & 0x7F) as i64) << shift;
shift += 7;
if byte & 0x80 == 0 {
if shift < 64 && (byte & 0x40) != 0 {
result |= !0 << shift;
}
break;
}
}
result
}
fn read_uleb128_q(&self, pos: &mut usize) -> u64 {
let mut result: u64 = 0;
let mut shift = 0;
loop {
if *pos >= self.expression.len() {
return 0;
}
let byte = self.expression[*pos];
*pos += 1;
result |= ((byte & 0x7F) as u64) << shift;
shift += 7;
if byte & 0x80 == 0 {
break;
}
}
result
}
}
pub struct X86ELFSectionReader {
data: Vec<u8>,
is_64bit: bool,
little_endian: bool,
shoff: u64,
shentsize: u16,
shnum: u16,
shstrndx: u16,
shstrtab_offset: Option<u64>,
}
impl X86ELFSectionReader {
pub fn new(data: Vec<u8>) -> Option<Self> {
if data.len() < 64 || &data[..4] != b"\x7FELF" {
return None;
}
let is_64bit = data[4] == 2;
let little_endian = data[5] == 1;
let read_u16 = |off: usize| -> u16 {
if little_endian {
u16::from_le_bytes([data[off], data[off + 1]])
} else {
u16::from_be_bytes([data[off], data[off + 1]])
}
};
let read_u32 = |off: usize| -> u32 {
if little_endian {
u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
} else {
u32::from_be_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
}
};
let read_u64 = |off: usize| -> u64 {
if little_endian {
u64::from_le_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
data[off + 4],
data[off + 5],
data[off + 6],
data[off + 7],
])
} else {
u64::from_be_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
data[off + 4],
data[off + 5],
data[off + 6],
data[off + 7],
])
}
};
let (shoff, shentsize, shnum, shstrndx) = if is_64bit {
if data.len() < 64 {
return None;
}
(read_u64(40), read_u16(58), read_u16(60), read_u16(62))
} else {
if data.len() < 52 {
return None;
}
(
read_u32(32) as u64,
read_u16(46),
read_u16(48),
read_u16(50),
)
};
let mut reader = Self {
data,
is_64bit,
little_endian,
shoff,
shentsize,
shnum,
shstrndx,
shstrtab_offset: None,
};
reader.shstrtab_offset = reader.section_offset(shstrndx as u64);
Some(reader)
}
fn section_offset(&self, index: u64) -> Option<u64> {
let hdr = self.shoff + index * self.shentsize as u64;
if hdr as usize + 40 > self.data.len() {
return None;
}
let off = if self.is_64bit {
self.read_u64_at(hdr as usize + 24)
} else {
self.read_u32_at(hdr as usize + 16) as u64
};
if off > 0 {
Some(off)
} else {
None
}
}
pub fn section_name(&self, index: u64) -> Option<String> {
let shstrtab = self.shstrtab_offset?;
let hdr = self.shoff + index * self.shentsize as u64;
if hdr as usize + 4 > self.data.len() {
return None;
}
let name_off = self.read_u32_at(hdr as usize);
let name_bytes = &self.data[shstrtab as usize + name_off as usize..];
let end = name_bytes
.iter()
.position(|&b| b == 0)
.unwrap_or(name_bytes.len());
Some(String::from_utf8_lossy(&name_bytes[..end]).to_string())
}
pub fn find_section(&self, name: &str) -> Option<Vec<u8>> {
for i in 0..self.shnum as u64 {
let hdr = self.shoff + i * self.shentsize as u64;
if hdr as usize + (if self.is_64bit { 64 } else { 40 }) > self.data.len() {
break;
}
let sh_name = self.read_u32_at(hdr as usize);
if let Some(st_off) = self.shstrtab_offset {
let name_start = (st_off + sh_name as u64) as usize;
let mut found_name = String::new();
for j in name_start..self.data.len() {
if self.data[j] == 0 {
break;
}
found_name.push(self.data[j] as char);
}
if found_name == name {
let sh_offset = if self.is_64bit {
self.read_u64_at(hdr as usize + 24)
} else {
self.read_u32_at(hdr as usize + 16) as u64
};
let sh_size = if self.is_64bit {
self.read_u64_at(hdr as usize + 32)
} else {
self.read_u32_at(hdr as usize + 20) as u64
};
let off = sh_offset as usize;
let sz = sh_size as usize;
if off + sz <= self.data.len() {
return Some(self.data[off..off + sz].to_vec());
}
}
}
}
None
}
pub fn list_debug_sections(&self) -> Vec<(String, u64)> {
let mut sections = Vec::new();
for i in 0..self.shnum as u64 {
if let Some(name) = self.section_name(i) {
if name.starts_with(".debug_") || name.starts_with(".zdebug_") {
let hdr = self.shoff + i * self.shentsize as u64;
let sz = if self.is_64bit {
self.read_u64_at(hdr as usize + 32)
} else {
self.read_u32_at(hdr as usize + 20) as u64
};
sections.push((name, sz));
}
}
}
sections
}
fn read_u16_at(&self, off: usize) -> u16 {
if off + 2 > self.data.len() {
return 0;
}
if self.little_endian {
u16::from_le_bytes([self.data[off], self.data[off + 1]])
} else {
u16::from_be_bytes([self.data[off], self.data[off + 1]])
}
}
fn read_u32_at(&self, off: usize) -> u32 {
if off + 4 > self.data.len() {
return 0;
}
if self.little_endian {
u32::from_le_bytes([
self.data[off],
self.data[off + 1],
self.data[off + 2],
self.data[off + 3],
])
} else {
u32::from_be_bytes([
self.data[off],
self.data[off + 1],
self.data[off + 2],
self.data[off + 3],
])
}
}
fn read_u64_at(&self, off: usize) -> u64 {
if off + 8 > self.data.len() {
return 0;
}
if self.little_endian {
u64::from_le_bytes([
self.data[off],
self.data[off + 1],
self.data[off + 2],
self.data[off + 3],
self.data[off + 4],
self.data[off + 5],
self.data[off + 6],
self.data[off + 7],
])
} else {
u64::from_be_bytes([
self.data[off],
self.data[off + 1],
self.data[off + 2],
self.data[off + 3],
self.data[off + 4],
self.data[off + 5],
self.data[off + 6],
self.data[off + 7],
])
}
}
}
pub struct X86SymbolDiffer {
old_index: X86SymbolIndex,
new_index: X86SymbolIndex,
}
#[derive(Debug, Clone)]
pub struct X86SymbolDiff {
pub added: Vec<X86SymbolInfo>,
pub removed: Vec<X86SymbolInfo>,
pub changed: Vec<X86SymbolDiffEntry>,
}
#[derive(Debug, Clone)]
pub struct X86SymbolDiffEntry {
pub old_info: X86SymbolInfo,
pub new_info: X86SymbolInfo,
}
impl X86SymbolDiffer {
pub fn new(old_index: X86SymbolIndex, new_index: X86SymbolIndex) -> Self {
Self {
old_index,
new_index,
}
}
pub fn diff(&self) -> X86SymbolDiff {
let mut added = Vec::new();
let mut removed = Vec::new();
let mut changed = Vec::new();
let old_names: HashSet<String> = self
.old_index
.lookup_by_name("")
.iter()
.map(|s| s.name.to_lowercase())
.collect();
let new_names: HashSet<String> = self
.new_index
.lookup_by_name("")
.iter()
.map(|s| s.name.to_lowercase())
.collect();
for name in &old_names {
if !new_names.contains(name) {
let old_results = self.old_index.lookup_by_name(name);
for s in old_results {
removed.push(s);
}
}
}
for name in &new_names {
if !old_names.contains(name) {
let new_results = self.new_index.lookup_by_name(name);
for s in new_results {
added.push(s);
}
}
}
for name in old_names.intersection(&new_names) {
let old_results = self.old_index.lookup_by_name(name);
let new_results = self.new_index.lookup_by_name(name);
for old_sym in &old_results {
for new_sym in &new_results {
if old_sym.name.to_lowercase() == new_sym.name.to_lowercase() {
if old_sym.address != new_sym.address
|| old_sym.size != new_sym.size
|| old_sym.source_file != new_sym.source_file
{
changed.push(X86SymbolDiffEntry {
old_info: old_sym.clone(),
new_info: new_sym.clone(),
});
}
}
}
}
}
X86SymbolDiff {
added,
removed,
changed,
}
}
}
#[derive(Debug, Clone)]
pub struct X86SymbolFilter {
pub name_contains: Option<String>,
pub address_range: Option<(u64, u64)>,
pub kinds: Option<Vec<X86SymbolKind>>,
pub source_files: Option<Vec<String>>,
pub min_size: Option<u64>,
pub max_size: Option<u64>,
}
impl X86SymbolFilter {
pub fn new() -> Self {
Self {
name_contains: None,
address_range: None,
kinds: None,
source_files: None,
min_size: None,
max_size: None,
}
}
pub fn matches(&self, info: &X86SymbolInfo) -> bool {
if let Some(ref pattern) = self.name_contains {
if !info.name.to_lowercase().contains(&pattern.to_lowercase()) {
return false;
}
}
if let Some((start, end)) = self.address_range {
if info.address < start || info.address >= end {
return false;
}
}
if let Some(ref kinds) = self.kinds {
if !kinds.contains(&info.kind) {
return false;
}
}
if let Some(ref files) = self.source_files {
if let Some(ref sf) = info.source_file {
if !files.iter().any(|f| sf.contains(f)) {
return false;
}
} else {
return false;
}
}
if let Some(min) = self.min_size {
if info.size < min {
return false;
}
}
if let Some(max) = self.max_size {
if info.size > max {
return false;
}
}
true
}
pub fn apply_to_index(&self, index: &X86SymbolIndex) -> Vec<X86SymbolInfo> {
let mut results = Vec::new();
if let Some(ref files) = self.source_files {
for file in files {
let symbols = index.lookup_by_source_file(file);
for sym in symbols {
if self.matches(&sym) {
results.push(sym);
}
}
}
return results;
}
if let Some(ref pattern) = self.name_contains {
let symbols = index.lookup_by_name(pattern);
for sym in symbols {
if self.matches(&sym) {
results.push(sym);
}
}
return results;
}
results
}
}
impl Default for X86SymbolFilter {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct X86GnuDebugLink {
pub filename: String,
pub crc: u32,
}
impl X86GnuDebugLink {
pub fn parse(data: &[u8]) -> Option<Self> {
if data.is_empty() {
return None;
}
let name_end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
let filename = String::from_utf8_lossy(&data[..name_end]).to_string();
let crc_offset = (name_end + 4) & !3;
if crc_offset + 4 > data.len() {
return None;
}
let crc = u32::from_le_bytes([
data[crc_offset],
data[crc_offset + 1],
data[crc_offset + 2],
data[crc_offset + 3],
]);
Some(Self { filename, crc })
}
pub fn create(filename: &str, crc: u32) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(filename.as_bytes());
data.push(0); while data.len() % 4 != 0 {
data.push(0);
}
data.extend_from_slice(&crc.to_le_bytes());
data
}
pub fn compute_crc32(data: &[u8]) -> u32 {
let mut crc: u32 = 0xFFFFFFFF;
for &byte in data {
crc ^= byte as u32;
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0xEDB88320;
} else {
crc >>= 1;
}
}
}
!crc
}
}
pub struct X86SymbolCache {
entries: HashMap<String, (Vec<u8>, u64)>,
max_entries: usize,
max_size_bytes: u64,
current_size: u64,
order: Vec<String>,
ttl_seconds: u64,
}
impl X86SymbolCache {
pub fn new(max_entries: usize, max_size_bytes: u64) -> Self {
Self {
entries: HashMap::new(),
max_entries,
max_size_bytes,
current_size: 0,
order: Vec::new(),
ttl_seconds: 0,
}
}
pub fn set_ttl(&mut self, seconds: u64) {
self.ttl_seconds = seconds;
}
pub fn insert(&mut self, key: &str, data: Vec<u8>) {
let size = data.len() as u64;
while self.entries.len() >= self.max_entries
|| self.current_size + size > self.max_size_bytes
{
if !self.evict_one() {
break;
}
}
if let Some((old_data, _)) = self.entries.remove(key) {
self.current_size = self.current_size.saturating_sub(old_data.len() as u64);
self.order.retain(|k| k != key);
}
let now = current_timestamp();
self.current_size += size;
self.entries.insert(key.to_string(), (data, now));
self.order.push(key.to_string());
}
pub fn get(&mut self, key: &str) -> Option<&Vec<u8>> {
if self.ttl_seconds > 0 {
if let Some((_, ts)) = self.entries.get(key) {
let now = current_timestamp();
if now - ts > self.ttl_seconds {
self.remove(key);
return None;
}
}
}
if self.entries.contains_key(key) {
self.order.retain(|k| k != key);
self.order.push(key.to_string());
self.entries.get(key).map(|(d, _)| d)
} else {
None
}
}
pub fn remove(&mut self, key: &str) -> Option<Vec<u8>> {
if let Some((data, _)) = self.entries.remove(key) {
self.current_size = self.current_size.saturating_sub(data.len() as u64);
self.order.retain(|k| k != key);
Some(data)
} else {
None
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.order.clear();
self.current_size = 0;
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn current_size(&self) -> u64 {
self.current_size
}
pub fn evict_stale(&mut self) -> usize {
if self.ttl_seconds == 0 {
return 0;
}
let now = current_timestamp();
let stale_keys: Vec<String> = self
.entries
.iter()
.filter(|(_, (_, ts))| now - ts > self.ttl_seconds)
.map(|(k, _)| k.clone())
.collect();
let count = stale_keys.len();
for key in stale_keys {
self.remove(&key);
}
count
}
fn evict_one(&mut self) -> bool {
if let Some(key) = self.order.first().cloned() {
self.remove(&key);
true
} else {
false
}
}
}
impl Default for X86SymbolCache {
fn default() -> Self {
Self::new(1024, 256 * 1024 * 1024)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_guid_standard() {
let data = [
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10,
];
let guid = format_guid(&data);
assert_eq!(guid.len(), 36);
assert!(guid.contains('-'));
assert_eq!(&guid[..8], "04030201");
}
#[test]
fn test_format_guid_short() {
let data = [0xAA; 8];
let guid = format_guid(&data);
assert_eq!(guid, "00000000-0000-0000-0000-000000000000");
}
#[test]
fn test_format_hex() {
assert_eq!(format_hex(&[0xAB, 0xCD, 0xEF]), "ABCDEF");
assert_eq!(format_hex(&[]), "");
assert_eq!(format_hex(&[0x00, 0xFF]), "00FF");
}
#[test]
fn test_parse_guid_valid() {
let guid_str = "01020304-0506-0708-090A-0B0C0D0E0F10";
let parsed = parse_guid(guid_str);
assert!(parsed.is_some());
let bytes = parsed.unwrap();
assert_eq!(bytes[0], 0x01);
assert_eq!(bytes[15], 0x10);
}
#[test]
fn test_parse_guid_nodashes() {
let guid_str = "0102030405060708090A0B0C0D0E0F10";
let parsed = parse_guid(guid_str);
assert!(parsed.is_some());
}
#[test]
fn test_parse_guid_invalid() {
assert!(parse_guid("too-short").is_none());
assert!(parse_guid("").is_none());
}
#[test]
fn test_fnv1a_32() {
assert_eq!(fnv1a_32(b""), 0x811c9dc5);
assert_eq!(fnv1a_32(b"a"), 0xe40c292c);
let h1 = fnv1a_32(b"hello");
let h2 = fnv1a_32(b"hello");
assert_eq!(h1, h2);
assert_ne!(fnv1a_32(b"hello"), fnv1a_32(b"world"));
}
#[test]
fn test_breakpad_debug_id() {
let data = [0x01u8; 20];
let id = breakpad_debug_id(&data);
assert!(!id.is_empty());
assert!(id.len() >= 32);
}
#[test]
fn test_sha1_to_breakpad_id() {
let sha1 = [0xABu8; 20];
let id = sha1_to_breakpad_id(&sha1);
assert!(id.len() >= 32);
}
#[test]
fn test_current_timestamp() {
let ts = current_timestamp();
assert!(ts > 1_700_000_000); }
#[test]
fn test_symbol_kind_default() {
assert_eq!(X86SymbolKind::default(), X86SymbolKind::Unknown);
}
#[test]
fn test_symbol_kind_equality() {
assert_eq!(X86SymbolKind::Function, X86SymbolKind::Function);
assert_ne!(X86SymbolKind::Function, X86SymbolKind::Data);
}
#[test]
fn test_symbol_info_new() {
let info = X86SymbolInfo::new("test_func", 0x1000, 0x50, X86SymbolKind::Function);
assert_eq!(info.name, "test_func");
assert_eq!(info.address, 0x1000);
assert_eq!(info.size, 0x50);
assert_eq!(info.kind, X86SymbolKind::Function);
assert!(info.source_file.is_none());
assert!(info.inline_frames.is_empty());
}
#[test]
fn test_inline_frame_new() {
let frame = X86InlineFrame::new("inlined_fn", 2);
assert_eq!(frame.function_name, "inlined_fn");
assert_eq!(frame.depth, 2);
assert_eq!(frame.address_range, (0, 0));
}
#[test]
fn test_source_location_new() {
let loc = X86SourceLocation::new("/src/main.rs", 42, 0x4000);
assert_eq!(loc.file_path, "/src/main.rs");
assert_eq!(loc.line, 42);
assert_eq!(loc.address, 0x4000);
assert!(loc.column.is_none());
}
#[test]
fn test_debug_id_new() {
let id = X86DebugId::new(vec![0xAB; 16], 1, X86DebugIdKind::PdbGuid);
assert_eq!(id.age, 1);
assert_eq!(id.kind, X86DebugIdKind::PdbGuid);
assert_eq!(id.id.len(), 16);
}
#[test]
fn test_debug_id_to_flat_string() {
let id = X86DebugId::new(vec![0x01; 16], 0xA, X86DebugIdKind::ElfBuildId);
let s = id.to_flat_string();
assert!(s.contains("01010101"));
assert!(s.ends_with('A'));
}
#[test]
fn test_debug_id_to_guid_string() {
let mut data = vec![0u8; 16];
data[0] = 0x01;
data[1] = 0x02;
let id = X86DebugId::new(data, 0, X86DebugIdKind::PdbGuid);
let s = id.to_guid_string();
assert!(s.contains("0201"));
}
#[test]
fn test_module_descriptor_new() {
let did = X86DebugId::new(vec![0xCC; 16], 0, X86DebugIdKind::PdbGuid);
let desc = X86ModuleDescriptor::new("ntdll.dll", did, 0x7FFE0000, 0x200000);
assert_eq!(desc.name, "ntdll.dll");
assert_eq!(desc.image_base, 0x7FFE0000);
assert_eq!(desc.arch, "x86_64");
}
#[test]
fn test_module_pdb_name() {
let did = X86DebugId::new(vec![0; 16], 0, X86DebugIdKind::Unknown);
let desc = X86ModuleDescriptor::new("kernel32.dll", did, 0, 0);
assert_eq!(desc.pdb_name(), "kernel32.pdb");
}
#[test]
fn test_module_sym_name() {
let did = X86DebugId::new(vec![0; 16], 0, X86DebugIdKind::Unknown);
let desc = X86ModuleDescriptor::new("libc.so.6", did, 0, 0);
assert_eq!(desc.sym_name(), "libc.so.6.sym");
}
#[test]
fn test_codeview_pdb70() {
let guid = [0x01u8; 16];
let cv = X86CodeViewInfo::new_pdb70(guid, 1, "test.pdb");
assert_eq!(cv.signature, CV_SIGNATURE_RSDS);
assert_eq!(cv.age, 1);
assert_eq!(cv.pdb_path, "test.pdb");
}
#[test]
fn test_codeview_pdb20() {
let cv = X86CodeViewInfo::new_pdb20(0x12345678, 1, "old.pdb");
assert_eq!(cv.signature, CV_SIGNATURE_NB10);
assert_eq!(cv.age, 1);
}
#[test]
fn test_codeview_guid_string() {
let guid = [0xAAu8; 16];
let cv = X86CodeViewInfo::new_pdb70(guid, 1, "x.pdb");
let s = cv.guid_string();
assert!(s.len() == 36);
}
#[test]
fn test_source_priority() {
let s = X86SymbolSource::HttpSymbolServer {
url: "http://example.com".into(),
priority: 10,
timeout_ms: 5000,
};
assert_eq!(s.priority(), 10);
}
#[test]
fn test_source_description() {
let s = X86SymbolSource::LocalStore {
root_path: PathBuf::from("/tmp/symbols"),
priority: 0,
};
assert!(s.description().contains("/tmp/symbols"));
}
#[test]
fn test_index_new_empty() {
let idx = X86SymbolIndex::new();
assert!(idx.is_empty());
assert_eq!(idx.len(), 0);
assert!(!idx.is_built);
}
#[test]
fn test_index_add_and_build() {
let mut idx = X86SymbolIndex::new();
idx.add_symbol(
"main",
0x1000,
0x50,
X86SymbolKind::Function,
Some("main.c"),
Some(1),
Some("app"),
);
idx.add_symbol(
"foo",
0x2000,
0x30,
X86SymbolKind::Function,
Some("foo.c"),
Some(10),
Some("app"),
);
idx.add_symbol(
"bar",
0x3000,
0x20,
X86SymbolKind::PublicSymbol,
None,
None,
None,
);
idx.build();
assert!(idx.is_built);
assert_eq!(idx.len(), 3);
assert_eq!(idx.stats.total_symbols, 3);
assert_eq!(idx.stats.total_functions, 2);
assert_eq!(idx.stats.total_publics, 1);
}
#[test]
fn test_index_lookup_by_address() {
let mut idx = X86SymbolIndex::new();
idx.add_symbol(
"func_a",
0x1000,
0x100,
X86SymbolKind::Function,
None,
None,
None,
);
idx.add_symbol(
"func_b",
0x2000,
0x100,
X86SymbolKind::Function,
None,
None,
None,
);
idx.build();
let (_, info) = idx.lookup_by_address(0x1050).unwrap();
assert_eq!(info.name, "func_a");
let (_, info) = idx.lookup_by_address(0x2050).unwrap();
assert_eq!(info.name, "func_b");
assert!(idx.lookup_by_address(0x5000).is_none());
assert!(idx.lookup_by_address(0x0FFF).is_none());
}
#[test]
fn test_index_lookup_by_name() {
let mut idx = X86SymbolIndex::new();
idx.add_symbol(
"MyFunction",
0x1000,
0x10,
X86SymbolKind::Function,
None,
None,
None,
);
idx.add_symbol(
"my_other",
0x2000,
0x10,
X86SymbolKind::Function,
None,
None,
None,
);
idx.add_symbol(
"NotThis",
0x3000,
0x10,
X86SymbolKind::Function,
None,
None,
None,
);
idx.build();
let results = idx.lookup_by_name("myfunction");
assert_eq!(results.len(), 1);
let results = idx.lookup_by_name("my");
assert!(results.len() >= 2); }
#[test]
fn test_index_lookup_by_source_file() {
let mut idx = X86SymbolIndex::new();
idx.add_symbol(
"f1",
0x1000,
1,
X86SymbolKind::Function,
Some("src/a.c"),
Some(1),
None,
);
idx.add_symbol(
"f2",
0x2000,
1,
X86SymbolKind::Function,
Some("src/b.c"),
Some(2),
None,
);
idx.add_symbol(
"f3",
0x3000,
1,
X86SymbolKind::Function,
Some("src/a.c"),
Some(5),
None,
);
idx.build();
let results = idx.lookup_by_source_file("src/a.c");
assert_eq!(results.len(), 2);
}
#[test]
fn test_index_inline_frames() {
let mut idx = X86SymbolIndex::new();
idx.add_symbol(
"outer",
0x1000,
0x200,
X86SymbolKind::Function,
Some("f.c"),
Some(1),
None,
);
let fi0 = idx.add_inline_frame(
"inner1",
Some("f.c"),
Some(5),
Some("f.c"),
Some(3),
0,
0x1100,
0x1200,
);
let fi1 = idx.add_inline_frame(
"inner2",
Some("f.c"),
Some(10),
Some("f.c"),
Some(9),
1,
0x1150,
0x11A0,
);
idx.set_symbol_inline_frames(0, &[fi0, fi1]);
idx.build();
let (_, info) = idx.lookup_by_address(0x1160).unwrap();
assert_eq!(info.inline_frames.len(), 2);
assert_eq!(info.inline_frames[0].function_name, "inner1");
assert_eq!(info.inline_frames[1].function_name, "inner2");
}
#[test]
fn test_index_lookup_source_location() {
let mut idx = X86SymbolIndex::new();
idx.add_symbol(
"f",
0x4000,
0x30,
X86SymbolKind::Function,
Some("/src/x.c"),
Some(42),
None,
);
idx.build();
let loc = idx.lookup_source_location(0x4010).unwrap();
assert_eq!(loc.file_path, "/src/x.c");
assert_eq!(loc.line, 42);
}
#[test]
fn test_index_clear() {
let mut idx = X86SymbolIndex::new();
idx.add_symbol("x", 0, 1, X86SymbolKind::Function, None, None, None);
idx.build();
assert_eq!(idx.len(), 1);
idx.clear();
assert_eq!(idx.len(), 0);
assert!(!idx.is_built);
}
#[test]
fn test_store_new() {
let store = X86SymbolStore::new_memory();
assert_eq!(store.cache_usage, 0);
}
#[test]
fn test_store_symbol_server_path() {
let path = X86SymbolStore::symbol_server_path(
"kernel32.pdb",
"12345678-1234-1234-1234-123456789ABC",
1,
);
let s = path.to_string_lossy();
assert!(s.contains("kernel32.pdb"));
assert!(s.contains("12345678123412341234123456789ABC1"));
}
#[test]
fn test_store_breakpad_symbol_path() {
let path = X86SymbolStore::breakpad_symbol_path("libc.so", "ABCD1234");
let s = path.to_string_lossy();
assert!(s.contains("libc.so"));
assert!(s.contains("ABCD1234"));
assert!(s.ends_with(".sym"));
}
#[test]
fn test_store_dwarf_debug_path() {
let path = X86SymbolStore::dwarf_debug_path("myapp", "DEADBEEF");
let s = path.to_string_lossy();
assert!(s.contains("myapp"));
assert!(s.contains("DEADBEEF"));
assert!(s.ends_with(".dbg"));
}
#[test]
fn test_store_elf_build_id_index() {
let mut store = X86SymbolStore::new_memory();
let bid = [0x01, 0x02, 0x03, 0x04];
store.index_elf_build_id(&bid, "libexample.so");
assert_eq!(
store.lookup_by_elf_build_id(&bid),
Some(&"libexample.so".to_string())
);
assert!(store.lookup_by_elf_build_id(&[0xFF; 4]).is_none());
}
#[test]
fn test_store_pe_codeview_index() {
let mut store = X86SymbolStore::new_memory();
let cv = X86CodeViewInfo::new_pdb70([0x01; 16], 1, "test.pdb");
store.index_pe_codeview(&cv, "test.dll");
let key = format!("{}:{:X}", cv.guid_string(), cv.age);
assert!(store.build_id_index.contains_key(&key));
}
#[test]
fn test_parse_pe_debug_directory_pdb70() {
let mut data = vec![0u8; 256];
data[12..16].copy_from_slice(&IMAGE_DEBUG_TYPE_CODEVIEW.to_le_bytes()); data[16..20].copy_from_slice(&100u32.to_le_bytes()); data[24..28].copy_from_slice(&128u32.to_le_bytes()); data[128..132].copy_from_slice(&CV_SIGNATURE_RSDS.to_le_bytes()); for i in 0..16 {
data[132 + i] = (i + 1) as u8;
} data[148..152].copy_from_slice(&1u32.to_le_bytes()); data[152] = b't';
data[153] = b'e';
data[154] = b's';
data[155] = b't';
data[156] = b'.';
data[157] = b'p';
data[158] = b'd';
data[159] = b'b';
data[160] = 0;
let cv = X86SymbolStore::parse_pe_debug_directory(&data, 0);
assert!(cv.is_some());
let cv = cv.unwrap();
assert_eq!(cv.signature, CV_SIGNATURE_RSDS);
assert_eq!(cv.age, 1);
assert!(cv.pdb_path.contains("test.pdb"));
}
#[test]
fn test_parse_pe_debug_directory_pdb20() {
let mut data = vec![0u8; 256];
data[12..16].copy_from_slice(&IMAGE_DEBUG_TYPE_CODEVIEW.to_le_bytes());
data[16..20].copy_from_slice(&100u32.to_le_bytes());
data[24..28].copy_from_slice(&128u32.to_le_bytes());
data[128..132].copy_from_slice(&CV_SIGNATURE_NB10.to_le_bytes()); data[132..136].copy_from_slice(&0x12345678u32.to_le_bytes()); data[136..140].copy_from_slice(&1u32.to_le_bytes()); data[140] = b'o';
data[141] = b'l';
data[142] = b'd';
data[143] = 0;
let cv = X86SymbolStore::parse_pe_debug_directory(&data, 0);
assert!(cv.is_some());
assert_eq!(cv.unwrap().signature, CV_SIGNATURE_NB10);
}
#[test]
fn test_parse_pe_debug_not_codeview() {
let mut data = vec![0u8; 64];
data[12..16].copy_from_slice(&IMAGE_DEBUG_TYPE_COFF.to_le_bytes()); assert!(X86SymbolStore::parse_pe_debug_directory(&data, 0).is_none());
}
#[test]
fn test_parse_macho_uuid() {
let mut data = vec![0u8; 64];
const LC_UUID: u32 = 0x1B;
data[0..4].copy_from_slice(&LC_UUID.to_le_bytes());
data[4..8].copy_from_slice(&24u32.to_le_bytes()); for i in 0..16 {
data[8 + i] = (i + 1) as u8;
}
let uuid = X86SymbolStore::parse_macho_uuid(&data, 0);
assert!(uuid.is_some());
assert_eq!(uuid.unwrap()[0], 1);
}
#[test]
fn test_parse_macho_uuid_wrong_cmd() {
let mut data = vec![0u8; 64];
data[0..4].copy_from_slice(&0x01u32.to_le_bytes()); data[4..8].copy_from_slice(&24u32.to_le_bytes());
assert!(X86SymbolStore::parse_macho_uuid(&data, 0).is_none());
}
#[test]
fn test_macho_uuid_index_lookup() {
let mut store = X86SymbolStore::new_memory();
let uuid = [0xABu8; 16];
store.index_macho_uuid(&uuid, "libfoo.dylib");
assert_eq!(
store.lookup_by_macho_uuid(&uuid),
Some(&"libfoo.dylib".to_string())
);
}
#[test]
fn test_cache_put_get() {
let mut store = X86SymbolStore::new_memory();
store.cache_put("key1", vec![1, 2, 3]);
assert_eq!(store.cache_get("key1").unwrap(), &vec![1, 2, 3]);
assert!(store.cache_get("key2").is_none());
}
#[test]
fn test_cache_remove() {
let mut store = X86SymbolStore::new_memory();
store.cache_put("k", vec![4, 5, 6]);
let removed = store.cache_remove("k");
assert_eq!(removed, Some(vec![4, 5, 6]));
assert!(store.cache_get("k").is_none());
}
#[test]
fn test_transaction_begin_commit() {
let mut store = X86SymbolStore::new_memory();
store.begin_transaction("tx1");
assert!(store
.transaction_add_file("tx1", PathBuf::from("test.txt"), vec![0x42])
.is_ok());
assert!(store.commit_transaction("tx1").is_ok());
}
#[test]
fn test_transaction_rollback() {
let mut store = X86SymbolStore::new_memory();
store.begin_transaction("tx2");
store
.transaction_add_file("tx2", PathBuf::from("ghost.txt"), vec![0x13])
.unwrap();
assert!(store.rollback_transaction("tx2").is_ok());
}
#[test]
fn test_transaction_not_found() {
let mut store = X86SymbolStore::new_memory();
assert!(store
.transaction_add_file("nope", PathBuf::from("x"), vec![])
.is_err());
}
#[test]
fn test_protocol_new() {
let store = X86SymbolStore::new_memory();
let proto = X86SymbolServerProtocol::new(store);
assert!(proto.list_sources().is_empty());
}
#[test]
fn test_protocol_add_sources() {
let store = X86SymbolStore::new_memory();
let mut proto = X86SymbolServerProtocol::new(store);
proto.add_source(X86SymbolSource::MemoryCache { priority: 0 });
proto.add_source(X86SymbolSource::MemoryCache { priority: 5 });
proto.add_source(X86SymbolSource::MemoryCache { priority: 3 });
assert_eq!(proto.list_sources().len(), 3);
assert_eq!(proto.list_sources()[0].priority(), 0);
}
#[test]
fn test_build_http_symbol_url() {
let url = X86SymbolServerProtocol::build_http_symbol_url(
"https://msdl.microsoft.com/download/symbols",
"ntdll.pdb",
"12345678-1234-1234-1234-123456789ABC",
1,
);
assert!(url.starts_with("https://msdl.microsoft.com"));
assert!(url.contains("ntdll.pdb"));
assert!(url.contains("12345678123412341234123456789ABC"));
}
#[test]
fn test_protocol_resolve_symbol_file_memory() {
let mut store = X86SymbolStore::new_memory();
let did = X86DebugId::new(vec![0x01; 16], 1, X86DebugIdKind::PdbGuid);
store.cache_put(
"test.pdb:010101010101010101010101010101011",
vec![0x42; 100],
);
let mut proto = X86SymbolServerProtocol::new(store);
proto.add_source(X86SymbolSource::MemoryCache { priority: 0 });
let result = proto.resolve_symbol_file("test.pdb", &did);
assert!(result.is_some());
}
#[test]
fn test_uploader_new() {
let store = X86SymbolStore::new_memory();
let proto = X86SymbolServerProtocol::new(X86SymbolStore::new_memory());
let uploader = X86SymbolUploader::new(store, proto);
assert!(uploader.compress);
}
#[test]
fn test_compress_decompress_roundtrip() {
let store = X86SymbolStore::new_memory();
let proto = X86SymbolServerProtocol::new(X86SymbolStore::new_memory());
let uploader = X86SymbolUploader::new(store, proto);
let original = b"AAABBBCCCDDDDEEE";
let compressed = uploader.compress_data(original);
assert!(compressed.len() >= 12);
let decompressed = uploader.decompress_data(&compressed);
assert!(decompressed.is_some());
assert_eq!(&decompressed.unwrap(), original);
}
#[test]
fn test_decompress_bad_magic() {
let store = X86SymbolStore::new_memory();
let proto = X86SymbolServerProtocol::new(X86SymbolStore::new_memory());
let uploader = X86SymbolUploader::new(store, proto);
assert!(uploader.decompress_data(b"BAD1").is_none());
}
#[test]
fn test_resolver_new() {
let proto = X86SymbolServerProtocol::new(X86SymbolStore::new_memory());
let resolver = X86SymbolResolver::new(proto);
assert!(resolver.list_modules().is_empty());
}
#[test]
fn test_resolver_register_module() {
let proto = X86SymbolServerProtocol::new(X86SymbolStore::new_memory());
let mut resolver = X86SymbolResolver::new(proto);
let did = X86DebugId::new(vec![0; 16], 0, X86DebugIdKind::Unknown);
let desc = X86ModuleDescriptor::new("mod.dll", did, 0x400000, 0x10000);
resolver.register_module(desc);
assert_eq!(resolver.list_modules().len(), 1);
}
#[test]
fn test_resolver_resolve_function() {
let proto = X86SymbolServerProtocol::new(X86SymbolStore::new_memory());
let mut resolver = X86SymbolResolver::new(proto);
let did = X86DebugId::new(vec![0; 16], 0, X86DebugIdKind::Unknown);
let desc = X86ModuleDescriptor::new("mod.dll", did, 0x400000, 0x10000);
resolver.register_module(desc);
assert!(resolver.resolve_function(0x401000).is_none());
}
#[test]
fn test_resolver_resolve_module() {
let proto = X86SymbolServerProtocol::new(X86SymbolStore::new_memory());
let mut resolver = X86SymbolResolver::new(proto);
let did = X86DebugId::new(vec![0; 16], 0, X86DebugIdKind::Unknown);
let desc = X86ModuleDescriptor::new("mod.dll", did, 0x400000, 0x10000);
resolver.register_module(desc);
let found = resolver.resolve_module(0x405000);
assert!(found.is_some());
assert_eq!(found.unwrap().name, "mod.dll");
assert!(resolver.resolve_module(0x500000).is_none());
}
#[test]
fn test_resolver_unregister_module() {
let proto = X86SymbolServerProtocol::new(X86SymbolStore::new_memory());
let mut resolver = X86SymbolResolver::new(proto);
let did = X86DebugId::new(vec![0; 16], 0, X86DebugIdKind::Unknown);
resolver.register_module(X86ModuleDescriptor::new("mod.dll", did, 0, 0));
assert_eq!(resolver.list_modules().len(), 1);
resolver.unregister_module("mod.dll");
assert_eq!(resolver.list_modules().len(), 0);
}
#[test]
fn test_breakpad_converter_new() {
let conv = X86BreakpadConverter::new();
assert_eq!(conv.arch, "x86");
assert_eq!(conv.os, "linux");
assert!(conv.include_cfi);
}
#[test]
fn test_breakpad_convert_dwarf_empty() {
let conv = X86BreakpadConverter::new();
let result = conv.convert_dwarf_to_breakpad(b"", "test", "ID01");
assert!(result.contains("MODULE"));
assert!(result.contains("test"));
assert!(result.contains("ID01"));
}
#[test]
fn test_breakpad_convert_pdb_empty() {
let conv = X86BreakpadConverter::new();
let result = conv.convert_pdb_to_breakpad(b"", "test.pdb", "GUID1");
assert!(result.contains("MODULE"));
assert!(result.contains("INFO CODE_ID"));
}
#[test]
fn test_source_server_new() {
let ss = X86SourceServer::new();
assert!(ss.variables.contains_key("SRCSRV_SOURCE_ROOT"));
}
#[test]
fn test_source_server_set_variable() {
let mut ss = X86SourceServer::new();
ss.set_variable("MY_VAR", "my_value");
assert_eq!(ss.variables.get("MY_VAR").unwrap(), "my_value");
}
#[test]
fn test_source_server_generate_ini() {
let mut ss = X86SourceServer::new();
ss.set_vcs_provider(X86VcsProvider::Git {
repository_url: "https://github.com/user/repo".into(),
branch: "main".into(),
});
let ini = ss.generate_srcsrv_ini();
assert!(ini.contains("[variables]"));
assert!(ini.contains("[verctrl]"));
assert!(ini.contains("provider=git"));
assert!(ini.contains("github.com"));
}
#[test]
fn test_source_server_index_sources() {
let ss = X86SourceServer::new();
let files = vec!["src/main.c".to_string(), "src/utils.c".to_string()];
let data = ss.index_sources(&files, "app.pdb");
let text = String::from_utf8_lossy(&data);
assert!(text.contains("SRCSRV: ini"));
assert!(text.contains("src/main.c"));
assert!(text.contains("src/utils.c"));
assert!(text.contains("SRCSRV: end"));
}
#[test]
fn test_source_server_vcs_descriptions() {
let mut ss = X86SourceServer::new();
assert_eq!(ss.vcs_description(), "Unknown");
ss.set_vcs_provider(X86VcsProvider::Subversion {
repository_url: "svn://server/repo".into(),
revision: 1234,
});
assert_eq!(ss.vcs_description(), "Subversion");
ss.set_vcs_provider(X86VcsProvider::Perforce {
server: "p4server:1666".into(),
client: "myclient".into(),
});
assert_eq!(ss.vcs_description(), "Perforce");
}
#[test]
fn test_symbol_server_new() {
let server = X86SymbolServer::new_in_memory();
assert!(server.resolver.list_modules().is_empty());
}
#[test]
fn test_symbol_server_load_module_elf() {
let mut elf = vec![0u8; 256];
elf[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
elf[4] = 2; elf[5] = 1; elf[6] = 1; elf[40] = 0x80;
elf[41] = 0x00; elf[58] = 64;
elf[59] = 0; elf[60] = 2;
elf[61] = 0; elf[62] = 1;
elf[63] = 0;
let sh1 = 0xC0;
elf[sh1..sh1 + 4].copy_from_slice(&0u32.to_le_bytes());
elf[sh1 + 4..sh1 + 8].copy_from_slice(&7u32.to_le_bytes());
elf[sh1 + 24..sh1 + 32].copy_from_slice(&0x100u64.to_le_bytes());
elf[sh1 + 32..sh1 + 40].copy_from_slice(&0x40u64.to_le_bytes());
let str_off = 0x140usize;
elf[str_off..str_off + 19].copy_from_slice(b".note.gnu.build-id\0");
elf[0x100..0x104].copy_from_slice(&4u32.to_le_bytes()); elf[0x104..0x108].copy_from_slice(&20u32.to_le_bytes()); elf[0x108..0x10C].copy_from_slice(&3u32.to_le_bytes()); elf[0x10C..0x110].copy_from_slice(b"GNU\0"); for i in 0..20 {
elf[0x110 + i] = (i + 1) as u8;
}
let mut server = X86SymbolServer::new_in_memory();
let result = server.load_module(&elf, "libtest.so", 0x100000, 0x1000);
let _ = result;
}
#[test]
fn test_symbol_server_generate_srcsrv_ini() {
let server = X86SymbolServer::new_in_memory();
let ini = server.generate_srcsrv_ini();
assert!(!ini.is_empty());
}
#[test]
fn test_symbol_server_index_sources() {
let server = X86SymbolServer::new_in_memory();
let files = vec!["a.c".to_string()];
let data = server.index_sources(&files, "a.pdb");
assert!(!data.is_empty());
}
#[test]
fn test_full_workflow_elf_to_breakpad() {
let mut server = X86SymbolServer::new_in_memory();
let mut elf = vec![0u8; 512];
elf[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
elf[4] = 2;
elf[5] = 1;
elf[40] = 0xC0;
elf[41] = 0x00; elf[58] = 64;
elf[59] = 0; elf[60] = 2;
elf[61] = 0; elf[62] = 1;
elf[63] = 0;
let sh1 = 0x100;
elf[sh1..sh1 + 4].copy_from_slice(&0u32.to_le_bytes());
elf[sh1 + 4..sh1 + 8].copy_from_slice(&7u32.to_le_bytes()); elf[sh1 + 24..sh1 + 32].copy_from_slice(&0x180u64.to_le_bytes()); elf[sh1 + 32..sh1 + 40].copy_from_slice(&0x40u64.to_le_bytes());
let str_off = 0x140;
elf[str_off..str_off + 19].copy_from_slice(b".note.gnu.build-id\0");
elf[0x180..0x184].copy_from_slice(&4u32.to_le_bytes()); elf[0x184..0x188].copy_from_slice(&20u32.to_le_bytes()); elf[0x188..0x18C].copy_from_slice(&3u32.to_le_bytes()); elf[0x18C..0x190].copy_from_slice(b"GNU\0");
for i in 0..20 {
elf[0x190 + i] = (i + 1) as u8;
}
let _ = server.load_module(&elf, "libtest.so", 0x100000, 0x2000);
let bp = server.convert_to_breakpad("libtest.so", "0102030405060708090A0B0C0D0E0F1001");
let _ = bp;
}
#[test]
fn test_pe_codeview_roundtrip() {
let guid = [
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD,
0xEE, 0xFF,
];
let cv = X86CodeViewInfo::new_pdb70(guid, 2, "kernel32.pdb");
let debug_id = cv.debug_id();
assert_eq!(debug_id.kind, X86DebugIdKind::PdbGuid);
assert_eq!(debug_id.age, 2);
assert_eq!(cv.guid_string(), format_guid(&guid));
}
#[test]
fn test_elf_build_id_parse_no_note_section() {
let mut elf = vec![0u8; 128];
elf[0..4].copy_from_slice(&[0x7F, b'E', b'L', b'F']);
elf[4] = 2;
elf[5] = 1;
elf[6] = 1;
let bid = X86SymbolStore::parse_elf_build_id(&elf);
assert!(bid.is_none());
}
#[test]
fn test_store_pdb_ms_layout_in_memory() {
let mut store = X86SymbolStore::new_memory();
let guid = "01020304-0506-0708-090A-0B0C0D0E0F10";
let result = store.store_pdb_ms_layout("test.pdb", guid, 1, b"PDB data");
assert!(result.is_err() || result.is_ok()); }
#[test]
fn test_uploader_elf() {
let store = X86SymbolStore::new_memory();
let proto = X86SymbolServerProtocol::new(X86SymbolStore::new_memory());
let mut uploader = X86SymbolUploader::new(store, proto);
let result = uploader.upload_from_elf(b"not an elf", "bad.so");
assert!(result.is_err());
}
#[test]
fn test_all_constants_are_consistent() {
assert!(MAX_SYMBOL_CACHE_SIZE > 0);
assert!(DEFAULT_SYMBOL_SERVER_TIMEOUT_MS > 0);
assert!(GUID_STRING_LENGTH == 36);
assert!(MAX_SYMBOL_SOURCE_CHAIN >= 1);
assert!(MAX_INLINE_DEPTH > 0);
assert!(CV_SIGNATURE_RSDS != CV_SIGNATURE_NB10);
assert!(!BREAKPAD_MODULE_MAGIC.is_empty());
}
#[test]
fn test_symbol_source_equality() {
let s1 = X86SymbolSource::MemoryCache { priority: 1 };
let s2 = X86SymbolSource::MemoryCache { priority: 1 };
assert_eq!(s1.priority(), s2.priority());
}
#[test]
fn test_symbol_kinds_variants() {
let kinds = [
X86SymbolKind::Function,
X86SymbolKind::PublicSymbol,
X86SymbolKind::Data,
X86SymbolKind::Label,
X86SymbolKind::Thunk,
X86SymbolKind::InlineFunction,
X86SymbolKind::Unknown,
];
for k in &kinds {
match k {
X86SymbolKind::Function
| X86SymbolKind::PublicSymbol
| X86SymbolKind::Data
| X86SymbolKind::Label
| X86SymbolKind::Thunk
| X86SymbolKind::InlineFunction
| X86SymbolKind::Unknown => {}
}
}
}
#[test]
fn test_exhaustive_index_workflow() {
let mut idx = X86SymbolIndex::new();
for i in 0..100 {
let addr = 0x1000 + i * 0x100;
let name = format!("func_{}", i);
let file = format!("src/mod{}.c", i % 5);
idx.add_symbol(
&name,
addr as u64,
0x50,
X86SymbolKind::Function,
Some(&file),
Some(i as u32),
Some("app"),
);
}
idx.build();
assert_eq!(idx.stats.total_symbols, 100);
assert_eq!(idx.stats.total_functions, 100);
assert_eq!(idx.stats.total_source_files, 5);
assert_eq!(idx.stats.total_modules, 1);
for i in 0..100 {
let addr = 0x1000u64 + i as u64 * 0x100 + 0x10;
let result = idx.lookup_by_address(addr);
assert!(result.is_some(), "Failed lookup at 0x{:X}", addr);
let (_, info) = result.unwrap();
assert_eq!(info.name, format!("func_{}", i));
}
let results = idx.lookup_by_name("func_5");
assert!(!results.is_empty());
let results = idx.lookup_by_source_file("src/mod0.c");
assert_eq!(results.len(), 20); }
#[test]
fn test_resolver_by_name() {
let proto = X86SymbolServerProtocol::new(X86SymbolStore::new_memory());
let mut resolver = X86SymbolResolver::new(proto);
let did = X86DebugId::new(vec![0x01; 16], 0, X86DebugIdKind::ElfBuildId);
let mut desc = X86ModuleDescriptor::new("libmath.so", did, 0x1000, 0x5000);
let tmp = std::env::temp_dir().join("x86_sym_test_libmath.txt");
let sym_data = "1000 sin\n2000 cos\n3000 tan\n";
std::fs::write(&tmp, sym_data).ok();
desc.symbol_path = Some(tmp.clone());
resolver.register_module(desc);
let results = resolver.resolve_by_name("sin");
let _ = results;
let _ = std::fs::remove_file(&tmp);
}
}