use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use core::fmt;
use crate::common::{ARM_TARGET2_SIZE, BaseAddresses, Encoding, LsdaOffset, Unwinder, padding};
use crate::constants::{DW_EH_PE_SLEB128, DW_EH_PE_ULEB128};
use crate::endianity::Endianity;
use crate::leb128;
use crate::read::{self, Reader};
use crate::write::{
Action, ActionId, Address, CallSite, Entry, EntryData, Error, ExceptionSpec, Filter,
GccExceptTable, LpStart, Lsda, LsdaId, Result, SpecId, TypeId, Writer,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConvertError {
Read(read::Error),
Write(Error),
}
impl fmt::Display for ConvertError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ConvertError::Read(e) => write!(f, "read error: {e}"),
ConvertError::Write(e) => write!(f, "write error: {e}"),
}
}
}
impl core::error::Error for ConvertError {}
impl From<read::Error> for ConvertError {
fn from(e: read::Error) -> Self {
ConvertError::Read(e)
}
}
impl From<Error> for ConvertError {
fn from(e: Error) -> Self {
ConvertError::Write(e)
}
}
pub type ConvertResult<T> = core::result::Result<T, ConvertError>;
const ALIGNMENTS: [u32; 5] = [4, 8, 16, 2, 1];
const MAX_UNREFERENCED: usize = 256;
impl GccExceptTable {
pub fn from_read<R: Reader>(
section: &read::GccExceptTable<R>,
offsets: &[LsdaOffset],
bases: &BaseAddresses,
) -> ConvertResult<(GccExceptTable, BTreeMap<LsdaOffset, LsdaId>)> {
let format = section.format();
let data = section.reader().to_slice()?;
let mut offsets: Vec<LsdaOffset> = offsets.to_vec();
offsets.sort_unstable();
offsets.dedup();
if offsets.last().is_some_and(|last| last.0 >= data.len()) {
return Err(read::Error::OffsetOutOfBounds.into());
}
let mut table = GccExceptTable::new(format);
let mut ids = BTreeMap::new();
let endian = section.reader().endian();
let mut cursor = 0;
for (i, &offset) in offsets.iter().enumerate() {
let region_end = offsets.get(i + 1).map_or(data.len(), |next| next.0);
let start = offset.0;
let gap = &data[cursor..start];
let padded = gap.iter().all(|&b| b == 0);
let alignment = ALIGNMENTS
.into_iter()
.find(|&a| padded && cursor + padding(cursor, a as usize) == start);
if alignment.is_none() {
table.add(Entry {
alignment: 1,
data: EntryData::Raw(gap.to_vec()),
});
}
let alignment = alignment.unwrap_or(1);
let lsda = section.lsda_at(offset)?;
let mut converted = None;
if let Some(candidate) = convert_lsda(&lsda, section.reader(), bases, region_end)? {
for layout in &candidate.layouts {
if layout.end > region_end {
continue;
}
let model = candidate.with_layout(*layout);
let mut check = Check::new(endian, start, &data[start..layout.end]);
if model.write(&mut check, format, bases).is_ok() && check.is_complete() {
converted = Some((model, layout.end));
break;
}
}
}
let (entry_data, end) = match converted {
Some((lsda, end)) => (EntryData::Lsda(lsda), end),
None => (EntryData::Raw(data[start..region_end].to_vec()), region_end),
};
ids.insert(
offset,
table.add(Entry {
alignment,
data: entry_data,
}),
);
cursor = end;
}
if cursor < data.len() {
table.add(Entry {
alignment: 1,
data: EntryData::Raw(data[cursor..].to_vec()),
});
}
Ok((table, ids))
}
}
#[derive(Debug, Clone, Copy)]
struct Layout {
actions: usize,
types: usize,
exception_specs: usize,
alignment: u32,
end: usize,
}
struct Candidate {
lsda: Lsda,
layouts: Vec<Layout>,
}
impl Candidate {
fn with_layout(&self, layout: Layout) -> Lsda {
let mut lsda = self.lsda.clone();
lsda.actions.truncate(layout.actions);
lsda.types.truncate(layout.types);
lsda.exception_specs.truncate(layout.exception_specs);
lsda.type_table_alignment = layout.alignment;
lsda
}
}
fn prefix_max<T>(items: &[T], initial: usize, f: impl Fn(&T) -> usize) -> Vec<usize> {
let mut max = Vec::with_capacity(items.len() + 1);
max.push(initial);
for (i, item) in items.iter().enumerate() {
max.push(max[i].max(f(item)));
}
max
}
#[allow(clippy::too_many_lines)]
fn convert_lsda<R: Reader>(
lsda: &read::Lsda<R>,
section: &R,
bases: &BaseAddresses,
region_end: usize,
) -> ConvertResult<Option<Candidate>> {
let tables = match lsda.parse_tables() {
Ok(tables) => tables,
Err(e @ read::Error::MissingBaseAddress(_)) => return Err(e.into()),
Err(_) => return Ok(None),
};
let constant = |pointer: read::Pointer| -> ConvertResult<Address> {
Ok(Address::Constant(pointer.address(bases)?))
};
let arm = lsda.format().unwinder == Unwinder::ArmEhabi;
let lpstart = match lsda.lpstart() {
Some(pointer) => Some(LpStart {
encoding: pointer.encoding(),
address: constant(pointer)?,
}),
None => None,
};
let referenced_actions = tables.actions.len();
let referenced_specs = tables.exception_specs.len();
let mut records = tables.actions.clone();
let mut specs = tables.exception_specs.clone();
let mut spec_ends = Vec::new();
let action_table = lsda.action_table_offset();
if let Some(base) = lsda.ttype_base() {
let mut offset = tables.action_table_end;
while offset < base && records.len() < referenced_actions + MAX_UNREFERENCED {
let Ok(action) = lsda.action_at(read::ActionOffset(offset - action_table)) else {
break;
};
offset += action.len();
records.push(action);
}
let unit = if arm { ARM_TARGET2_SIZE as usize } else { 1 };
let mut offset = if specs.is_empty() { base } else { tables.end };
while offset < region_end && specs.len() < referenced_specs + MAX_UNREFERENCED {
let spec = read::SpecOffset((offset - base) / unit);
let Ok(mut iter) = lsda.exception_spec(spec) else {
break;
};
let mut entries = Vec::new();
let terminated = loop {
match iter.next() {
Ok(Some(entry)) => entries.push(entry),
Ok(None) => break true,
Err(_) => break false,
}
};
offset = iter.offset();
if !terminated || offset > region_end {
break;
}
specs.push(read::ExceptionSpec {
offset: spec,
entries,
});
spec_ends.push(offset);
}
}
let action_id = |offset: read::ActionOffset| {
records
.binary_search_by_key(&offset, |action| action.offset)
.ok()
.map(ActionId)
};
let spec_id = |offset: read::SpecOffset| {
specs
.binary_search_by_key(&offset, |spec| spec.offset)
.ok()
.map(SpecId)
};
let type_id = |index: u64| TypeId(index as usize - 1);
let mut actions = Vec::with_capacity(records.len());
for (i, action) in records.iter().enumerate() {
let filter = match action.filter {
read::Filter::Cleanup => Some(Filter::Cleanup),
read::Filter::Catch(index) => Some(Filter::Catch(type_id(index))),
read::Filter::ExceptionSpec(offset) => spec_id(offset).map(Filter::ExceptionSpec),
};
let next = match action.next {
Some(offset) => action_id(offset).map(Some),
None => Some(None),
};
let (Some(filter), Some(next)) = (filter, next) else {
if i < referenced_actions {
return Ok(None);
}
break;
};
actions.push(Action { filter, next });
}
let referenced_records = prefix_max(&actions, 0, |action| action.next.map_or(0, |id| id.0 + 1));
let action_types = prefix_max(&actions, 0, |action| match action.filter {
Filter::Catch(id) => id.0 + 1,
_ => 0,
});
let action_specs = prefix_max(&actions, referenced_specs, |action| match action.filter {
Filter::ExceptionSpec(id) => id.0 + 1,
_ => 0,
});
let mut call_sites = Vec::with_capacity(tables.call_sites.len());
for call_site in &tables.call_sites {
let action = match call_site.action {
Some(offset) => match action_id(offset) {
Some(id) => Some(id),
None => return Ok(None),
},
None => None,
};
let Ok(leb128_lengths) = leb128_lengths(lsda, section, call_site) else {
return Ok(None);
};
call_sites.push(CallSite {
start: call_site.start,
length: call_site.length,
landing_pad: call_site.landing_pad,
action,
leb128_lengths,
});
}
let mut types = Vec::with_capacity(tables.types.len());
for pointer in &tables.types {
types.push(if pointer.is_null() {
None
} else {
Some(constant(*pointer)?)
});
}
let mut exception_specs = Vec::with_capacity(specs.len());
for spec in &specs {
let entries = spec.entries.iter();
exception_specs.push(if arm {
ExceptionSpec::TypeInfos(
entries
.filter_map(|entry| match entry {
read::SpecEntry::TypeInfo(pointer) => {
Some(Address::Constant(pointer.value()))
}
read::SpecEntry::Type(_) => None,
})
.collect(),
)
} else {
ExceptionSpec::Types(
entries
.filter_map(|entry| match *entry {
read::SpecEntry::Type(index) => Some(type_id(index)),
read::SpecEntry::TypeInfo(_) => None,
})
.collect(),
)
});
}
let spec_types = prefix_max(&exception_specs, 0, |spec| match spec {
ExceptionSpec::Types(types) => types.iter().map(|id| id.0 + 1).max().unwrap_or(0),
ExceptionSpec::TypeInfos(_) => 0,
});
let mut layouts = Vec::new();
let Some(base) = lsda.ttype_base() else {
layouts.push(Layout {
actions: referenced_actions,
types: 0,
exception_specs: 0,
alignment: 4,
end: tables.end,
});
return Ok(Some(Candidate {
lsda: Lsda {
lpstart,
ttype_encoding: lsda.ttype_encoding(),
call_site_encoding: lsda.call_site_encoding(),
type_table_alignment: 4,
call_sites,
actions,
types,
exception_specs,
},
layouts,
}));
};
let Ok(size) = lsda.ttype_entry_size() else {
return Ok(None);
};
let available = base.saturating_sub(tables.action_table_end) / size;
for index in types.len() + 1..=available {
let Ok(pointer) = lsda.type_entry(index as u64) else {
return Ok(None);
};
types.push(if pointer.is_null() {
None
} else {
Some(constant(pointer)?)
});
}
let mut non_null = Vec::with_capacity(types.len() + 1);
non_null.push(0);
for (i, entry) in types.iter().enumerate() {
non_null.push(if entry.is_some() { i + 1 } else { non_null[i] });
}
let entry_alignment = size as u32;
let alignments = [entry_alignment]
.into_iter()
.chain(ALIGNMENTS.into_iter().filter(|&a| a != entry_alignment));
for count in referenced_actions..=actions.len() {
let spec_count = action_specs[count];
if referenced_records[count] > count || spec_count > exception_specs.len() {
continue;
}
let end = match spec_count.checked_sub(referenced_specs + 1) {
Some(extra) => spec_ends[extra],
None => tables.end,
};
let action_end = match count.checked_sub(1) {
Some(last) => action_table + records[last].offset.0 + records[last].len(),
None => action_table,
};
let referenced = action_types[count].max(spec_types[spec_count]);
let entries = if count == referenced_actions {
types.len()
} else {
types.len().min(base.saturating_sub(action_end) / size)
};
let fewest = referenced.max(non_null[entries]);
for alignment in alignments.clone() {
let types_start = action_end + padding(action_end, alignment as usize);
let Some(table_size) = base.checked_sub(types_start) else {
continue;
};
let type_count = table_size / size;
if table_size % size == 0 && (fewest..=entries).contains(&type_count) {
layouts.push(Layout {
actions: count,
types: type_count,
exception_specs: spec_count,
alignment,
end,
});
}
}
}
Ok(Some(Candidate {
lsda: Lsda {
lpstart,
ttype_encoding: lsda.ttype_encoding(),
call_site_encoding: lsda.call_site_encoding(),
type_table_alignment: 4,
call_sites,
actions,
types,
exception_specs,
},
layouts,
}))
}
fn leb128_lengths<R: Reader>(
lsda: &read::Lsda<R>,
section: &R,
call_site: &read::CallSite,
) -> read::Result<[u8; 3]> {
let sjlj = lsda.format().unwinder == Unwinder::SjLj;
let encoding = if sjlj {
Encoding::ULEB128
} else {
lsda.call_site_encoding()
};
let signed = encoding.format() == DW_EH_PE_SLEB128;
if encoding.format() != DW_EH_PE_ULEB128 && !signed {
return Ok([0; 3]);
}
let mut input = section.range_from(call_site.offset)?;
let mut lengths = [0; 3];
let fields = if sjlj { 2..3 } else { 0..3 };
for field in fields {
let before = input.len();
let shortest = if signed {
leb128::signed_len(input.read_sleb128()?)
} else {
leb128::unsigned_len(input.read_uleb128()?)
};
let len = before - input.len();
if len > shortest {
lengths[field] = u8::try_from(len).map_err(|_| read::Error::BadUnsignedLeb128)?;
}
}
Ok(lengths)
}
#[derive(Debug)]
struct Check<'a, E: Endianity> {
endian: E,
start: usize,
expected: &'a [u8],
written: usize,
}
impl<'a, E: Endianity> Check<'a, E> {
fn new(endian: E, start: usize, expected: &'a [u8]) -> Self {
Check {
endian,
start,
expected,
written: 0,
}
}
fn is_complete(&self) -> bool {
self.written == self.expected.len()
}
}
impl<E: Endianity> Writer for Check<'_, E> {
type Endian = E;
fn endian(&self) -> Self::Endian {
self.endian
}
fn len(&self) -> usize {
self.start + self.written
}
fn write(&mut self, bytes: &[u8]) -> Result<()> {
let end = self.written + bytes.len();
if self.expected.get(self.written..end) != Some(bytes) {
return Err(Error::OffsetOutOfBounds);
}
self.written = end;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Format;
use crate::endianity::LittleEndian;
use crate::write::EndianVec;
fn roundtrip(data: &[u8], offsets: &[usize], format: Format) -> GccExceptTable {
let bases = BaseAddresses::default().set_section(0x4000);
let section = read::GccExceptTable::new(data, LittleEndian, format);
let offsets: Vec<_> = offsets.iter().map(|&o| LsdaOffset(o)).collect();
let (table, ids) = GccExceptTable::from_read(§ion, &offsets, &bases).unwrap();
assert!(offsets.iter().all(|offset| ids.contains_key(offset)));
let mut w = EndianVec::new(LittleEndian);
let written = table.write(&mut w, &bases).unwrap();
assert_eq!(w.slice(), data);
for (offset, id) in ids {
assert_eq!(written.get(id), offset);
}
table
}
#[test]
fn test_unreferenced_records() {
let data = [
0xff, 0x9b, 0x05, 0x01, 0x00, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x01, 0x00, ];
let table = roundtrip(&data, &[0, 12], Format::new(8));
let EntryData::Lsda(lsda) = &table.entries()[0].data else {
panic!("expected an LSDA");
};
assert_eq!(lsda.call_sites, []);
assert_eq!(
lsda.actions,
[Action {
filter: Filter::ExceptionSpec(SpecId(0)),
next: None
}]
);
assert_eq!(lsda.exception_specs, [ExceptionSpec::Types(Vec::new())]);
}
#[test]
fn test_padded_leb128() {
let data = [
0xff, 0xff, 0x01, 0x05, 0x10, 0xf0, 0x00, 0x00, 0x00, ];
let table = roundtrip(&data, &[0], Format::new(8));
let EntryData::Lsda(lsda) = &table.entries()[0].data else {
panic!("expected an LSDA");
};
assert_eq!(lsda.call_sites[0].length, 0x70);
assert_eq!(lsda.call_sites[0].leb128_lengths, [0, 2, 0]);
}
#[test]
fn test_roundtrip() {
let mut data = Vec::new();
data.extend([0xff, 0xff, 0x01, 0x04, 0x00, 0x04, 0x08, 0x00]);
let mut lsda = Lsda::new();
lsda.ttype_encoding = crate::Encoding::PCREL_SDATA4;
let int = lsda.add_type(Some(Address::Constant(0x5000)));
let action = lsda.add_action(Action {
filter: Filter::Catch(int),
next: None,
});
lsda.call_sites.push(CallSite {
start: 0,
length: 4,
landing_pad: 8,
action: Some(action),
..Default::default()
});
let mut w = EndianVec::new(LittleEndian);
w.write(&data).unwrap();
let second = w.len();
lsda.write(
&mut w,
Format::new(8),
&BaseAddresses::default().set_section(0x4000),
)
.unwrap();
let data = w.into_vec();
let table = roundtrip(&data, &[second, 0, second], Format::new(8));
assert_eq!(table.entries().len(), 2);
assert!(matches!(table.entries()[0].data, EntryData::Lsda(_)));
let EntryData::Lsda(converted) = &table.entries()[1].data else {
panic!("expected an LSDA");
};
assert_eq!(converted, &lsda);
}
#[test]
fn test_raw_fallback() {
let mut data = alloc::vec![0xaa, 0xbb];
let start = data.len();
data.extend([0xff, 0xff, 0x01, 0x84, 0x00, 0x00, 0x04, 0x08, 0x00]);
data.extend([0x01, 0x02]);
let table = roundtrip(&data, &[start], Format::new(4));
assert_eq!(table.entries().len(), 2);
assert!(matches!(&table.entries()[0].data, EntryData::Raw(raw) if raw == &[0xaa, 0xbb]));
assert!(matches!(&table.entries()[1].data, EntryData::Raw(raw) if raw.len() == 11));
}
#[test]
fn test_malformed_lsda_is_raw() {
let data = [0xff, 0xff, 0x01, 0x04, 0x00, 0x04, 0x00, 0x01, 0x7f, 0x00];
let table = roundtrip(&data, &[0], Format::new(8));
assert!(matches!(&table.entries()[0].data, EntryData::Raw(raw) if raw == &data));
}
#[test]
fn test_unreferenced_types() {
let mut lsda = Lsda::new();
lsda.ttype_encoding = crate::Encoding::UDATA4;
let used = lsda.add_type(Some(Address::Constant(0x100)));
lsda.add_type(Some(Address::Constant(0x200)));
let action = lsda.add_action(Action {
filter: Filter::Catch(used),
next: None,
});
lsda.call_sites.push(CallSite {
start: 0,
length: 4,
landing_pad: 8,
action: Some(action),
..Default::default()
});
let mut w = EndianVec::new(LittleEndian);
lsda.write(&mut w, Format::new(4), &BaseAddresses::default())
.unwrap();
let table = roundtrip(w.slice(), &[0], Format::new(4));
let EntryData::Lsda(converted) = &table.entries()[0].data else {
panic!("expected an LSDA");
};
assert_eq!(converted, &lsda);
}
#[test]
fn test_trailing_data() {
let data = [0xff, 0xff, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09];
let table = roundtrip(&data, &[0], Format::new(8).with_unwinder(Unwinder::SjLj));
assert_eq!(table.entries().len(), 2);
}
}