use std::borrow::Cow;
use std::collections::HashMap;
use gimli::{AttributeValue, DebugLineOffset, DwarfFileType, Format, Reader, Section};
use super::references::attribute_reader;
use super::{DW_AT_LLVM_STMT_SEQUENCE, gimli_error};
use crate::convert::ConversionWarning;
use crate::model::{AddressRange, FileEntry, FileIndex, LineEntry};
use crate::normalize::compact_line_rows;
use crate::{Error, GsymBuilder, Result};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct SequencedLine {
pub(super) entry: LineEntry,
pub(super) statement_sequence: Option<u64>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct LineSequenceRange {
pub(super) range: AddressRange,
pub(super) statement_sequence: Option<u64>,
}
pub(super) struct UnitLines {
pub(super) entries: Vec<SequencedLine>,
pub(super) files: HashMap<u64, FileIndex>,
pub(super) sequences: Vec<LineSequenceRange>,
}
impl UnitLines {
pub(super) fn for_range(
&self,
range: AddressRange,
requested_sequence: Option<u64>,
) -> (Vec<LineEntry>, bool) {
let sequence_exists = requested_sequence.is_none_or(|requested| {
self.sequences
.iter()
.any(|sequence| sequence.statement_sequence == Some(requested))
});
let selected_sequence = requested_sequence.filter(|_| sequence_exists);
let clamping_sequence = selected_sequence.or_else(|| {
self.sequences
.iter()
.find(|sequence| sequence.range.contains(range.start))
.and_then(|sequence| sequence.statement_sequence)
});
let start = self
.entries
.partition_point(|line| line.entry.address < range.start);
let end = self
.entries
.partition_point(|line| line.entry.address < range.end);
let mut output = self
.entries
.get(start..end)
.unwrap_or_default()
.iter()
.filter(|line| {
selected_sequence.is_none_or(|selected| line.statement_sequence == Some(selected))
})
.map(|line| line.entry)
.collect::<Vec<_>>();
if self.sequences.iter().any(|sequence| {
selected_sequence.is_none_or(|selected| sequence.statement_sequence == Some(selected))
&& sequence.range.contains(range.start)
}) && output.first().is_none_or(|line| line.address > range.start)
&& let Some(previous) = self
.entries
.get(..start)
.unwrap_or_default()
.iter()
.rev()
.find(|line| {
clamping_sequence
.is_none_or(|selected| line.statement_sequence == Some(selected))
})
{
let mut clamped = previous.entry;
clamped.address = range.start;
output.insert(0, clamped);
}
compact_line_rows(&mut output);
let invalid_sequence =
requested_sequence.is_some() && !sequence_exists && !output.is_empty();
(output, invalid_sequence)
}
}
pub(super) fn collect_lines<R: Reader<Offset = usize>>(
dwarf: &gimli::Dwarf<R>,
unit: &gimli::Unit<R>,
builder: &mut GsymBuilder,
warnings: &mut Vec<ConversionWarning>,
) -> Result<UnitLines> {
let line_program = if let Some(line_program) = unit.line_program.clone() {
line_program
} else if dwarf.file_type == DwarfFileType::Dwo
&& matches!(unit.header.version(), 4 | 5)
&& !dwarf.debug_line.reader().is_empty()
{
dwarf
.debug_line
.program(
DebugLineOffset(0),
unit.header.address_size(),
unit.comp_dir.clone(),
unit.name.clone(),
)
.map_err(gimli_error)?
} else {
return Ok(UnitLines {
entries: Vec::new(),
files: HashMap::new(),
sequences: Vec::new(),
});
};
let (program, sequences) = line_program.sequences().map_err(gimli_error)?;
let header = program.header();
let statement_sequence_offsets = line_sequence_offsets(header)?;
if statement_sequence_offsets.len() != sequences.len() {
warnings.push(ConversionWarning::LineSequenceMismatch {
sequences: sequences.len(),
offsets: statement_sequence_offsets.len(),
});
}
let mut files = intern_header_files(dwarf, unit, header, builder)?;
let mut output = Vec::new();
let mut sequence_ranges = Vec::new();
for (index, sequence) in sequences.into_iter().enumerate() {
let statement_sequence = statement_sequence_offsets.get(index).copied();
if sequence.start < sequence.end {
sequence_ranges.push(LineSequenceRange {
range: AddressRange::new(sequence.start, sequence.end),
statement_sequence,
});
}
let mut rows = program.resume_from(&sequence);
while let Some((header, row)) = rows.next_row().map_err(gimli_error)? {
if row.end_sequence() {
continue;
}
let dwarf_index = row.file_index();
let file = if let Some(file) = files.get(&dwarf_index).copied() {
file
} else {
let Some(entry) = row.file(header) else {
warnings.push(ConversionWarning::MissingLineFile {
address: row.address(),
index: dwarf_index,
});
continue;
};
let Some(file) = intern_file(dwarf, unit, header, entry, builder)? else {
continue;
};
files.insert(dwarf_index, file);
file
};
let line = match row.line() {
None => 0,
Some(line) => {
if let Ok(line) = u32::try_from(line.get()) {
line
} else {
warnings.push(ConversionWarning::UnrepresentableLine {
address: row.address(),
line: line.get(),
});
continue;
}
}
};
output.push(SequencedLine {
entry: LineEntry {
address: row.address(),
file,
line,
},
statement_sequence,
});
}
}
output.sort_by_key(|row| row.entry.address);
output.dedup();
Ok(UnitLines {
entries: output,
files,
sequences: sequence_ranges,
})
}
pub(super) fn statement_sequence_offset<R: Reader<Offset = usize>>(
unit: &gimli::Unit<R>,
entry: &gimli::DebuggingInformationEntry<R>,
) -> Option<u64> {
let value = match entry.attr_value(DW_AT_LLVM_STMT_SEQUENCE) {
Some(AttributeValue::SecOffset(offset)) => Some(offset as u64),
Some(AttributeValue::DebugLineRef(offset)) => Some(offset.0 as u64),
Some(value) => value.udata_value(),
None => None,
}?;
let invalid = match unit.encoding().format {
Format::Dwarf32 => u64::from(u32::MAX),
Format::Dwarf64 => u64::MAX,
};
(value != invalid).then_some(value)
}
fn line_sequence_offsets<R: Reader<Offset = usize>>(
header: &gimli::LineProgramHeader<R>,
) -> Result<Vec<u64>> {
let program = header.raw_program_buf();
let program = program.to_slice().map_err(gimli_error)?;
let standard_opcode_lengths = header
.standard_opcode_lengths()
.to_slice()
.map_err(gimli_error)?;
let encoding = header.encoding();
let program_offset = (header.offset().0 as u64)
.checked_add(u64::from(encoding.format.initial_length_size()))
.and_then(|offset| offset.checked_add(2))
.and_then(|offset| offset.checked_add(u64::from(encoding.version >= 5).saturating_mul(2)))
.and_then(|offset| offset.checked_add(u64::from(encoding.format.word_size())))
.and_then(|offset| offset.checked_add(header.header_length() as u64))
.ok_or(Error::Overflow("DWARF line-program offset"))?;
scan_line_sequence_offsets(
program.as_ref(),
standard_opcode_lengths.as_ref(),
header.opcode_base(),
program_offset,
)
}
pub(super) fn scan_line_sequence_offsets(
program: &[u8],
standard_opcode_lengths: &[u8],
opcode_base: u8,
program_offset: u64,
) -> Result<Vec<u64>> {
let mut cursor = 0_usize;
let mut sequence_start = program_offset;
let mut output = Vec::new();
while cursor < program.len() {
let opcode = read_line_byte(program, &mut cursor)?;
if opcode == 0 {
let length = usize::try_from(read_line_uleb(program, &mut cursor)?)
.map_err(|_| Error::Overflow("extended DWARF line opcode length"))?;
if length == 0 {
return Err(Error::malformed(
"DWARF line program",
"zero-length extended opcode",
));
}
let payload_start = cursor;
let subopcode = read_line_byte(program, &mut cursor)
.map_err(|_| Error::malformed("DWARF line program", "truncated extended opcode"))?;
take_line_bytes(program, &mut cursor, length.saturating_sub(1))?;
if subopcode == gimli::constants::DW_LNE_end_sequence.0 {
output.push(sequence_start);
sequence_start = program_offset
.checked_add(cursor as u64)
.ok_or(Error::Overflow("DWARF line sequence offset"))?;
}
debug_assert_eq!(cursor, payload_start.saturating_add(length));
} else if opcode < opcode_base {
match gimli::DwLns(opcode) {
gimli::constants::DW_LNS_fixed_advance_pc => {
take_line_bytes(program, &mut cursor, 2)?;
}
gimli::constants::DW_LNS_copy
| gimli::constants::DW_LNS_negate_stmt
| gimli::constants::DW_LNS_set_basic_block
| gimli::constants::DW_LNS_const_add_pc
| gimli::constants::DW_LNS_set_prologue_end
| gimli::constants::DW_LNS_set_epilogue_begin => {}
_ => {
let operand_count = standard_opcode_lengths
.get(usize::from(opcode.saturating_sub(1)))
.copied()
.unwrap_or(0);
for _ in 0..operand_count {
let _ = read_line_uleb(program, &mut cursor)?;
}
}
}
}
}
Ok(output)
}
fn read_line_byte(program: &[u8], cursor: &mut usize) -> Result<u8> {
let byte = program
.get(*cursor)
.copied()
.ok_or_else(|| Error::malformed("DWARF line program", "truncated instruction"))?;
*cursor = cursor.saturating_add(1);
Ok(byte)
}
fn take_line_bytes(program: &[u8], cursor: &mut usize, length: usize) -> Result<()> {
let end = cursor
.checked_add(length)
.ok_or(Error::Overflow("DWARF line instruction length"))?;
program
.get(*cursor..end)
.ok_or_else(|| Error::malformed("DWARF line program", "truncated instruction"))?;
*cursor = end;
Ok(())
}
fn read_line_uleb(program: &[u8], cursor: &mut usize) -> Result<u64> {
let mut value = 0_u64;
for shift in (0..=63).step_by(7) {
let byte = read_line_byte(program, cursor)?;
let payload = u64::from(byte & 0x7f);
if shift == 63 && payload > 1 {
return Err(Error::malformed(
"DWARF line program",
"overflowing ULEB128 operand",
));
}
value |= payload << shift;
if byte & 0x80 == 0 {
return Ok(value);
}
}
Err(Error::malformed(
"DWARF line program",
"overlong ULEB128 operand",
))
}
pub(super) fn intern_header_files<R: Reader<Offset = usize>>(
dwarf: &gimli::Dwarf<R>,
unit: &gimli::Unit<R>,
header: &gimli::LineProgramHeader<R>,
builder: &mut GsymBuilder,
) -> Result<HashMap<u64, FileIndex>> {
let first_file_index = u64::from(header.version() <= 4);
let mut files = HashMap::with_capacity(header.file_names().len());
for (offset, file) in header.file_names().iter().enumerate() {
let dwarf_index = first_file_index
.checked_add(
u64::try_from(offset).map_err(|_| Error::Overflow("DWARF file-table index"))?,
)
.ok_or(Error::Overflow("DWARF file-table index"))?;
if let Some(gsym_index) = intern_file(dwarf, unit, header, file, builder)? {
files.insert(dwarf_index, gsym_index);
}
}
Ok(files)
}
fn intern_file<R: Reader<Offset = usize>>(
dwarf: &gimli::Dwarf<R>,
unit: &gimli::Unit<R>,
header: &gimli::LineProgramHeader<R>,
file: &gimli::FileEntry<R>,
builder: &mut GsymBuilder,
) -> Result<Option<FileIndex>> {
let Some(filename_reader) = attribute_reader(dwarf, unit, file.path_name())? else {
return Ok(None);
};
let filename = filename_reader.to_slice().map_err(gimli_error)?;
if filename.is_empty() {
return Ok(None);
}
let (filename_directory, basename) = split_path(&filename);
if basename.is_empty() {
return Ok(None);
}
let directory = if is_absolute_path(&filename) {
filename_directory.to_vec()
} else {
let implicit_comp_dir = header.version() < 5 && file.directory_index() == 0;
let directory_reader = match file.directory(header) {
Some(value) if !implicit_comp_dir => attribute_reader(dwarf, unit, value)?,
_ => None,
};
let directory = match &directory_reader {
Some(value) => value.to_slice().map_err(gimli_error)?,
None => Cow::<[u8]>::Borrowed(&[]),
};
let needs_comp_dir = header.version() < 5 || file.directory_index() != 0;
let comp_dir = if needs_comp_dir && !is_absolute_path(&directory) {
unit.comp_dir
.as_ref()
.map(Reader::to_slice)
.transpose()
.map_err(gimli_error)?
} else {
None
};
let capacity = directory
.len()
.saturating_add(comp_dir.as_ref().map_or(0, |path| path.len()))
.saturating_add(filename_directory.len())
.saturating_add(2);
let mut path = if capacity == 2 {
Vec::new()
} else {
Vec::with_capacity(capacity)
};
if let Some(comp_dir) = comp_dir {
append_path(&mut path, &comp_dir);
}
append_path(&mut path, &directory);
append_path(&mut path, filename_directory);
path
};
builder
.add_file(FileEntry::new(directory, basename))
.map(Some)
}
fn is_absolute_path(path: &[u8]) -> bool {
if path.starts_with(b"/") {
return true;
}
let windows_drive = match path {
[_, b':', separator, ..] => matches!(separator, b'/' | b'\\'),
_ => false,
};
if windows_drive {
return true;
}
match path {
[first, second, third, rest @ ..] => {
matches!(first, b'/' | b'\\')
&& first == second
&& !matches!(third, b'/' | b'\\')
&& rest.iter().any(|byte| matches!(byte, b'/' | b'\\'))
}
_ => false,
}
}
fn append_path(path: &mut Vec<u8>, component: &[u8]) {
if component.is_empty() {
return;
}
if !path.is_empty() && !path.ends_with(b"/") && !component.starts_with(b"/") {
path.push(b'/');
}
path.extend_from_slice(component);
}
fn split_path(path: &[u8]) -> (&[u8], &[u8]) {
let Some(separator) = path.iter().rposition(|byte| *byte == b'/') else {
return (&[], path);
};
let (directory, basename) = path.split_at(separator.saturating_add(1));
let directory = if separator == 0 {
directory
} else {
directory.strip_suffix(b"/").unwrap_or(directory)
};
(directory, basename)
}
#[cfg(test)]
mod tests {
use super::{append_path, is_absolute_path, split_path};
#[test]
fn recognizes_llvm_absolute_paths_from_posix_and_windows() {
for path in [
b"/src/main.c".as_slice(),
br"C:\src\main.c".as_slice(),
br"\\server\share\main.c".as_slice(),
] {
assert!(is_absolute_path(path), "{}", String::from_utf8_lossy(path));
}
for path in [
b"src/main.c".as_slice(),
br"C:src\main.c".as_slice(),
br"\src\main.c".as_slice(),
br"\\server".as_slice(),
] {
assert!(!is_absolute_path(path), "{}", String::from_utf8_lossy(path));
}
}
#[test]
fn joins_and_splits_native_posix_paths_without_normalizing() {
let mut path = Vec::new();
append_path(&mut path, b"/recorded/build");
append_path(&mut path, b"../src");
append_path(&mut path, b"main.c");
assert_eq!(
split_path(&path),
(b"/recorded/build/../src".as_slice(), b"main.c".as_slice())
);
assert_eq!(
split_path(b"/main.c"),
(b"/".as_slice(), b"main.c".as_slice())
);
}
}