mod discovery;
mod image;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
use object::Object;
use self::discovery::{
Discovery, DiscoveryCache, discover_dwp, discover_separate_debug, discover_supplementary,
validated_gnu_debugdata,
};
pub(in crate::convert) use self::image::AddressLayout;
use self::image::{
cross_check_elf_architecture, cross_check_elf_identity, malformed, parse_elf,
require_supported_kind,
};
use super::ConversionWarning;
use crate::builder::{BuilderOptions, GsymBuilder};
use crate::{ElfInputKind, Endian, Error, Result, WriterOptions};
#[derive(Clone, Copy, Eq, PartialEq)]
pub struct ElfInputs<'data> {
pub image: &'data [u8],
pub debug: Option<&'data [u8]>,
pub symbols: Option<&'data [u8]>,
pub supplementary: Option<&'data [u8]>,
pub dwp: Option<&'data [u8]>,
}
impl fmt::Debug for ElfInputs<'_> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ElfInputs")
.field("image_len", &self.image.len())
.field("debug_len", &self.debug.map(<[u8]>::len))
.field("symbols_len", &self.symbols.map(<[u8]>::len))
.field("supplementary_len", &self.supplementary.map(<[u8]>::len))
.field("dwp_len", &self.dwp.map(<[u8]>::len))
.finish()
}
}
impl<'data> ElfInputs<'data> {
#[must_use]
pub const fn new(image: &'data [u8]) -> Self {
Self {
image,
debug: None,
symbols: None,
supplementary: None,
dwp: None,
}
}
#[must_use]
pub const fn with_debug(mut self, debug: &'data [u8]) -> Self {
self.debug = Some(debug);
self
}
#[must_use]
pub const fn with_symbols(mut self, symbols: &'data [u8]) -> Self {
self.symbols = Some(symbols);
self
}
#[must_use]
pub const fn with_supplementary(mut self, supplementary: &'data [u8]) -> Self {
self.supplementary = Some(supplementary);
self
}
#[must_use]
pub const fn with_dwp(mut self, dwp: &'data [u8]) -> Self {
self.dwp = Some(dwp);
self
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConversionOptions {
pub writer: WriterOptions,
pub include_symbols: bool,
pub dwarf: Option<DwarfImportOptions>,
pub discovery: DiscoveryPolicy,
pub debug_directories: Vec<PathBuf>,
pub debuginfod_urls: Vec<String>,
pub debuginfod_cache: PathBuf,
pub debuginfod_max_download_size: u64,
pub gnu_debugdata_max_decompressed_size: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct DwarfImportOptions {
pub inline_info: bool,
pub call_sites: bool,
}
impl Default for DwarfImportOptions {
fn default() -> Self {
Self {
inline_info: true,
call_sites: false,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub enum DiscoveryPolicy {
Disabled,
#[default]
Enabled,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum DiscoveryEvent<'path> {
DebuginfodRequest {
artifact: &'static str,
build_id: &'path str,
endpoint: &'path str,
related_path: &'path Path,
},
}
impl Default for ConversionOptions {
fn default() -> Self {
Self {
writer: WriterOptions::default(),
include_symbols: true,
dwarf: Some(DwarfImportOptions::default()),
discovery: DiscoveryPolicy::Enabled,
debug_directories: vec![PathBuf::from("/usr/lib/debug")],
debuginfod_urls: std::env::var("DEBUGINFOD_URLS")
.ok()
.map(|urls| urls.split_whitespace().map(str::to_owned).collect())
.unwrap_or_default(),
debuginfod_cache: std::env::var_os("DEBUGINFOD_CACHE_PATH").map_or_else(
|| std::env::temp_dir().join("gsym-rs-debuginfod"),
PathBuf::from,
),
debuginfod_max_download_size: 1 << 30,
gnu_debugdata_max_decompressed_size: 1 << 30,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[non_exhaustive]
pub struct ConversionStats {
pub symbol_functions: usize,
pub dwarf_functions: usize,
pub line_rows: usize,
pub inline_nodes: usize,
pub rejected_ranges: usize,
}
#[non_exhaustive]
pub struct ConversionReport {
pub builder: GsymBuilder,
pub stats: ConversionStats,
pub warnings: Vec<ConversionWarning>,
pub discovered_debug: Option<PathBuf>,
pub discovered_supplementary: Option<PathBuf>,
pub discovered_dwp: Option<PathBuf>,
}
impl fmt::Debug for ConversionReport {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ConversionReport")
.field("builder", &self.builder)
.field("stats", &self.stats)
.field("warning_count", &self.warnings.len())
.field("discovered_debug", &self.discovered_debug)
.field("discovered_supplementary", &self.discovered_supplementary)
.field("discovered_dwp", &self.discovered_dwp)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug, Default)]
pub struct ElfConverter {
options: ConversionOptions,
discovery_cache: OnceLock<Arc<DiscoveryCache>>,
}
impl PartialEq for ElfConverter {
fn eq(&self, other: &Self) -> bool {
self.options == other.options
}
}
impl Eq for ElfConverter {}
impl ElfConverter {
#[must_use]
pub const fn new(options: ConversionOptions) -> Self {
Self {
options,
discovery_cache: OnceLock::new(),
}
}
#[must_use]
pub const fn options(&self) -> &ConversionOptions {
&self.options
}
pub fn convert(&self, inputs: ElfInputs<'_>) -> Result<ConversionReport> {
self.convert_inner(inputs, None)
}
fn convert_inner(
&self,
inputs: ElfInputs<'_>,
dwo_base: Option<&Path>,
) -> Result<ConversionReport> {
let image = parse_elf(inputs.image, ElfInputKind::Image)?;
require_supported_kind(&image)?;
let layout = AddressLayout::new(&image)?;
let base_address = layout.ranges.first().map_or(0, |range| range.start);
if layout.ranges.is_empty() {
return Err(Error::InvalidModel("ELF image has no executable sections"));
}
let build_id = image
.build_id()
.map_err(|error| malformed("ELF build ID", error))?
.unwrap_or_default()
.to_vec();
let mut warnings = Vec::new();
let mini_debug = if inputs.debug.is_none()
&& (self.options.include_symbols || self.options.dwarf.is_some())
{
validated_gnu_debugdata(
&image,
self.options.gnu_debugdata_max_decompressed_size,
&mut warnings,
)
} else {
None
};
let debug_bytes = inputs
.debug
.or(mini_debug.as_deref())
.unwrap_or(inputs.image);
let separate_debug = inputs.debug.is_some() || mini_debug.is_some();
let debug = parse_elf(debug_bytes, ElfInputKind::Debug)?;
if inputs.debug.is_some() {
cross_check_elf_identity(&image, &debug, ElfInputKind::Debug)?;
}
let supplementary = inputs
.supplementary
.map(|bytes| parse_elf(bytes, ElfInputKind::Supplementary))
.transpose()?;
let dwp = inputs
.dwp
.map(|bytes| parse_elf(bytes, ElfInputKind::Dwp))
.transpose()?;
if let Some(dwp) = &dwp {
cross_check_elf_architecture(&image, dwp, ElfInputKind::Dwp)?;
}
let mut writer = self.options.writer.clone();
writer.endian = if image.is_little_endian() {
Endian::Little
} else {
Endian::Big
};
writer.base_address = Some(base_address);
if writer.build_id.is_empty() {
writer.build_id = build_id;
}
let mut builder = GsymBuilder::with_options(BuilderOptions {
writer,
executable_ranges: layout.ranges.clone().into_boxed_slice(),
..BuilderOptions::default()
});
let mut stats = ConversionStats::default();
if self.options.include_symbols {
let symbol_file = inputs
.symbols
.map(|bytes| parse_elf(bytes, ElfInputKind::Symbols))
.transpose()?;
if let Some(symbol_file) = &symbol_file {
cross_check_elf_identity(&image, symbol_file, ElfInputKind::Symbols)?;
}
let mut sources: Vec<(&[u8], &object::File<'_>)> = Vec::with_capacity(3);
if let (Some(bytes), Some(file)) = (inputs.symbols, symbol_file.as_ref()) {
sources.push((bytes, file));
}
if separate_debug {
sources.push((debug_bytes, &debug));
}
sources.push((inputs.image, &image));
stats.symbol_functions = import_distinct_symbols(
&sources,
&layout,
&mut builder,
&mut stats.rejected_ranges,
)?;
}
if let Some(dwarf_options) = self.options.dwarf {
super::dwarf::import_dwarf(super::dwarf::DwarfImport {
file: &debug,
supplementary: supplementary.as_ref(),
dwp: dwp.as_ref(),
dwo_base,
layout: &layout,
executable_ranges: &layout.ranges,
builder: &mut builder,
include_inlines: dwarf_options.inline_info,
include_call_sites: dwarf_options.call_sites,
stats: &mut stats,
warnings: &mut warnings,
})?;
}
Ok(ConversionReport {
builder,
stats,
warnings,
discovered_debug: None,
discovered_supplementary: None,
discovered_dwp: None,
})
}
pub fn convert_path(&self, image_path: impl AsRef<Path>) -> Result<ConversionReport> {
self.convert_path_with_observer(image_path, |_| {})
}
pub fn convert_path_with_observer(
&self,
image_path: impl AsRef<Path>,
mut observer: impl FnMut(DiscoveryEvent<'_>),
) -> Result<ConversionReport> {
let image_path = image_path.as_ref();
let image_bytes = read_file(image_path, "read ELF image")?;
let discovery_cache = self.discovery_cache.get_or_init(Arc::default);
let discovery_enabled = self.options.discovery == DiscoveryPolicy::Enabled;
let companion_discovery_enabled =
discovery_enabled && (self.options.include_symbols || self.options.dwarf.is_some());
let discovery = if companion_discovery_enabled {
discover_separate_debug(
image_path,
&image_bytes,
&self.options,
discovery_cache,
&mut observer,
)?
} else {
Discovery::default()
};
let debug_path = discovery
.artifact
.as_ref()
.map_or(image_path, |artifact| artifact.path.as_path());
let debug_source = discovery
.artifact
.as_ref()
.map_or(image_bytes.as_slice(), |artifact| artifact.bytes.as_slice());
let dwarf_discovery_enabled = discovery_enabled && self.options.dwarf.is_some();
let supplementary_discovery = if dwarf_discovery_enabled {
discover_supplementary(
debug_path,
debug_source,
&self.options,
discovery_cache,
&mut observer,
)?
} else {
Discovery::default()
};
let dwp = dwarf_discovery_enabled
.then(|| discover_dwp(image_path, debug_path))
.flatten();
let dwo_base = dwarf_discovery_enabled
.then(|| debug_path.parent())
.flatten();
let mut report = self.convert_inner(
ElfInputs {
image: &image_bytes,
debug: discovery
.artifact
.as_ref()
.map(|artifact| artifact.bytes.as_slice()),
symbols: None,
supplementary: supplementary_discovery
.artifact
.as_ref()
.map(|artifact| artifact.bytes.as_slice()),
dwp: dwp.as_ref().map(|artifact| artifact.bytes.as_slice()),
},
dwo_base,
)?;
report.warnings.extend(discovery.warnings);
report.warnings.extend(supplementary_discovery.warnings);
report.discovered_debug = discovery.artifact.map(|artifact| artifact.path);
report.discovered_supplementary = supplementary_discovery
.artifact
.map(|artifact| artifact.path);
report.discovered_dwp = dwp.map(|artifact| artifact.path);
Ok(report)
}
}
fn import_distinct_symbols(
sources: &[(&[u8], &object::File<'_>)],
layout: &AddressLayout,
builder: &mut GsymBuilder,
rejected: &mut usize,
) -> Result<usize> {
let mut visited: Vec<&[u8]> = Vec::with_capacity(sources.len());
let mut unique = Vec::with_capacity(sources.len());
for &(bytes, file) in sources {
if !visited.iter().any(|other| std::ptr::eq(*other, bytes)) {
visited.push(bytes);
unique.push(file);
}
}
if let [file] = unique.as_slice() {
let mut imported = 0_usize;
return image::visit_symbols(file, layout, |function, disposition| {
match disposition {
image::SymbolDisposition::Import => {
builder.add_function(function)?;
imported = imported.saturating_add(1);
}
image::SymbolDisposition::Reject => {
*rejected = rejected.saturating_add(1);
}
}
Ok(())
})
.map(|()| imported);
}
let mut accepted = Vec::new();
let mut skipped = Vec::new();
for file in unique {
image::visit_symbols(file, layout, |function, disposition| {
match disposition {
image::SymbolDisposition::Import => accepted.push(function),
image::SymbolDisposition::Reject => skipped.push(function),
}
Ok(())
})?;
}
accepted.sort_unstable();
accepted.dedup();
skipped.sort_unstable();
skipped.dedup();
skipped.retain(|function| accepted.binary_search(function).is_err());
let imported = accepted.len();
*rejected = rejected.saturating_add(skipped.len());
for function in accepted {
builder.add_function(function)?;
}
Ok(imported)
}
fn read_file(path: &Path, description: &'static str) -> Result<Vec<u8>> {
std::fs::read(path).map_err(|source| Error::IoAtPath {
operation: description,
path: path.to_path_buf(),
source,
})
}