thiserror + expose some as lib

This commit is contained in:
Corwin 2024-04-18 00:43:47 +01:00
parent 72e7850152
commit f269e6364a
No known key found for this signature in database
5 changed files with 148 additions and 47 deletions

View file

@ -8,7 +8,7 @@ description = "CLI utility to convert agb stack trace dumps into human readable
repository = "https://github.com/agbrs/agb" repository = "https://github.com/agbrs/agb"
[dependencies] [dependencies]
anyhow = "1" thiserror = "1"
clap = { version = "4", features = ["derive"] } clap = { version = "4", features = ["derive"] }
addr2line = { version = "0.22", default-features = false, features = [ addr2line = { version = "0.22", default-features = false, features = [
"rustc-demangle", "rustc-demangle",

View file

@ -1,8 +1,20 @@
use std::{slice::ChunksExact, sync::OnceLock}; use std::{slice::ChunksExact, sync::OnceLock};
use thiserror::Error;
const ALPHABET: &[u8] = b"0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"; const ALPHABET: &[u8] = b"0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
pub fn gwilym_decode(input: &str) -> anyhow::Result<GwilymDecodeIter<'_>> { #[derive(Debug, Error)]
pub enum GwilymDecodeError {
#[error("Does not contain version")]
NoVersion,
#[error("Only version 1 is supported")]
WrongVersion,
#[error("Input must be a multiple of 3 but have {0}")]
LengthWrong(usize),
}
pub fn gwilym_decode(input: &str) -> Result<GwilymDecodeIter<'_>, GwilymDecodeError> {
GwilymDecodeIter::new(input) GwilymDecodeIter::new(input)
} }
@ -11,21 +23,21 @@ pub struct GwilymDecodeIter<'a> {
} }
impl<'a> GwilymDecodeIter<'a> { impl<'a> GwilymDecodeIter<'a> {
fn new(input: &'a str) -> anyhow::Result<Self> { fn new(input: &'a str) -> Result<Self, GwilymDecodeError> {
let input = input let input = input
.strip_prefix("https://agbrs.dev/crash#") .strip_prefix("https://agbrs.dev/crash#")
.unwrap_or(input); .unwrap_or(input);
let Some((input, version)) = input.rsplit_once('v') else { let Some((input, version)) = input.rsplit_once('v') else {
anyhow::bail!("Does not contain version"); return Err(GwilymDecodeError::NoVersion);
}; };
if version != "1" { if version != "1" {
anyhow::bail!("Only version 1 is supported"); return Err(GwilymDecodeError::WrongVersion);
} }
if input.len() % 3 != 0 { if input.len() % 3 != 0 {
anyhow::bail!("Input string must have length a multiple of 3"); return Err(GwilymDecodeError::LengthWrong(input.len()));
} }
Ok(Self { Ok(Self {
@ -75,11 +87,11 @@ fn get_value_for_char(input: u8) -> u32 {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{gwilym_decode, ALPHABET}; use super::*;
use std::fmt::Write; use std::fmt::Write;
#[test] #[test]
fn should_correctly_decode_16s() -> anyhow::Result<()> { fn should_correctly_decode_16s() -> Result<(), GwilymDecodeError> {
assert_eq!( assert_eq!(
&gwilym_decode("2QI65Q69306Kv1")?.collect::<Vec<_>>(), &gwilym_decode("2QI65Q69306Kv1")?.collect::<Vec<_>>(),
&[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195] &[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195]
@ -112,7 +124,7 @@ mod test {
} }
#[test] #[test]
fn should_correctly_decode_16s_and_32s() -> anyhow::Result<()> { fn should_correctly_decode_16s_and_32s() -> Result<(), Box<dyn std::error::Error>> {
let trace: &[u32] = &[ let trace: &[u32] = &[
0x0300_2990, 0x0300_2990,
0x0800_3289, 0x0800_3289,
@ -143,7 +155,7 @@ mod test {
} }
#[test] #[test]
fn should_strip_the_agbrsdev_prefix() -> anyhow::Result<()> { fn should_strip_the_agbrsdev_prefix() -> Result<(), Box<dyn std::error::Error>> {
assert_eq!( assert_eq!(
&gwilym_decode("https://agbrs.dev/crash#2QI65Q69306Kv1")?.collect::<Vec<_>>(), &gwilym_decode("https://agbrs.dev/crash#2QI65Q69306Kv1")?.collect::<Vec<_>>(),
&[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195] &[0x0800_16d3, 0x0800_315b, 0x0800_3243, 0x0800_0195]

93
agb-debug/src/lib.rs Normal file
View file

@ -0,0 +1,93 @@
mod gwilym_encoding;
mod load_dwarf;
use addr2line::gimli::{self, EndianReader};
pub use gwilym_encoding::{gwilym_decode, GwilymDecodeError};
pub use load_dwarf::{load_dwarf, GimliDwarf, LoadDwarfError};
use thiserror::Error;
pub use addr2line;
pub struct AddressInfo {
pub location: Location,
pub is_interesting: bool,
pub is_inline: bool,
pub function: String,
}
#[derive(Debug, Error)]
pub enum AddressInfoError {
#[error(transparent)]
Gimli(#[from] gimli::Error),
}
pub struct Location {
pub filename: String,
pub line: u32,
pub col: u32,
}
pub type Addr2LineContext = addr2line::Context<gimli::EndianRcSlice<gimli::RunTimeEndian>>;
impl Default for Location {
fn default() -> Self {
Self {
filename: "??".to_string(),
line: 0,
col: 0,
}
}
}
pub fn address_info(
ctx: &Addr2LineContext,
address: u64,
) -> Result<Vec<AddressInfo>, AddressInfoError> {
let mut frames = ctx.find_frames(address).skip_all_loads()?;
let mut is_first = true;
let mut infos = Vec::new();
while let Some(frame) = frames.next()? {
let function_name = if let Some(ref func) = frame.function {
func.demangle()?.into_owned()
} else {
"unknown function".to_string()
};
let location = frame
.location
.as_ref()
.map(|location| Location {
filename: location.file.unwrap_or("??").to_owned(),
line: location.line.unwrap_or(0),
col: location.column.unwrap_or(0),
})
.unwrap_or_default();
let is_interesting = is_interesting_function(&function_name, &location.filename);
infos.push(AddressInfo {
location,
is_interesting,
is_inline: is_first,
function: function_name,
});
is_first = false;
}
Ok(infos)
}
fn is_interesting_function(function_name: &str, path: &str) -> bool {
if function_name == "rust_begin_unwind" {
return false; // this is the unwind exception call
}
if path.ends_with("panicking.rs") {
return false; // probably part of rust's internal panic mechanisms
}
true
}

View file

@ -4,38 +4,52 @@ use addr2line::{
gimli, gimli,
object::{self, Object}, object::{self, Object},
}; };
use anyhow::bail; use thiserror::Error;
pub fn load_dwarf( #[derive(Debug, Error)]
file_content: &[u8], pub enum LoadDwarfError {
) -> anyhow::Result<gimli::Dwarf<gimli::EndianRcSlice<gimli::RunTimeEndian>>> { #[error("Gba file is empty")]
GbaFileEmpty,
#[error("Failed to load debug information from ROM file, it might not have been included?")]
NoDebugInformation,
#[error("Failed to load debug information: {0}")]
DeserializationError(#[from] rmp_serde::decode::Error),
#[error(transparent)]
GimliError(#[from] gimli::Error),
}
pub type GimliDwarf = gimli::Dwarf<gimli::EndianRcSlice<gimli::RunTimeEndian>>;
pub fn load_dwarf(file_content: &[u8]) -> Result<GimliDwarf, LoadDwarfError> {
if let Ok(object) = object::File::parse(file_content) { if let Ok(object) = object::File::parse(file_content) {
return load_from_object(&object); return Ok(load_from_object(&object)?);
} }
// the file might have been padded, so ensure we skip any padding before continuing // the file might have been padded, so ensure we skip any padding before continuing
let last_non_zero_byte = file_content let last_non_zero_byte = file_content
.iter() .iter()
.rposition(|&b| b != 0) .rposition(|&b| b != 0)
.ok_or_else(|| anyhow::anyhow!("Gba file is empty"))?; .ok_or_else(|| LoadDwarfError::GbaFileEmpty)?;
let file_content = &file_content[..last_non_zero_byte + 1]; let file_content = &file_content[..last_non_zero_byte + 1];
let last_8_bytes = &file_content[file_content.len() - 8..]; let last_8_bytes = &file_content[file_content.len() - 8..];
let len = u32::from_le_bytes(last_8_bytes[0..4].try_into()?) as usize; let len = u32::from_le_bytes(
last_8_bytes[0..4]
.try_into()
.or(Err(LoadDwarfError::NoDebugInformation))?,
) as usize;
let version = &last_8_bytes[4..]; let version = &last_8_bytes[4..];
if version != b"agb1" { if version != b"agb1" {
bail!("Failed to load debug information from ROM file, it might not have been included?"); return Err(LoadDwarfError::NoDebugInformation);
} }
let compressed_debug_data = &file_content[file_content.len() - len - 8..file_content.len() - 8]; let compressed_debug_data = &file_content[file_content.len() - len - 8..file_content.len() - 8];
let decompressing_reader = let decompressing_reader =
lz4_flex::frame::FrameDecoder::new(Cursor::new(compressed_debug_data)); lz4_flex::frame::FrameDecoder::new(Cursor::new(compressed_debug_data));
let debug_info: HashMap<String, Vec<u8>> = let debug_info: HashMap<String, Vec<u8>> = rmp_serde::decode::from_read(decompressing_reader)?;
rmp_serde::decode::from_read(decompressing_reader)
.map_err(|e| anyhow::anyhow!("Failed to load debug information: {e}"))?;
let dwarf = gimli::Dwarf::load(|id| { let dwarf = gimli::Dwarf::load(|id| {
let data = debug_info let data = debug_info
@ -54,7 +68,7 @@ pub fn load_dwarf(
fn load_from_object<'file>( fn load_from_object<'file>(
object: &object::File<'file, &'file [u8]>, object: &object::File<'file, &'file [u8]>,
) -> anyhow::Result<gimli::Dwarf<gimli::EndianRcSlice<gimli::RunTimeEndian>>> { ) -> Result<GimliDwarf, gimli::Error> {
let endian = if object.is_little_endian() { let endian = if object.is_little_endian() {
gimli::RunTimeEndian::Little gimli::RunTimeEndian::Little
} else { } else {

View file

@ -1,5 +1,6 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
error::Error,
fs::{self, File}, fs::{self, File},
io::Read, io::Read,
path::PathBuf, path::PathBuf,
@ -7,12 +8,9 @@ use std::{
}; };
use addr2line::gimli; use addr2line::gimli;
use agb_debug::Location;
use clap::Parser; use clap::Parser;
use colored::Colorize; use colored::Colorize;
use load_dwarf::load_dwarf;
mod gwilym_encoding;
mod load_dwarf;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@ -24,23 +22,7 @@ struct Args {
dump: String, dump: String,
} }
struct Location { fn main() -> Result<(), Box<dyn Error>> {
filename: String,
line: u32,
col: u32,
}
impl Default for Location {
fn default() -> Self {
Self {
filename: "??".to_string(),
line: 0,
col: 0,
}
}
}
fn main() -> anyhow::Result<()> {
let cli = Args::parse(); let cli = Args::parse();
let modification_time = fs::metadata(&cli.elf_path)? let modification_time = fs::metadata(&cli.elf_path)?
@ -48,11 +30,11 @@ fn main() -> anyhow::Result<()> {
.unwrap_or(SystemTime::UNIX_EPOCH); .unwrap_or(SystemTime::UNIX_EPOCH);
let file = fs::read(&cli.elf_path)?; let file = fs::read(&cli.elf_path)?;
let dwarf = load_dwarf(&file)?; let dwarf = agb_debug::load_dwarf(&file)?;
let ctx = addr2line::Context::from_dwarf(dwarf)?; let ctx = addr2line::Context::from_dwarf(dwarf)?;
for (i, address) in gwilym_encoding::gwilym_decode(&cli.dump)?.enumerate() { for (i, address) in agb_debug::gwilym_decode(&cli.dump)?.enumerate() {
print_address(&ctx, i, address.into(), modification_time)?; print_address(&ctx, i, address.into(), modification_time)?;
} }
@ -64,7 +46,7 @@ fn print_address(
index: usize, index: usize,
address: u64, address: u64,
elf_modification_time: SystemTime, elf_modification_time: SystemTime,
) -> anyhow::Result<()> { ) -> Result<(), Box<dyn Error>> {
let mut frames = ctx.find_frames(address).skip_all_loads()?; let mut frames = ctx.find_frames(address).skip_all_loads()?;
let mut is_first = true; let mut is_first = true;
@ -119,7 +101,7 @@ fn print_line_of_code(
frame: &addr2line::Frame<'_, impl gimli::Reader>, frame: &addr2line::Frame<'_, impl gimli::Reader>,
location: Location, location: Location,
elf_modification_time: SystemTime, elf_modification_time: SystemTime,
) -> anyhow::Result<()> { ) -> Result<(), Box<dyn Error>> {
let Some(filename) = frame.location.as_ref().and_then(|location| location.file) else { let Some(filename) = frame.location.as_ref().and_then(|location| location.file) else {
return Ok(()); return Ok(());
}; };