Compare commits

..

35 Commits

Author SHA1 Message Date
Luke Street 676488433f Fix resolving symbols for section-relative relocations
Also fixes MIPS `j` handling when jumping within the function.

Reworks `ObjReloc` struct to be a little more sensible.
2024-10-11 18:09:18 -06:00
Luke Street 83de98b5ee Version v2.3.1 2024-10-10 22:58:33 -06:00
Luke Street c1ba4e91d1 ci: Setup python venv for cargo-zigbuild 2024-10-10 22:39:31 -06:00
Luke Street 575900024d Avoid resetting diff state on unit config reload 2024-10-10 22:31:04 -06:00
Luke Street cbe299e859 Fix logic issue with 0-sized symbols
Fixes #119
2024-10-10 22:20:48 -06:00
Luke Street 741d93e211 Add symbol mapping feature (#118)
This allows users to "map" (or "link") symbols with different names so that they can be compared without having to update either the target or base objects. Symbol mappings are persisted in objdiff.json, so generators will need to ensure that they're preserved when updating. (Example: https://github.com/encounter/dtk-template/commit/d1334bb79e71af1a7b3b090bffda4adc483f722c)

Resolves #117
2024-10-09 21:44:18 -06:00
Luke Street 603dbd6882 Round match percent down before display
Ensures that 100% isn't displayed until it's a
perfect match.
2024-10-07 20:17:56 -06:00
Luke Street 6fb0a63de2 Click on empty space in row to clear highlight
Resolves #116
2024-10-07 19:53:16 -06:00
Luke Street ab2e84a2c6 Deprioritize generated GCC symbols in find_section_symbol
Resolves #115
2024-10-07 19:49:52 -06:00
Luke Street 9596051cb4 Allow collapsing sidebar in symbols view 2024-10-07 19:46:16 -06:00
Luke Street a5d9d8282e Update all dependencies 2024-10-03 22:00:43 -06:00
Amber Brault 3287a0f65c Bump cwextab again to 1.0.2 (#114)
* Bump cwextab

* Updated cwextab to not error on null actions

* Bump cwextab again
2024-10-03 01:12:37 -06:00
Amber Brault fab9c62dfb Bump cwextab (#113)
* Bump cwextab

* Updated cwextab to not error on null actions
2024-10-01 23:20:09 -06:00
Luke Street 08cd768260 Add total_units, complete_units to progress report 2024-09-30 21:41:57 -06:00
Luke Street 8acaaf528c Version v2.2.0 2024-09-29 12:26:41 -06:00
Luke Street 6e881a74e1 Remove armv7-unknown-linux-musleabi build 2024-09-29 11:57:13 -06:00
Luke Street cc1bc44e69 Use mimalloc when targeting musl 2024-09-29 11:52:04 -06:00
Luke Street c7b85518ab Rework jobs view & error handling improvements
Job status is now shown in the top menu bar,
with a new Jobs window that can be toggled.

Build and diff errors are now handled more
gracefully.

Fixes #40
2024-09-28 12:14:20 -06:00
Luke Street bb039a1445 Add "Open source file" option
Available when right-clicking an object in
the object list or when viewing an object

Resolves #99
2024-09-28 11:50:56 -06:00
Luke Street 8fc142d316 Debounce loaded object modification check
Before, this was running 2 fs::metadata
calls every frame. We don't need to do it
nearly that often, so now it only checks
once every 500ms.

This required refactoring AppConfig into
a separate AppState that holds transient
runtime state along with the loaded
AppConfig.
2024-09-28 10:55:22 -06:00
Luke Street b0123b3f83 Improve build log message when command doesn't exist
Before, it didn't include the actual command
that was attempted to run.
2024-09-28 10:55:09 -06:00
Luke Street 2ec17aee9b Improve config read/write performance
We were accidentally using unbuffered readers
and writers before, leading to long pauses on
the main thread on slow filesystems. (e.g.
FUSE or WSL)
2024-09-28 10:54:54 -06:00
Luke Street ec9731e1e5 Set app_id in eframe NativeOptions
Fixes missing WM_CLASS on Wayland
2024-09-28 10:53:58 -06:00
OndrikB a06382c27e Disambiguate dummy symbols (#107)
* Disambiguate dummy symbols

* Small formatting improvement

* Put HashMap logic into symbol creation
2024-09-27 00:33:36 -06:00
Luke Street e013638c5a clippy fixes 2024-09-27 00:30:30 -06:00
Luke Street 70ab82f1f7 gui: Highlight registers in columns separately
This matches the behavior of decomp.me and the
CLI.

Resolves #71
2024-09-27 00:27:36 -06:00
Luke Street c5896689cf Use ppc750cl Opcode::from 2024-09-27 00:12:21 -06:00
Luke Street 67719dd93e report: Exclude "hidden" functions
Fixes #111
2024-09-27 00:12:21 -06:00
Luke Street 258e141017 Upgrade all dependencies 2024-09-27 00:12:16 -06:00
Luke Street dbdda55065 Add Report::split
A hack for supporting games that build
all versions at once.
2024-09-26 23:47:03 -06:00
Steven Casper a43320af1f PPC: Guess reloc data type based on the instruction. (#108)
* Guess reloc data type based on the instruction.

Adds an entry to the reloc tooltip to show the inferred data type
and value.

* Fix clippy warning

* Match on Opcode rather than mnemonic string
2024-09-25 23:45:37 -06:00
Amber Brault 35bbd40f5d Actually update extab stuff (#110)
* Update cwextab

* Update

* Update ppc.rs

* Make fmt shut up
2024-09-24 09:16:14 -06:00
Amber Brault c1cb4b0b19 Update cwextab (#109) 2024-09-23 21:24:33 -06:00
Luke Street 2379853faa Remove unused imports 2024-09-10 23:29:22 -06:00
Luke Street 5e1aff180f Remove vergen / GIT_COMMIT_SHA handling 2024-09-10 23:22:40 -06:00
45 changed files with 4169 additions and 2179 deletions
+5 -6
View File
@@ -110,11 +110,6 @@ jobs:
name: linux-aarch64
build: zigbuild
features: default
- platform: ubuntu-latest
target: armv7-unknown-linux-musleabi
name: linux-armv7l
build: zigbuild
features: default
- platform: windows-latest
target: i686-pc-windows-msvc
name: windows-x86
@@ -147,7 +142,11 @@ jobs:
uses: actions/checkout@v4
- name: Install cargo-zigbuild
if: matrix.build == 'zigbuild'
run: pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
run: |
python3 -m venv .venv
. .venv/bin/activate
echo PATH=$PATH >> $GITHUB_ENV
pip install ziglang==0.13.0 cargo-zigbuild==0.19.1
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
Generated
+622 -727
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -13,7 +13,7 @@ strip = "debuginfo"
codegen-units = 1
[workspace.package]
version = "2.0.0"
version = "2.3.2"
authors = ["Luke Street <luke@street.dev>"]
edition = "2021"
license = "MIT OR Apache-2.0"
+7
View File
@@ -133,6 +133,13 @@
},
"metadata": {
"ref": "#/$defs/metadata"
},
"symbol_mappings": {
"type": "object",
"description": "Manual symbol mappings from target to base.",
"additionalProperties": {
"type": "string"
}
}
}
},
+1 -2
View File
@@ -70,7 +70,6 @@ feature-depth = 1
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
"RUSTSEC-2024-0370",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
@@ -240,7 +239,7 @@ allow-git = []
[sources.allow-org]
# github.com organizations to allow git sources for
github = ["encounter"]
github = ["notify-rs"]
# gitlab.com organizations to allow git sources for
gitlab = []
# bitbucket.org organizations to allow git sources for
+3 -1
View File
@@ -11,7 +11,6 @@ description = """
A local diffing tool for decompilation projects.
"""
publish = false
build = "build.rs"
[dependencies]
anyhow = "1.0"
@@ -29,3 +28,6 @@ supports-color = "3.0"
time = { version = "0.3", features = ["formatting", "local-offset"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1"
-9
View File
@@ -1,9 +0,0 @@
fn main() {
let output = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.expect("Failed to execute git");
let rev = String::from_utf8(output.stdout).expect("Failed to parse git output");
println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}");
println!("cargo:rustc-rerun-if-changed=.git/HEAD");
}
+1 -2
View File
@@ -31,10 +31,9 @@ where T: FromArgs
Ok(v) => {
if v.version {
println!(
"{} {} {}",
"{} {}",
command_name.first().unwrap_or(&""),
env!("CARGO_PKG_VERSION"),
env!("GIT_COMMIT_SHA"),
);
std::process::exit(0);
} else {
+33 -21
View File
@@ -102,26 +102,32 @@ pub fn run(args: Args) -> Result<()> {
let unit_path =
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
let Some(object) = project_config.objects.iter_mut().find_map(|obj| {
if obj.name.as_deref() == Some(u) {
let Some(object) = project_config
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.find_map(|obj| {
if obj.name.as_deref() == Some(u) {
resolve_paths(obj);
return Some(obj);
}
let up = unit_path.as_deref()?;
resolve_paths(obj);
return Some(obj);
}
let up = unit_path.as_deref()?;
if [&obj.base_path, &obj.target_path]
.into_iter()
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
.any(|p| p == up)
{
return Some(obj);
}
resolve_paths(obj);
if [&obj.base_path, &obj.target_path]
.into_iter()
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
.any(|p| p == up)
{
return Some(obj);
}
None
}) else {
None
})
else {
bail!("Unit not found: {}", u)
};
@@ -129,7 +135,13 @@ pub fn run(args: Args) -> Result<()> {
} else if let Some(symbol_name) = &args.symbol {
let mut idx = None;
let mut count = 0usize;
for (i, obj) in project_config.objects.iter_mut().enumerate() {
for (i, obj) in project_config
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.enumerate()
{
resolve_paths(obj);
if obj
@@ -148,7 +160,7 @@ pub fn run(args: Args) -> Result<()> {
}
match (count, idx) {
(0, None) => bail!("Symbol not found: {}", symbol_name),
(1, Some(i)) => &mut project_config.objects[i],
(1, Some(i)) => &mut project_config.units_mut()[i],
(2.., Some(_)) => bail!(
"Multiple instances of {} were found, try specifying a unit",
symbol_name
@@ -303,7 +315,7 @@ fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
None
}
#[allow(dead_code)]
#[expect(dead_code)]
struct FunctionDiffUi {
relax_reloc_diffs: bool,
left_highlight: HighlightKind,
@@ -758,7 +770,7 @@ impl FunctionDiffUi {
self.scroll_y += self.per_page / if half { 2 } else { 1 };
}
#[allow(clippy::too_many_arguments)]
#[expect(clippy::too_many_arguments)]
fn print_sym(
&self,
out: &mut Text<'static>,
+9 -6
View File
@@ -94,7 +94,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
};
info!(
"Generating report for {} units (using {} threads)",
project.objects.len(),
project.units().len(),
if args.deduplicate { 1 } else { rayon::current_num_threads() }
);
@@ -103,7 +103,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
let mut existing_functions: HashSet<String> = HashSet::new();
if args.deduplicate {
// If deduplicating, we need to run single-threaded
for object in &mut project.objects {
for object in project.units.as_deref_mut().unwrap_or_default() {
if let Some(unit) = report_object(
object,
project_dir,
@@ -116,7 +116,9 @@ fn generate(args: GenerateArgs) -> Result<()> {
}
} else {
let vec = project
.objects
.units
.as_deref_mut()
.unwrap_or_default()
.par_iter_mut()
.map(|object| {
report_object(
@@ -132,7 +134,7 @@ fn generate(args: GenerateArgs) -> Result<()> {
}
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
let mut categories = Vec::new();
for category in &project.progress_categories {
for category in project.progress_categories() {
categories.push(ReportCategory {
id: category.id.clone(),
name: category.name.clone(),
@@ -199,7 +201,7 @@ fn report_object(
.unwrap_or_default(),
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
};
let mut measures = Measures::default();
let mut measures = Measures { total_units: 1, ..Default::default() };
let mut sections = vec![];
let mut functions = vec![];
@@ -237,7 +239,7 @@ fn report_object(
}
for (symbol, symbol_diff) in section.symbols.iter().zip(&section_diff.symbols) {
if symbol.size == 0 {
if symbol.size == 0 || symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
continue;
}
if let Some(existing_functions) = &mut existing_functions {
@@ -280,6 +282,7 @@ fn report_object(
if metadata.complete.unwrap_or(false) {
measures.complete_code = measures.total_code;
measures.complete_data = measures.total_data;
measures.complete_units = 1;
}
measures.calc_fuzzy_match_percent();
measures.calc_matched_percent();
+6
View File
@@ -2,6 +2,12 @@ mod argp_version;
mod cmd;
mod util;
// musl's allocator is very slow, so use mimalloc when targeting musl.
// Otherwise, use the system allocator to avoid extra code size.
#[cfg(target_env = "musl")]
#[global_allocator]
static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
use std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr};
use anyhow::{Error, Result};
+7 -3
View File
@@ -17,8 +17,8 @@ crate-type = ["cdylib", "rlib"]
[features]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
any-arch = [] # Implicit, used to check if any arch is enabled
config = ["globset", "semver", "serde_json", "serde_yaml"]
any-arch = ["bimap"] # Implicit, used to check if any arch is enabled
config = ["bimap", "globset", "semver", "serde_json", "serde_yaml"]
dwarf = ["gimli"]
mips = ["any-arch", "rabbitizer"]
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
@@ -27,8 +27,12 @@ arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"]
bindings = ["serde_json", "prost", "pbjson"]
wasm = ["bindings", "console_error_panic_hook", "console_log"]
[package.metadata.docs.rs]
features = ["all"]
[dependencies]
anyhow = "1.0"
bimap = { version = "0.6", features = ["serde"], optional = true }
byteorder = "1.5"
filetime = "0.2"
flagset = "0.4"
@@ -57,7 +61,7 @@ gimli = { version = "0.31", default-features = false, features = ["read-all"], o
# ppc
cwdemangle = { version = "1.0", optional = true }
cwextab = { version = "0.2", optional = true }
cwextab = { version = "1.0.2", optional = true }
ppc750cl = { version = "0.3", optional = true }
# mips
Binary file not shown.
+4
View File
@@ -32,6 +32,10 @@ message Measures {
uint64 complete_data = 13;
// Completed (or "linked") data percent
float complete_data_percent = 14;
// Total number of units
uint32 total_units = 15;
// Completed (or "linked") units
uint32 complete_units = 16;
}
// Project progress report
+12 -5
View File
@@ -83,7 +83,7 @@ impl ObjArch for ObjArchMips {
&self,
address: u64,
code: &[u8],
_section_index: usize,
section_index: usize,
relocations: &[ObjReloc],
line_info: &BTreeMap<u64, u32>,
config: &DiffObjConfig,
@@ -140,11 +140,18 @@ impl ObjArch for ObjArchMips {
| OperandType::cpu_label
| OperandType::cpu_branch_target_label => {
if let Some(reloc) = reloc {
if matches!(&reloc.target_section, Some(s) if s == ".text")
&& reloc.target.address > start_address
&& reloc.target.address < end_address
// If the relocation target is within the current function, we can
// convert it into a relative branch target. Note that we check
// target_address > start_address instead of >= so that recursive
// tail calls are not considered branch targets.
let target_address =
reloc.target.address.checked_add_signed(reloc.addend);
if reloc.target.orig_section_index == Some(section_index)
&& matches!(target_address, Some(addr) if addr > start_address && addr < end_address)
{
args.push(ObjInsArg::BranchDest(reloc.target.address));
let target_address = target_address.unwrap();
args.push(ObjInsArg::BranchDest(target_address));
branch_dest = Some(target_address);
} else {
push_reloc(&mut args, reloc)?;
branch_dest = None;
+100 -1
View File
@@ -1,11 +1,13 @@
use std::{borrow::Cow, collections::BTreeMap};
use std::{borrow::Cow, collections::BTreeMap, ffi::CStr};
use anyhow::{bail, Result};
use byteorder::ByteOrder;
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
use crate::{
diff::DiffObjConfig,
obj::{ObjIns, ObjReloc, ObjSection},
util::ReallySigned,
};
#[cfg(feature = "arm")]
@@ -17,6 +19,97 @@ pub mod ppc;
#[cfg(feature = "x86")]
pub mod x86;
/// Represents the type of data associated with an instruction
pub enum DataType {
Int8,
Int16,
Int32,
Int64,
Int128,
Float,
Double,
Bytes,
String,
}
impl DataType {
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
if self.required_len().is_some_and(|l| bytes.len() < l) {
return None;
}
match self {
DataType::Int8 => {
let i = i8::from_ne_bytes(bytes.try_into().unwrap());
if i < 0 {
format!("Int8: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int8: {:#x}", i)
}
}
DataType::Int16 => {
let i = Endian::read_i16(bytes);
if i < 0 {
format!("Int16: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int16: {:#x}", i)
}
}
DataType::Int32 => {
let i = Endian::read_i32(bytes);
if i < 0 {
format!("Int32: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int32: {:#x}", i)
}
}
DataType::Int64 => {
let i = Endian::read_i64(bytes);
if i < 0 {
format!("Int64: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int64: {:#x}", i)
}
}
DataType::Int128 => {
let i = Endian::read_i128(bytes);
if i < 0 {
format!("Int128: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int128: {:#x}", i)
}
}
DataType::Float => {
format!("Float: {}", Endian::read_f32(bytes))
}
DataType::Double => {
format!("Double: {}", Endian::read_f64(bytes))
}
DataType::Bytes => {
format!("Bytes: {:#?}", bytes)
}
DataType::String => {
format!("String: {:?}", CStr::from_bytes_until_nul(bytes).ok()?)
}
}
.into()
}
fn required_len(&self) -> Option<usize> {
match self {
DataType::Int8 => Some(1),
DataType::Int16 => Some(2),
DataType::Int32 => Some(4),
DataType::Int64 => Some(8),
DataType::Int128 => Some(16),
DataType::Float => Some(4),
DataType::Double => Some(8),
DataType::Bytes => None,
DataType::String => None,
}
}
}
pub trait ObjArch: Send + Sync {
fn process_code(
&self,
@@ -42,6 +135,12 @@ pub trait ObjArch: Send + Sync {
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None }
fn display_data_type(&self, _ty: DataType, bytes: &[u8]) -> Option<String> {
Some(format!("Bytes: {:#x?}", bytes))
}
// Downcast methods
#[cfg(feature = "ppc")]
fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None }
+38 -5
View File
@@ -1,15 +1,16 @@
use std::{borrow::Cow, collections::BTreeMap};
use anyhow::{bail, ensure, Result};
use byteorder::BigEndian;
use cwextab::{decode_extab, ExceptionTableData};
use object::{
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget,
Symbol, SymbolKind,
};
use ppc750cl::{Argument, InsIter, GPR};
use ppc750cl::{Argument, InsIter, Opcode, GPR};
use crate::{
arch::{ObjArch, ProcessCodeResult},
arch::{DataType, ObjArch, ProcessCodeResult},
diff::DiffObjConfig,
obj::{ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSection, ObjSymbol},
};
@@ -186,6 +187,34 @@ impl ObjArch for ObjArchPpc {
}
}
fn guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> {
// Always shows the first string of the table. Not ideal, but it's really hard to find
// the actual string being referenced.
if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) {
return Some(DataType::String);
}
match Opcode::from(instruction.op as u8) {
Opcode::Lbz | Opcode::Lbzu | Opcode::Lbzux | Opcode::Lbzx => Some(DataType::Int8),
Opcode::Lhz | Opcode::Lhzu | Opcode::Lhzux | Opcode::Lhzx => Some(DataType::Int16),
Opcode::Lha | Opcode::Lhau | Opcode::Lhaux | Opcode::Lhax => Some(DataType::Int16),
Opcode::Lwz | Opcode::Lwzu | Opcode::Lwzux | Opcode::Lwzx => Some(DataType::Int32),
Opcode::Lfs | Opcode::Lfsu | Opcode::Lfsux | Opcode::Lfsx => Some(DataType::Float),
Opcode::Lfd | Opcode::Lfdu | Opcode::Lfdux | Opcode::Lfdx => Some(DataType::Double),
Opcode::Stb | Opcode::Stbu | Opcode::Stbux | Opcode::Stbx => Some(DataType::Int8),
Opcode::Sth | Opcode::Sthu | Opcode::Sthux | Opcode::Sthx => Some(DataType::Int16),
Opcode::Stw | Opcode::Stwu | Opcode::Stwux | Opcode::Stwx => Some(DataType::Int32),
Opcode::Stfs | Opcode::Stfsu | Opcode::Stfsux | Opcode::Stfsx => Some(DataType::Float),
Opcode::Stfd | Opcode::Stfdu | Opcode::Stfdux | Opcode::Stfdx => Some(DataType::Double),
_ => None,
}
}
fn display_data_type(&self, ty: DataType, bytes: &[u8]) -> Option<String> {
ty.display_bytes::<BigEndian>(bytes)
}
fn ppc(&self) -> Option<&ObjArchPpc> { Some(self) }
}
@@ -303,9 +332,13 @@ fn decode_exception_info(file: &File<'_>) -> Result<Option<BTreeMap<usize, Excep
continue;
};
let data = match decode_extab(extab_data) {
Some(decoded_data) => decoded_data,
None => {
log::warn!("Exception table decoding failed for function {}", extab_func_name);
Ok(decoded_data) => decoded_data,
Err(e) => {
log::warn!(
"Exception table decoding failed for function {}, reason: {}",
extab_func_name,
e.to_string()
);
return Ok(None);
}
};
+46 -45
View File
@@ -70,9 +70,13 @@ impl FunctionDiff {
// let (_section, symbol) = object.section_symbol(symbol_ref);
// Symbol::from(symbol)
// });
let instructions = symbol_diff.instructions.iter().map(InstructionDiff::from).collect();
let instructions = symbol_diff
.instructions
.iter()
.map(|ins_diff| InstructionDiff::new(object, ins_diff))
.collect();
Self {
symbol: Some(Symbol::from(symbol)),
symbol: Some(Symbol::new(symbol)),
// diff_symbol,
instructions,
match_percent: symbol_diff.match_percent,
@@ -90,8 +94,8 @@ impl DataDiff {
}
}
impl<'a> From<&'a ObjSymbol> for Symbol {
fn from(value: &'a ObjSymbol) -> Self {
impl Symbol {
pub fn new(value: &ObjSymbol) -> Self {
Self {
name: value.name.to_string(),
demangled_name: value.demangled_name.clone(),
@@ -122,29 +126,29 @@ fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
flags
}
impl<'a> From<&'a ObjIns> for Instruction {
fn from(value: &'a ObjIns) -> Self {
impl Instruction {
pub fn new(object: &ObjInfo, instruction: &ObjIns) -> Self {
Self {
address: value.address,
size: value.size as u32,
opcode: value.op as u32,
mnemonic: value.mnemonic.clone(),
formatted: value.formatted.clone(),
arguments: value.args.iter().map(Argument::from).collect(),
relocation: value.reloc.as_ref().map(Relocation::from),
branch_dest: value.branch_dest,
line_number: value.line,
original: value.orig.clone(),
address: instruction.address,
size: instruction.size as u32,
opcode: instruction.op as u32,
mnemonic: instruction.mnemonic.clone(),
formatted: instruction.formatted.clone(),
arguments: instruction.args.iter().map(Argument::new).collect(),
relocation: instruction.reloc.as_ref().map(|reloc| Relocation::new(object, reloc)),
branch_dest: instruction.branch_dest,
line_number: instruction.line,
original: instruction.orig.clone(),
}
}
}
impl<'a> From<&'a ObjInsArg> for Argument {
fn from(value: &'a ObjInsArg) -> Self {
impl Argument {
pub fn new(value: &ObjInsArg) -> Self {
Self {
value: Some(match value {
ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()),
ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::from(v)),
ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::new(v)),
ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}),
ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest),
}),
@@ -152,8 +156,8 @@ impl<'a> From<&'a ObjInsArg> for Argument {
}
}
impl From<&ObjInsArgValue> for ArgumentValue {
fn from(value: &ObjInsArgValue) -> Self {
impl ArgumentValue {
pub fn new(value: &ObjInsArgValue) -> Self {
Self {
value: Some(match value {
ObjInsArgValue::Signed(v) => argument_value::Value::Signed(*v),
@@ -164,42 +168,39 @@ impl From<&ObjInsArgValue> for ArgumentValue {
}
}
impl<'a> From<&'a ObjReloc> for Relocation {
fn from(value: &ObjReloc) -> Self {
impl Relocation {
pub fn new(object: &ObjInfo, reloc: &ObjReloc) -> Self {
Self {
r#type: match value.flags {
r#type: match reloc.flags {
object::RelocationFlags::Elf { r_type } => r_type,
object::RelocationFlags::MachO { r_type, .. } => r_type as u32,
object::RelocationFlags::Coff { typ } => typ as u32,
object::RelocationFlags::Xcoff { r_rtype, .. } => r_rtype as u32,
_ => unreachable!(),
},
type_name: String::new(), // TODO
target: Some(RelocationTarget::from(&value.target)),
type_name: object.arch.display_reloc(reloc.flags).into_owned(),
target: Some(RelocationTarget {
symbol: Some(Symbol::new(&reloc.target)),
addend: reloc.addend,
}),
}
}
}
impl<'a> From<&'a ObjSymbol> for RelocationTarget {
fn from(value: &'a ObjSymbol) -> Self {
Self { symbol: Some(Symbol::from(value)), addend: value.addend }
}
}
impl<'a> From<&'a ObjInsDiff> for InstructionDiff {
fn from(value: &'a ObjInsDiff) -> Self {
impl InstructionDiff {
pub fn new(object: &ObjInfo, instruction_diff: &ObjInsDiff) -> Self {
Self {
instruction: value.ins.as_ref().map(Instruction::from),
diff_kind: DiffKind::from(value.kind) as i32,
branch_from: value.branch_from.as_ref().map(InstructionBranchFrom::from),
branch_to: value.branch_to.as_ref().map(InstructionBranchTo::from),
arg_diff: value.arg_diff.iter().map(ArgumentDiff::from).collect(),
instruction: instruction_diff.ins.as_ref().map(|ins| Instruction::new(object, ins)),
diff_kind: DiffKind::from(instruction_diff.kind) as i32,
branch_from: instruction_diff.branch_from.as_ref().map(InstructionBranchFrom::new),
branch_to: instruction_diff.branch_to.as_ref().map(InstructionBranchTo::new),
arg_diff: instruction_diff.arg_diff.iter().map(ArgumentDiff::new).collect(),
}
}
}
impl From<&Option<ObjInsArgDiff>> for ArgumentDiff {
fn from(value: &Option<ObjInsArgDiff>) -> Self {
impl ArgumentDiff {
pub fn new(value: &Option<ObjInsArgDiff>) -> Self {
Self { diff_index: value.as_ref().map(|v| v.idx as u32) }
}
}
@@ -228,8 +229,8 @@ impl From<ObjDataDiffKind> for DiffKind {
}
}
impl<'a> From<&'a ObjInsBranchFrom> for InstructionBranchFrom {
fn from(value: &'a ObjInsBranchFrom) -> Self {
impl InstructionBranchFrom {
pub fn new(value: &ObjInsBranchFrom) -> Self {
Self {
instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(),
branch_index: value.branch_idx as u32,
@@ -237,8 +238,8 @@ impl<'a> From<&'a ObjInsBranchFrom> for InstructionBranchFrom {
}
}
impl<'a> From<&'a ObjInsBranchTo> for InstructionBranchTo {
fn from(value: &'a ObjInsBranchTo) -> Self {
impl InstructionBranchTo {
pub fn new(value: &ObjInsBranchTo) -> Self {
Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 }
}
}
+119 -9
View File
@@ -8,9 +8,10 @@ use serde_json::error::Category;
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
pub const REPORT_VERSION: u32 = 1;
pub const REPORT_VERSION: u32 = 2;
impl Report {
/// Attempts to parse the report as binary protobuf or JSON.
pub fn parse(data: &[u8]) -> Result<Self> {
if data.is_empty() {
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
@@ -25,6 +26,7 @@ impl Report {
Ok(report)
}
/// Attempts to parse the report as JSON, migrating from the legacy report format if necessary.
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
match serde_json::from_slice::<Self>(bytes) {
Ok(report) => Ok(report),
@@ -43,16 +45,23 @@ impl Report {
}
}
/// Migrates the report to the latest version.
/// Fails if the report version is newer than supported.
pub fn migrate(&mut self) -> Result<()> {
if self.version == 0 {
self.migrate_v0()?;
}
if self.version == 1 {
self.migrate_v1()?;
}
if self.version != REPORT_VERSION {
bail!("Unsupported report version: {}", self.version);
}
Ok(())
}
/// Adds `complete_code`, `complete_data`, `complete_code_percent`, and `complete_data_percent`
/// to measures, and sets `progress_categories` in unit metadata.
fn migrate_v0(&mut self) -> Result<()> {
let Some(measures) = &mut self.measures else {
bail!("Missing measures in report");
@@ -61,15 +70,16 @@ impl Report {
let Some(unit_measures) = &mut unit.measures else {
bail!("Missing measures in report unit");
};
let Some(metadata) = &mut unit.metadata else {
bail!("Missing metadata in report unit");
let mut complete = false;
if let Some(metadata) = &mut unit.metadata {
if metadata.module_name.is_some() || metadata.module_id.is_some() {
metadata.progress_categories = vec!["modules".to_string()];
} else {
metadata.progress_categories = vec!["dol".to_string()];
}
complete = metadata.complete.unwrap_or(false);
};
if metadata.module_name.is_some() || metadata.module_id.is_some() {
metadata.progress_categories = vec!["modules".to_string()];
} else {
metadata.progress_categories = vec!["dol".to_string()];
}
if metadata.complete.unwrap_or(false) {
if complete {
unit_measures.complete_code = unit_measures.total_code;
unit_measures.complete_data = unit_measures.total_data;
unit_measures.complete_code_percent = 100.0;
@@ -84,10 +94,42 @@ impl Report {
measures.complete_data += unit_measures.complete_data;
}
measures.calc_matched_percent();
self.calculate_progress_categories();
self.version = 1;
Ok(())
}
/// Adds `total_units` and `complete_units` to measures.
fn migrate_v1(&mut self) -> Result<()> {
let Some(total_measures) = &mut self.measures else {
bail!("Missing measures in report");
};
for unit in &mut self.units {
let Some(measures) = &mut unit.measures else {
bail!("Missing measures in report unit");
};
let complete = unit.metadata.as_ref().and_then(|m| m.complete).unwrap_or(false) as u32;
let progress_categories =
unit.metadata.as_ref().map(|m| m.progress_categories.as_slice()).unwrap_or(&[]);
measures.total_units = 1;
measures.complete_units = complete;
total_measures.total_units += 1;
total_measures.complete_units += complete;
for id in progress_categories {
if let Some(category) = self.categories.iter_mut().find(|c| &c.id == id) {
let Some(measures) = &mut category.measures else {
bail!("Missing measures in category");
};
measures.total_units += 1;
measures.complete_units += complete;
}
}
}
self.version = 2;
Ok(())
}
/// Calculate progress categories based on unit metadata.
pub fn calculate_progress_categories(&mut self) {
for unit in &self.units {
let Some(metadata) = unit.metadata.as_ref() else {
@@ -117,6 +159,72 @@ impl Report {
measures.calc_matched_percent();
}
}
/// Split the report into multiple reports based on progress categories.
/// Assumes progress categories are in the format `version`, `version.category`.
/// This is a hack for projects that generate all versions in a single report.
pub fn split(self) -> Vec<(String, Report)> {
let mut reports = Vec::new();
// Map units to Option to allow taking ownership
let mut units = self.units.into_iter().map(Some).collect::<Vec<_>>();
for category in &self.categories {
if category.id.contains(".") {
// Skip subcategories
continue;
}
fn is_sub_category(id: &str, parent: &str, sep: char) -> bool {
id.starts_with(parent)
&& id.get(parent.len()..).map_or(false, |s| s.starts_with(sep))
}
let mut sub_categories = self
.categories
.iter()
.filter(|c| is_sub_category(&c.id, &category.id, '.'))
.cloned()
.collect::<Vec<_>>();
// Remove category prefix
for sub_category in &mut sub_categories {
sub_category.id = sub_category.id[category.id.len() + 1..].to_string();
}
let mut sub_units = units
.iter_mut()
.filter_map(|opt| {
let unit = opt.as_mut()?;
let metadata = unit.metadata.as_ref()?;
if metadata.progress_categories.contains(&category.id) {
opt.take()
} else {
None
}
})
.collect::<Vec<_>>();
for sub_unit in &mut sub_units {
// Remove leading version/ from unit name
if let Some(name) =
sub_unit.name.strip_prefix(&category.id).and_then(|s| s.strip_prefix('/'))
{
sub_unit.name = name.to_string();
}
// Filter progress categories
let Some(metadata) = sub_unit.metadata.as_mut() else {
continue;
};
metadata.progress_categories = metadata
.progress_categories
.iter()
.filter(|c| is_sub_category(c, &category.id, '.'))
.map(|c| c[category.id.len() + 1..].to_string())
.collect();
}
reports.push((category.id.clone(), Report {
measures: category.measures,
units: sub_units,
version: self.version,
categories: sub_categories,
}));
}
reports
}
}
impl Measures {
@@ -176,6 +284,8 @@ impl AddAssign for Measures {
self.matched_functions += other.matched_functions;
self.complete_code += other.complete_code;
self.complete_data += other.complete_data;
self.total_units += other.total_units;
self.complete_units += other.complete_units;
}
}
+105 -48
View File
@@ -1,77 +1,100 @@
use std::{
fs,
fs::File,
io::Read,
io::{BufReader, BufWriter, Read},
path::{Path, PathBuf},
};
use anyhow::{anyhow, Context, Result};
use bimap::BiBTreeMap;
use filetime::FileTime;
use globset::{Glob, GlobSet, GlobSetBuilder};
#[inline]
fn bool_true() -> bool { true }
#[derive(Default, Clone, serde::Deserialize)]
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectConfig {
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min_version: Option<String>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_make: Option<String>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub custom_args: Option<Vec<String>>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_dir: Option<PathBuf>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_dir: Option<PathBuf>,
#[serde(default = "bool_true")]
pub build_base: bool,
#[serde(default)]
pub build_target: bool,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_base: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_target: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub watch_patterns: Option<Vec<Glob>>,
#[serde(default, alias = "units")]
pub objects: Vec<ProjectObject>,
#[serde(default)]
pub progress_categories: Vec<ProjectProgressCategory>,
#[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
pub units: Option<Vec<ProjectObject>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_categories: Option<Vec<ProjectProgressCategory>>,
}
#[derive(Default, Clone, serde::Deserialize)]
impl ProjectConfig {
#[inline]
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
#[inline]
pub fn units_mut(&mut self) -> &mut Vec<ProjectObject> {
self.units.get_or_insert_with(Vec::new)
}
#[inline]
pub fn progress_categories(&self) -> &[ProjectProgressCategory] {
self.progress_categories.as_deref().unwrap_or_default()
}
#[inline]
pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> {
self.progress_categories.get_or_insert_with(Vec::new)
}
}
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectObject {
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_path: Option<PathBuf>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_path: Option<PathBuf>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use metadata.reverse_fn_order")]
pub reverse_fn_order: Option<bool>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
#[deprecated(note = "Use metadata.complete")]
pub complete: Option<bool>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub scratch: Option<ScratchConfig>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<ProjectObjectMetadata>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub symbol_mappings: Option<SymbolMappings>,
}
#[derive(Default, Clone, serde::Deserialize)]
pub type SymbolMappings = BiBTreeMap<String, String>;
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectObjectMetadata {
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub complete: Option<bool>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reverse_fn_order: Option<bool>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_path: Option<String>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub progress_categories: Option<Vec<String>>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auto_generated: Option<bool>,
}
#[derive(Default, Clone, serde::Deserialize)]
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct ProjectProgressCategory {
#[serde(default)]
pub id: String,
@@ -112,32 +135,36 @@ impl ProjectObject {
}
pub fn complete(&self) -> Option<bool> {
#[allow(deprecated)]
#[expect(deprecated)]
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
}
pub fn reverse_fn_order(&self) -> Option<bool> {
#[allow(deprecated)]
#[expect(deprecated)]
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
}
pub fn hidden(&self) -> bool {
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
}
pub fn source_path(&self) -> Option<&String> {
self.metadata.as_ref().and_then(|m| m.source_path.as_ref())
}
}
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ScratchConfig {
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compiler: Option<String>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub c_flags: Option<String>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ctx_path: Option<PathBuf>,
#[serde(default)]
pub build_ctx: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_ctx: Option<bool>,
}
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];
@@ -150,13 +177,13 @@ pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
#[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo {
pub path: PathBuf,
pub timestamp: FileTime,
pub timestamp: Option<FileTime>,
}
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
for filename in CONFIG_FILENAMES.iter() {
let config_path = dir.join(filename);
let Ok(mut file) = File::open(&config_path) else {
let Ok(file) = File::open(&config_path) else {
continue;
};
let metadata = file.metadata();
@@ -165,9 +192,10 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
continue;
}
let ts = FileTime::from_last_modification_time(&metadata);
let mut reader = BufReader::new(file);
let mut result = match filename.contains("json") {
true => read_json_config(&mut file),
false => read_yml_config(&mut file),
true => read_json_config(&mut reader),
false => read_yml_config(&mut reader),
};
if let Ok(config) = &result {
// Validate min_version if present
@@ -175,12 +203,41 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
result = Err(e);
}
}
return Some((result, ProjectConfigInfo { path: config_path, timestamp: ts }));
return Some((result, ProjectConfigInfo { path: config_path, timestamp: Some(ts) }));
}
}
None
}
pub fn save_project_config(
config: &ProjectConfig,
info: &ProjectConfigInfo,
) -> Result<ProjectConfigInfo> {
if let Some(last_ts) = info.timestamp {
// Check if the file has changed since we last read it
if let Ok(metadata) = fs::metadata(&info.path) {
let ts = FileTime::from_last_modification_time(&metadata);
if ts != last_ts {
return Err(anyhow!("Config file has changed since last read"));
}
}
}
let mut writer =
BufWriter::new(File::create(&info.path).context("Failed to create config file")?);
let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json");
match ext {
"json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"),
"yml" | "yaml" => {
serde_yaml::to_writer(&mut writer, config).context("Failed to write YAML")
}
_ => Err(anyhow!("Unknown config file extension: {ext}")),
}?;
let file = writer.into_inner().context("Failed to flush file")?;
let metadata = file.metadata().context("Failed to get file metadata")?;
let ts = FileTime::from_last_modification_time(&metadata);
Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) })
}
fn validate_min_version(config: &ProjectConfig) -> Result<()> {
let Some(min_version) = &config.min_version else { return Ok(()) };
let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))

Some files were not shown because too many files have changed in this diff Show More