mirror of
https://github.com/encounter/objdiff.git
synced 2025-10-05 09:29:51 +00:00
Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
572afa8551 | |||
90e81fad7e | |||
7a8efb4c88 | |||
56dac46280 | |||
1866158092 | |||
|
fe8e7029f6 | ||
|
e2c70342c9 | ||
|
e6035b00df |
44
AGENTS.md
Normal file
44
AGENTS.md
Normal file
@ -0,0 +1,44 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
- `objdiff-core`: core library with diffing logic and shared components
|
||||
- `objdiff-cli`: CLI/TUI with `ratatui`
|
||||
- `objdiff-gui`: GUI with `eframe`/`egui`
|
||||
- `objdiff-wasm`: WebAssembly bindings
|
||||
|
||||
objdiff has three main frontends: GUI, CLI/TUI, and [web](https://github.com/encounter/objdiff-web) (utilizing `objdiff-wasm`).
|
||||
|
||||
`objdiff-gui` is the most fully-featured and is the primary target for new features.
|
||||
|
||||
`objdiff-cli` has an interactive TUI `diff` mode (powered by `ratatui`) and a non-interactive `report` mode to generate detailed progress reports (e.g. for <https://decomp.dev>).
|
||||
|
||||
`objdiff-wasm` is compiled into a [WebAssembly Component](https://component-model.bytecodealliance.org/). The [API](objdiff-wasm/wit/objdiff.wit) is defined using [WIT](https://component-model.bytecodealliance.org/design/wit.html). The web interface is separate, but mirrors the GUI in functionality.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
Run `cargo build` for a debug build and `cargo build --release` for an optimized build.
|
||||
The CLI can be exercised with `cargo run --release --bin objdiff-cli -- --help`.
|
||||
|
||||
The WASM build is not included in the workspace by default due to differences in toolchain and target; build it with `npm -C objdiff-wasm run build`. (nightly required)
|
||||
In general, do NOT use `--workspace`, as it will include the WASM crate and likely fail. Formatting and linting commands are exempt.
|
||||
|
||||
Pre-commit tasks:
|
||||
|
||||
- `cargo test` to run the test suite
|
||||
- `cargo +nightly fmt --all` to format code (nightly required)
|
||||
- `cargo +nightly clippy --all-targets --all-features --workspace -- -D warnings` to lint code (nightly required)
|
||||
- `cargo deny check` (`cargo install --locked cargo-deny` if needed) if dependencies change
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
Favor focused unit tests adjacent to the code, and integration scenarios in `objdiff-core/tests`.
|
||||
Integration tests utilize snapshots with the `insta` crate; run `curl -LsSf https://insta.rs/install.sh | sh` if needed.
|
||||
To generate updated snapshots, use `cargo insta test`, review diffs manually, then accept changes with `cargo insta accept` (or simply `mv` to accept specific snapshots).
|
||||
|
||||
## Configuration Tips
|
||||
|
||||
- `config.schema.json`: JSON schema for the user-facing project config file (`objdiff.json`)
|
||||
- `objdiff-core/config-schema.json`: Internal options schema to generate `DiffObjConfig` (see `objdiff-core/config_gen.rs`)
|
||||
|
||||
The project configuration (`objdiff.json`) is intended to be generated by the projects' build system. It includes paths to the target (expected) and base (current) object files, along with build commands and file watch patterns. See `README.md` for a summary of key options.
|
725
Cargo.lock
generated
725
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -14,7 +14,7 @@ default-members = [
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "3.2.1"
|
||||
version = "3.3.0"
|
||||
authors = ["Luke Street <luke@street.dev>"]
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
@ -111,6 +111,25 @@
|
||||
"items": {
|
||||
"$ref": "#/$defs/progress_category"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"description": "Diff configuration options that should be applied automatically when the project is loaded.",
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"demangler": "gnu_legacy"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
@ -156,6 +175,20 @@
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"type": "object",
|
||||
"description": "Diff configuration options that should be applied when this unit is active.",
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -24,7 +24,8 @@ use objdiff_core::{
|
||||
watcher::{Watcher, create_watcher},
|
||||
},
|
||||
config::{
|
||||
ProjectConfig, ProjectObject, ProjectObjectMetadata, build_globset,
|
||||
ProjectConfig, ProjectObject, ProjectObjectMetadata, ProjectOptions, apply_project_options,
|
||||
build_globset,
|
||||
path::{check_path_buf, platform_path, platform_path_serde_option},
|
||||
},
|
||||
diff::{DiffObjConfig, MappingConfig, ObjectDiff},
|
||||
@ -77,11 +78,11 @@ pub struct Args {
|
||||
}
|
||||
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
let (target_path, base_path, project_config) =
|
||||
let (target_path, base_path, project_config, unit_options) =
|
||||
match (&args.target, &args.base, &args.project, &args.unit) {
|
||||
(Some(_), Some(_), None, None)
|
||||
| (Some(_), None, None, None)
|
||||
| (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None),
|
||||
| (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None, None),
|
||||
(None, None, p, u) => {
|
||||
let project = match p {
|
||||
Some(project) => project.clone(),
|
||||
@ -106,28 +107,32 @@ pub fn run(args: Args) -> Result<()> {
|
||||
.base_dir
|
||||
.as_ref()
|
||||
.map(|p| project.join(p.with_platform_encoding()));
|
||||
let objects = project_config
|
||||
.units
|
||||
let units = project_config.units.as_deref().unwrap_or_default();
|
||||
let objects = units
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|o| {
|
||||
ObjectConfig::new(
|
||||
o,
|
||||
&project,
|
||||
target_obj_dir.as_deref(),
|
||||
base_obj_dir.as_deref(),
|
||||
.enumerate()
|
||||
.map(|(idx, o)| {
|
||||
(
|
||||
ObjectConfig::new(
|
||||
o,
|
||||
&project,
|
||||
target_obj_dir.as_deref(),
|
||||
base_obj_dir.as_deref(),
|
||||
),
|
||||
idx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let object = if let Some(u) = u {
|
||||
let (object, unit_idx) = if let Some(u) = u {
|
||||
objects
|
||||
.iter()
|
||||
.find(|obj| obj.name == *u)
|
||||
.find(|(obj, _)| obj.name == *u)
|
||||
.map(|(obj, idx)| (obj, *idx))
|
||||
.ok_or_else(|| anyhow!("Unit not found: {}", u))?
|
||||
} else if let Some(symbol_name) = &args.symbol {
|
||||
let mut idx = None;
|
||||
let mut count = 0usize;
|
||||
for (i, obj) in objects.iter().enumerate() {
|
||||
for (i, (obj, unit_idx)) in objects.iter().enumerate() {
|
||||
if obj
|
||||
.target_path
|
||||
.as_deref()
|
||||
@ -135,7 +140,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||
.transpose()?
|
||||
.unwrap_or(false)
|
||||
{
|
||||
idx = Some(i);
|
||||
idx = Some((i, *unit_idx));
|
||||
count += 1;
|
||||
if count > 1 {
|
||||
break;
|
||||
@ -144,7 +149,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||
}
|
||||
match (count, idx) {
|
||||
(0, None) => bail!("Symbol not found: {}", symbol_name),
|
||||
(1, Some(i)) => &objects[i],
|
||||
(1, Some((i, unit_idx))) => (&objects[i].0, unit_idx),
|
||||
(2.., Some(_)) => bail!(
|
||||
"Multiple instances of {} were found, try specifying a unit",
|
||||
symbol_name
|
||||
@ -154,18 +159,29 @@ pub fn run(args: Args) -> Result<()> {
|
||||
} else {
|
||||
bail!("Must specify one of: symbol, project and unit, target and base objects")
|
||||
};
|
||||
let unit_options = units.get(unit_idx).and_then(|u| u.options().cloned());
|
||||
let target_path = object.target_path.clone();
|
||||
let base_path = object.base_path.clone();
|
||||
(target_path, base_path, Some(project_config))
|
||||
(target_path, base_path, Some(project_config), unit_options)
|
||||
}
|
||||
_ => bail!("Either target and base or project and unit must be specified"),
|
||||
};
|
||||
|
||||
run_interactive(args, target_path, base_path, project_config)
|
||||
run_interactive(args, target_path, base_path, project_config, unit_options)
|
||||
}
|
||||
|
||||
fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)> {
|
||||
fn build_config_from_args(
|
||||
args: &Args,
|
||||
project_config: Option<&ProjectConfig>,
|
||||
unit_options: Option<&ProjectOptions>,
|
||||
) -> Result<(DiffObjConfig, MappingConfig)> {
|
||||
let mut diff_config = DiffObjConfig::default();
|
||||
if let Some(options) = project_config.and_then(|config| config.options.as_ref()) {
|
||||
apply_project_options(&mut diff_config, options)?;
|
||||
}
|
||||
if let Some(options) = unit_options {
|
||||
apply_project_options(&mut diff_config, options)?;
|
||||
}
|
||||
apply_config_args(&mut diff_config, &args.config)?;
|
||||
let mut mapping_config = MappingConfig {
|
||||
mappings: Default::default(),
|
||||
@ -316,11 +332,13 @@ fn run_interactive(
|
||||
target_path: Option<Utf8PlatformPathBuf>,
|
||||
base_path: Option<Utf8PlatformPathBuf>,
|
||||
project_config: Option<ProjectConfig>,
|
||||
unit_options: Option<ProjectOptions>,
|
||||
) -> Result<()> {
|
||||
let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") };
|
||||
let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]")
|
||||
.context("Failed to parse time format")?;
|
||||
let (diff_obj_config, mapping_config) = build_config_from_args(&args)?;
|
||||
let (diff_obj_config, mapping_config) =
|
||||
build_config_from_args(&args, project_config.as_ref(), unit_options.as_ref())?;
|
||||
let mut state = AppState {
|
||||
jobs: Default::default(),
|
||||
waker: Default::default(),
|
||||
|
@ -7,7 +7,7 @@ use objdiff_core::{
|
||||
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, REPORT_VERSION,
|
||||
Report, ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
|
||||
},
|
||||
config::path::platform_path,
|
||||
config::{ProjectObject, ProjectOptions, apply_project_options, path::platform_path},
|
||||
diff,
|
||||
obj::{self, SectionKind, SymbolFlag, SymbolKind},
|
||||
};
|
||||
@ -83,14 +83,13 @@ pub fn run(args: Args) -> Result<()> {
|
||||
}
|
||||
|
||||
fn generate(args: GenerateArgs) -> Result<()> {
|
||||
let mut diff_config = diff::DiffObjConfig {
|
||||
let base_diff_config = diff::DiffObjConfig {
|
||||
function_reloc_diffs: diff::FunctionRelocDiffs::None,
|
||||
combine_data_sections: true,
|
||||
combine_text_sections: true,
|
||||
ppc_calculate_pool_relocations: false,
|
||||
..Default::default()
|
||||
};
|
||||
apply_config_args(&mut diff_config, &args.config)?;
|
||||
|
||||
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||
let project_dir = args.project.as_deref().unwrap_or_else(|| Utf8PlatformPath::new("."));
|
||||
@ -101,31 +100,44 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err),
|
||||
None => bail!("No project configuration found"),
|
||||
};
|
||||
info!(
|
||||
"Generating report for {} units (using {} threads)",
|
||||
project.units().len(),
|
||||
if args.deduplicate { 1 } else { rayon::current_num_threads() }
|
||||
);
|
||||
|
||||
let target_obj_dir =
|
||||
project.target_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
|
||||
let base_obj_dir =
|
||||
project.base_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
|
||||
let objects = project
|
||||
.units
|
||||
let project_units = project.units.as_deref().unwrap_or_default();
|
||||
let objects = project_units
|
||||
.iter()
|
||||
.flatten()
|
||||
.map(|o| {
|
||||
ObjectConfig::new(o, project_dir, target_obj_dir.as_deref(), base_obj_dir.as_deref())
|
||||
.enumerate()
|
||||
.map(|(idx, o)| {
|
||||
(
|
||||
ObjectConfig::new(
|
||||
o,
|
||||
project_dir,
|
||||
target_obj_dir.as_deref(),
|
||||
base_obj_dir.as_deref(),
|
||||
),
|
||||
idx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
info!(
|
||||
"Generating report for {} units (using {} threads)",
|
||||
objects.len(),
|
||||
if args.deduplicate { 1 } else { rayon::current_num_threads() }
|
||||
);
|
||||
|
||||
let start = Instant::now();
|
||||
let mut units = vec![];
|
||||
let mut existing_functions: HashSet<String> = HashSet::new();
|
||||
if args.deduplicate {
|
||||
// If deduplicating, we need to run single-threaded
|
||||
for object in &objects {
|
||||
for (object, unit_idx) in &objects {
|
||||
let diff_config = build_unit_diff_config(
|
||||
&base_diff_config,
|
||||
project.options.as_ref(),
|
||||
project_units.get(*unit_idx).and_then(ProjectObject::options),
|
||||
&args.config,
|
||||
)?;
|
||||
if let Some(unit) = report_object(object, &diff_config, Some(&mut existing_functions))?
|
||||
{
|
||||
units.push(unit);
|
||||
@ -134,7 +146,15 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
} else {
|
||||
let vec = objects
|
||||
.par_iter()
|
||||
.map(|object| report_object(object, &diff_config, None))
|
||||
.map(|(object, unit_idx)| {
|
||||
let diff_config = build_unit_diff_config(
|
||||
&base_diff_config,
|
||||
project.options.as_ref(),
|
||||
project_units.get(*unit_idx).and_then(ProjectObject::options),
|
||||
&args.config,
|
||||
)?;
|
||||
report_object(object, &diff_config, None)
|
||||
})
|
||||
.collect::<Result<Vec<Option<ReportUnit>>>>()?;
|
||||
units = vec.into_iter().flatten().collect();
|
||||
}
|
||||
@ -156,6 +176,24 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_unit_diff_config(
|
||||
base: &diff::DiffObjConfig,
|
||||
project_options: Option<&ProjectOptions>,
|
||||
unit_options: Option<&ProjectOptions>,
|
||||
cli_args: &[String],
|
||||
) -> Result<diff::DiffObjConfig> {
|
||||
let mut diff_config = base.clone();
|
||||
if let Some(options) = project_options {
|
||||
apply_project_options(&mut diff_config, options)?;
|
||||
}
|
||||
if let Some(options) = unit_options {
|
||||
apply_project_options(&mut diff_config, options)?;
|
||||
}
|
||||
// CLI args override project and unit options
|
||||
apply_config_args(&mut diff_config, cli_args)?;
|
||||
Ok(diff_config)
|
||||
}
|
||||
|
||||
fn report_object(
|
||||
object: &ObjectConfig,
|
||||
diff_config: &diff::DiffObjConfig,
|
||||
|
@ -41,7 +41,8 @@ any-arch = [
|
||||
"dep:regex",
|
||||
"dep:similar",
|
||||
"dep:syn",
|
||||
"dep:encoding_rs"
|
||||
"dep:encoding_rs",
|
||||
"demangler",
|
||||
]
|
||||
bindings = [
|
||||
"dep:prost",
|
||||
@ -88,38 +89,37 @@ std = [
|
||||
]
|
||||
mips = [
|
||||
"any-arch",
|
||||
"dep:cpp_demangle",
|
||||
"dep:cwdemangle",
|
||||
"dep:rabbitizer",
|
||||
]
|
||||
ppc = [
|
||||
"any-arch",
|
||||
"dep:cwdemangle",
|
||||
"dep:cwextab",
|
||||
"dep:powerpc",
|
||||
"dep:rlwinmdec",
|
||||
]
|
||||
x86 = [
|
||||
"any-arch",
|
||||
"dep:cpp_demangle",
|
||||
"dep:iced-x86",
|
||||
"dep:msvc-demangler",
|
||||
]
|
||||
arm = [
|
||||
"any-arch",
|
||||
"dep:arm-attr",
|
||||
"dep:cpp_demangle",
|
||||
"dep:unarm",
|
||||
]
|
||||
arm64 = [
|
||||
"any-arch",
|
||||
"dep:cpp_demangle",
|
||||
"dep:yaxpeax-arch",
|
||||
"dep:yaxpeax-arm",
|
||||
]
|
||||
superh = [
|
||||
"any-arch",
|
||||
]
|
||||
demangler = [
|
||||
"dep:cpp_demangle",
|
||||
"dep:cwdemangle",
|
||||
"dep:gnuv2_demangle",
|
||||
"dep:msvc-demangler",
|
||||
]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["all"]
|
||||
@ -150,7 +150,6 @@ gimli = { git = "https://github.com/gimli-rs/gimli", rev = "7335f00e7c39fd501511
|
||||
typed-arena = { version = "2.0", default-features = false, optional = true }
|
||||
|
||||
# ppc
|
||||
cwdemangle = { version = "1.0", optional = true }
|
||||
cwextab = { version = "1.1", optional = true }
|
||||
powerpc = { version = "0.4", optional = true }
|
||||
rlwinmdec = { version = "1.1", optional = true }
|
||||
@ -159,9 +158,7 @@ rlwinmdec = { version = "1.1", optional = true }
|
||||
rabbitizer = { version = "2.0.0-alpha.4", default-features = false, features = ["all_extensions"], optional = true }
|
||||
|
||||
# x86
|
||||
cpp_demangle = { version = "0.4", default-features = false, features = ["alloc"], optional = true }
|
||||
iced-x86 = { version = "1.21", default-features = false, features = ["decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums", "no_std"], optional = true }
|
||||
msvc-demangler = { version = "0.11", optional = true }
|
||||
|
||||
# arm
|
||||
unarm = { version = "1.9", optional = true }
|
||||
@ -179,6 +176,12 @@ tempfile = { version = "3.20", optional = true }
|
||||
time = { version = "0.3", optional = true }
|
||||
encoding_rs = { version = "0.8.35", optional = true }
|
||||
|
||||
# demangler
|
||||
cpp_demangle = { version = "0.4", optional = true, default-features = false, features = ["alloc"] }
|
||||
cwdemangle = { version = "1.0", optional = true }
|
||||
gnuv2_demangle = { version = "0.1.0", optional = true }
|
||||
msvc-demangler = { version = "0.11", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winapi = { version = "0.3", optional = true, features = ["winbase"] }
|
||||
|
||||
|
@ -25,6 +25,42 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "demangler",
|
||||
"type": "choice",
|
||||
"default": "auto",
|
||||
"name": "Demangler",
|
||||
"description": "Which demangler should be used to demangle each symbol.",
|
||||
"items": [
|
||||
{
|
||||
"value": "auto",
|
||||
"name": "Auto",
|
||||
"description": "Try to automatically guess the mangling format."
|
||||
},
|
||||
{
|
||||
"value": "none",
|
||||
"name": "None",
|
||||
"description": "Disable demangling."
|
||||
},
|
||||
{
|
||||
"value": "codewarrior",
|
||||
"name": "CodeWarrior"
|
||||
},
|
||||
{
|
||||
"value": "itanium",
|
||||
"name": "Itanium"
|
||||
},
|
||||
{
|
||||
"value": "msvc",
|
||||
"name": "MSVC"
|
||||
},
|
||||
{
|
||||
"value": "gnu_legacy",
|
||||
"name": "GNU g++ (Legacy)",
|
||||
"description": "Use the old GNU mangling ABI. Used up to g++ 2.9.x"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "analyzeDataFlow",
|
||||
"type": "boolean",
|
||||
@ -158,6 +194,10 @@
|
||||
"value": "o32",
|
||||
"name": "O32"
|
||||
},
|
||||
{
|
||||
"value": "o64",
|
||||
"name": "O64"
|
||||
},
|
||||
{
|
||||
"value": "n32",
|
||||
"name": "N32"
|
||||
@ -165,6 +205,14 @@
|
||||
{
|
||||
"value": "n64",
|
||||
"name": "N64"
|
||||
},
|
||||
{
|
||||
"value": "eabi32",
|
||||
"name": "eabi32"
|
||||
},
|
||||
{
|
||||
"value": "eabi64",
|
||||
"name": "eabi64"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -247,6 +295,7 @@
|
||||
"name": "General",
|
||||
"properties": [
|
||||
"functionRelocDiffs",
|
||||
"demangler",
|
||||
"spaceBetweenArgs",
|
||||
"combineDataSections",
|
||||
"combineTextSections"
|
||||
@ -268,26 +317,17 @@
|
||||
{
|
||||
"id": "mips",
|
||||
"name": "MIPS",
|
||||
"properties": [
|
||||
"mips.abi",
|
||||
"mips.instrCategory",
|
||||
"mips.registerPrefix"
|
||||
]
|
||||
"properties": ["mips.abi", "mips.instrCategory", "mips.registerPrefix"]
|
||||
},
|
||||
{
|
||||
"id": "ppc",
|
||||
"name": "PowerPC",
|
||||
"properties": [
|
||||
"ppc.calculatePoolRelocations",
|
||||
"analyzeDataFlow"
|
||||
]
|
||||
"properties": ["ppc.calculatePoolRelocations", "analyzeDataFlow"]
|
||||
},
|
||||
{
|
||||
"id": "x86",
|
||||
"name": "x86",
|
||||
"properties": [
|
||||
"x86.formatter"
|
||||
]
|
||||
"properties": ["x86.formatter"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,9 +1,4 @@
|
||||
use alloc::{
|
||||
collections::BTreeMap,
|
||||
format,
|
||||
string::{String, ToString},
|
||||
vec::Vec,
|
||||
};
|
||||
use alloc::{collections::BTreeMap, format, string::ToString, vec::Vec};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use arm_attr::{BuildAttrs, enums::CpuArch, tag::Tag};
|
||||
@ -11,7 +6,7 @@ use object::{Endian as _, Object as _, ObjectSection as _, ObjectSymbol as _, el
|
||||
use unarm::{args, arm, thumb};
|
||||
|
||||
use crate::{
|
||||
arch::{Arch, RelocationOverride, RelocationOverrideTarget},
|
||||
arch::{Arch, OPCODE_DATA, OPCODE_INVALID, RelocationOverride, RelocationOverrideTarget},
|
||||
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig, display::InstructionPart},
|
||||
obj::{
|
||||
InstructionRef, Relocation, RelocationFlags, ResolvedInstructionRef, ResolvedRelocation,
|
||||
@ -164,7 +159,7 @@ impl ArchArm {
|
||||
}
|
||||
_ => bail!("Invalid instruction size {}", ins_ref.size),
|
||||
};
|
||||
let (ins, parsed_ins) = if ins_ref.opcode == u16::MAX {
|
||||
let (ins, parsed_ins) = if ins_ref.opcode == OPCODE_DATA {
|
||||
let mut args = args::Arguments::default();
|
||||
args[0] = args::Argument::UImm(code);
|
||||
let mnemonic = if ins_ref.size == 4 { ".word" } else { ".hword" };
|
||||
@ -238,7 +233,7 @@ impl Arch for ArchArm {
|
||||
ops.push(InstructionRef {
|
||||
address: address as u64,
|
||||
size: data.len() as u8,
|
||||
opcode: u16::MAX,
|
||||
opcode: OPCODE_DATA,
|
||||
branch_dest: None,
|
||||
});
|
||||
break;
|
||||
@ -257,7 +252,7 @@ impl Arch for ArchArm {
|
||||
ops.push(InstructionRef {
|
||||
address: address as u64,
|
||||
size: ins_size as u8,
|
||||
opcode: u16::MAX,
|
||||
opcode: OPCODE_INVALID,
|
||||
branch_dest: None,
|
||||
});
|
||||
address += ins_size as u32;
|
||||
@ -318,7 +313,7 @@ impl Arch for ArchArm {
|
||||
};
|
||||
(opcode, branch_dest)
|
||||
}
|
||||
unarm::ParseMode::Data => (u16::MAX, None),
|
||||
unarm::ParseMode::Data => (OPCODE_DATA, None),
|
||||
};
|
||||
|
||||
ops.push(InstructionRef {
|
||||
@ -409,12 +404,6 @@ impl Arch for ArchArm {
|
||||
}
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
cpp_demangle::Symbol::new(name)
|
||||
.ok()
|
||||
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
|
||||
}
|
||||
|
||||
fn reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
@ -471,12 +460,16 @@ impl Arch for ArchArm {
|
||||
section: &Section,
|
||||
mut next_address: u64,
|
||||
) -> Result<u64> {
|
||||
// Trim any trailing 4-byte zeroes from the end (padding)
|
||||
while next_address >= symbol.address + 4
|
||||
&& let Some(data) = section.data_range(next_address - 4, 4)
|
||||
&& data == [0u8; 4]
|
||||
// TODO: This should probably check the disasm mode and trim accordingly,
|
||||
// but self.disasm_modes isn't populated until post_init, so it needs a refactor.
|
||||
|
||||
// Trim any trailing 2-byte zeroes from the end (padding)
|
||||
while next_address >= symbol.address + 2
|
||||
&& let Some(data) = section.data_range(next_address - 2, 2)
|
||||
&& data == [0u8; 2]
|
||||
&& section.relocation_at(next_address - 2, 2).is_none()
|
||||
{
|
||||
next_address -= 4;
|
||||
next_address -= 2;
|
||||
}
|
||||
Ok(next_address.saturating_sub(symbol.address))
|
||||
}
|
||||
|
@ -1,8 +1,4 @@
|
||||
use alloc::{
|
||||
format,
|
||||
string::{String, ToString},
|
||||
vec::Vec,
|
||||
};
|
||||
use alloc::{format, string::ToString, vec::Vec};
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use anyhow::Result;
|
||||
@ -14,7 +10,7 @@ use yaxpeax_arm::armv8::a64::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
arch::Arch,
|
||||
arch::{Arch, OPCODE_INVALID},
|
||||
diff::{DiffObjConfig, display::InstructionPart},
|
||||
obj::{
|
||||
InstructionRef, Relocation, RelocationFlags, ResolvedInstructionRef, ResolvedRelocation,
|
||||
@ -60,7 +56,7 @@ impl Arch for ArchArm64 {
|
||||
ops.push(InstructionRef {
|
||||
address,
|
||||
size: 4,
|
||||
opcode: u16::MAX,
|
||||
opcode: OPCODE_INVALID,
|
||||
branch_dest: None,
|
||||
});
|
||||
continue;
|
||||
@ -87,7 +83,7 @@ impl Arch for ArchArm64 {
|
||||
let decoder = InstDecoder::default();
|
||||
let mut ins = Instruction::default();
|
||||
if decoder.decode_into(&mut ins, &mut reader).is_err() {
|
||||
cb(InstructionPart::opcode("<invalid>", u16::MAX))?;
|
||||
cb(InstructionPart::opcode("<invalid>", OPCODE_INVALID))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -108,12 +104,6 @@ impl Arch for ArchArm64 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
cpp_demangle::Symbol::new(name)
|
||||
.ok()
|
||||
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
|
||||
}
|
||||
|
||||
fn reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
@ -2295,7 +2285,7 @@ where Cb: FnMut(InstructionPart<'static>) {
|
||||
// Opcode is #[repr(u16)], but the tuple variants negate that, so we have to do this instead.
|
||||
const fn opcode_to_u16(opcode: Opcode) -> u16 {
|
||||
match opcode {
|
||||
Opcode::Invalid => u16::MAX,
|
||||
Opcode::Invalid => OPCODE_INVALID,
|
||||
Opcode::UDF => 0,
|
||||
Opcode::MOVN => 1,
|
||||
Opcode::MOVK => 2,
|
||||
|
@ -1,6 +1,6 @@
|
||||
use alloc::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
string::{String, ToString},
|
||||
string::ToString,
|
||||
vec::Vec,
|
||||
};
|
||||
|
||||
@ -46,8 +46,11 @@ impl ArchMips {
|
||||
object::FileFlags::None => {}
|
||||
object::FileFlags::Elf { e_flags, .. } => {
|
||||
abi = match e_flags & EF_MIPS_ABI {
|
||||
elf::EF_MIPS_ABI_O32 | elf::EF_MIPS_ABI_O64 => Abi::O32,
|
||||
elf::EF_MIPS_ABI_EABI32 | elf::EF_MIPS_ABI_EABI64 => Abi::N32,
|
||||
elf::EF_MIPS_ABI_O32 => Abi::O32,
|
||||
elf::EF_MIPS_ABI_O64 if e_flags & elf::EF_MIPS_ABI2 != 0 => Abi::N64,
|
||||
elf::EF_MIPS_ABI_O64 => Abi::O64,
|
||||
elf::EF_MIPS_ABI_EABI32 => Abi::EABI32,
|
||||
elf::EF_MIPS_ABI_EABI64 => Abi::EABI64,
|
||||
_ => {
|
||||
if e_flags & elf::EF_MIPS_ABI2 != 0 {
|
||||
Abi::N32
|
||||
@ -170,8 +173,11 @@ impl ArchMips {
|
||||
.with_abi(match diff_config.mips_abi {
|
||||
MipsAbi::Auto => self.abi,
|
||||
MipsAbi::O32 => Abi::O32,
|
||||
MipsAbi::O64 => Abi::O64,
|
||||
MipsAbi::N32 => Abi::N32,
|
||||
MipsAbi::N64 => Abi::N64,
|
||||
MipsAbi::Eabi32 => Abi::EABI32,
|
||||
MipsAbi::Eabi64 => Abi::EABI64,
|
||||
})
|
||||
}
|
||||
|
||||
@ -298,13 +304,6 @@ impl Arch for ArchMips {
|
||||
}
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
cpp_demangle::Symbol::new(name)
|
||||
.ok()
|
||||
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
|
||||
.or_else(|| cwdemangle::demangle(name, &cwdemangle::DemangleOptions::default()))
|
||||
}
|
||||
|
||||
fn reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
@ -356,6 +355,7 @@ impl Arch for ArchMips {
|
||||
while new_address >= symbol.address + 4
|
||||
&& let Some(data) = section.data_range(new_address - 4, 4)
|
||||
&& data == [0u8; 4]
|
||||
&& section.relocation_at(next_address - 4, 4).is_none()
|
||||
{
|
||||
new_address -= 4;
|
||||
}
|
||||
|
@ -41,6 +41,9 @@ pub mod superh;
|
||||
#[cfg(feature = "x86")]
|
||||
pub mod x86;
|
||||
|
||||
pub const OPCODE_INVALID: u16 = u16::MAX;
|
||||
pub const OPCODE_DATA: u16 = u16::MAX - 1;
|
||||
|
||||
/// Represents the type of data associated with an instruction
|
||||
#[derive(PartialEq)]
|
||||
pub enum DataType {
|
||||
@ -368,8 +371,6 @@ pub trait Arch: Any + Debug + Send + Sync {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn demangle(&self, _name: &str) -> Option<String> { None }
|
||||
|
||||
fn reloc_name(&self, _flags: RelocationFlags) -> Option<&'static str> { None }
|
||||
|
||||
fn data_reloc_size(&self, flags: RelocationFlags) -> usize;
|
||||
|
@ -308,18 +308,6 @@ impl Arch for ArchPpc {
|
||||
}
|
||||
}
|
||||
|
||||
fn demangle(&self, mut name: &str) -> Option<String> {
|
||||
if name.starts_with('?') {
|
||||
msvc_demangler::demangle(name, msvc_demangler::DemangleFlags::llvm()).ok()
|
||||
} else {
|
||||
name = name.trim_start_matches('.');
|
||||
cpp_demangle::Symbol::new(name)
|
||||
.ok()
|
||||
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
|
||||
.or_else(|| cwdemangle::demangle(name, &cwdemangle::DemangleOptions::default()))
|
||||
}
|
||||
}
|
||||
|
||||
fn reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
@ -469,6 +457,7 @@ impl Arch for ArchPpc {
|
||||
while next_address >= symbol.address + 4
|
||||
&& let Some(data) = section.data_range(next_address - 4, 4)
|
||||
&& data == [0u8; 4]
|
||||
&& section.relocation_at(next_address - 4, 4).is_none()
|
||||
{
|
||||
next_address -= 4;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use alloc::{collections::BTreeMap, format, string::String, vec, vec::Vec};
|
||||
use alloc::{collections::BTreeMap, format, vec, vec::Vec};
|
||||
|
||||
use anyhow::Result;
|
||||
use object::elf;
|
||||
@ -132,12 +132,6 @@ impl Arch for ArchSuperH {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
cpp_demangle::Symbol::new(name)
|
||||
.ok()
|
||||
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
|
||||
}
|
||||
|
||||
fn reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match flags {
|
||||
RelocationFlags::Elf(r_type) => match r_type {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use alloc::{boxed::Box, format, string::String, vec::Vec};
|
||||
use alloc::{boxed::Box, format, vec::Vec};
|
||||
use core::cmp::Ordering;
|
||||
|
||||
use anyhow::{Context, Result, anyhow, bail};
|
||||
@ -9,7 +9,7 @@ use iced_x86::{
|
||||
use object::{Endian as _, Object as _, ObjectSection as _, elf, pe};
|
||||
|
||||
use crate::{
|
||||
arch::{Arch, RelocationOverride, RelocationOverrideTarget},
|
||||
arch::{Arch, OPCODE_DATA, RelocationOverride, RelocationOverrideTarget},
|
||||
diff::{DiffObjConfig, X86Formatter, display::InstructionPart},
|
||||
obj::{InstructionRef, Relocation, RelocationFlags, ResolvedInstructionRef, Section, Symbol},
|
||||
};
|
||||
@ -89,8 +89,6 @@ impl ArchX86 {
|
||||
}
|
||||
}
|
||||
|
||||
const DATA_OPCODE: u16 = u16::MAX - 1;
|
||||
|
||||
impl Arch for ArchX86 {
|
||||
fn scan_instructions_internal(
|
||||
&self,
|
||||
@ -121,7 +119,7 @@ impl Arch for ArchX86 {
|
||||
out.push(InstructionRef {
|
||||
address,
|
||||
size: size as u8,
|
||||
opcode: DATA_OPCODE,
|
||||
opcode: OPCODE_DATA,
|
||||
branch_dest: None,
|
||||
});
|
||||
|
||||
@ -148,7 +146,7 @@ impl Arch for ArchX86 {
|
||||
out.push(InstructionRef {
|
||||
address: indirect_array_address + i as u64,
|
||||
size: 1,
|
||||
opcode: DATA_OPCODE,
|
||||
opcode: OPCODE_DATA,
|
||||
branch_dest: None,
|
||||
});
|
||||
}
|
||||
@ -187,14 +185,14 @@ impl Arch for ArchX86 {
|
||||
diff_config: &DiffObjConfig,
|
||||
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
|
||||
) -> Result<()> {
|
||||
if resolved.ins_ref.opcode == DATA_OPCODE {
|
||||
if resolved.ins_ref.opcode == OPCODE_DATA {
|
||||
let (mnemonic, imm) = match resolved.ins_ref.size {
|
||||
1 => (".byte", resolved.code[0] as u64),
|
||||
2 => (".word", self.endianness.read_u16_bytes(resolved.code.try_into()?) as u64),
|
||||
4 => (".dword", self.endianness.read_u32_bytes(resolved.code.try_into()?) as u64),
|
||||
_ => bail!("Unsupported x86 inline data size {}", resolved.ins_ref.size),
|
||||
};
|
||||
cb(InstructionPart::opcode(mnemonic, DATA_OPCODE))?;
|
||||
cb(InstructionPart::opcode(mnemonic, OPCODE_DATA))?;
|
||||
if resolved.relocation.is_some() {
|
||||
cb(InstructionPart::reloc())?;
|
||||
} else {
|
||||
@ -302,16 +300,6 @@ impl Arch for ArchX86 {
|
||||
Ok(Some(RelocationOverride { target: RelocationOverrideTarget::Keep, addend }))
|
||||
}
|
||||
|
||||
fn demangle(&self, name: &str) -> Option<String> {
|
||||
if name.starts_with('?') {
|
||||
msvc_demangler::demangle(name, msvc_demangler::DemangleFlags::llvm()).ok()
|
||||
} else {
|
||||
cpp_demangle::Symbol::new(name)
|
||||
.ok()
|
||||
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
|
||||
}
|
||||
}
|
||||
|
||||
fn reloc_name(&self, flags: RelocationFlags) -> Option<&'static str> {
|
||||
match self.arch {
|
||||
Architecture::X86 => match flags {
|
||||
@ -836,7 +824,7 @@ mod test {
|
||||
ins_ref: InstructionRef {
|
||||
address: 0x1234,
|
||||
size: 1,
|
||||
opcode: DATA_OPCODE,
|
||||
opcode: OPCODE_DATA,
|
||||
branch_dest: None,
|
||||
},
|
||||
code: &code,
|
||||
@ -850,7 +838,7 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(parts, &[
|
||||
InstructionPart::opcode(".byte", DATA_OPCODE),
|
||||
InstructionPart::opcode(".byte", OPCODE_DATA),
|
||||
InstructionPart::unsigned(0xABu64),
|
||||
]);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
pub mod path;
|
||||
|
||||
use alloc::{
|
||||
borrow::Cow,
|
||||
collections::BTreeMap,
|
||||
string::{String, ToString},
|
||||
vec::Vec,
|
||||
@ -45,6 +46,8 @@ pub struct ProjectConfig {
|
||||
pub units: Option<Vec<ProjectObject>>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub progress_categories: Option<Vec<ProjectProgressCategory>>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub options: Option<ProjectOptions>,
|
||||
}
|
||||
|
||||
impl ProjectConfig {
|
||||
@ -116,6 +119,8 @@ pub struct ProjectObject {
|
||||
pub metadata: Option<ProjectObjectMetadata>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub symbol_mappings: Option<BTreeMap<String, String>>,
|
||||
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
|
||||
pub options: Option<ProjectOptions>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
@ -143,6 +148,15 @@ pub struct ProjectProgressCategory {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub type ProjectOptions = BTreeMap<String, ProjectOptionValue>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged))]
|
||||
pub enum ProjectOptionValue {
|
||||
Bool(bool),
|
||||
String(String),
|
||||
}
|
||||
|
||||
impl ProjectObject {
|
||||
pub fn name(&self) -> &str {
|
||||
if let Some(name) = &self.name {
|
||||
@ -179,6 +193,8 @@ impl ProjectObject {
|
||||
pub fn auto_generated(&self) -> Option<bool> {
|
||||
self.metadata.as_ref().and_then(|m| m.auto_generated)
|
||||
}
|
||||
|
||||
pub fn options(&self) -> Option<&ProjectOptions> { self.options.as_ref() }
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Eq, PartialEq)]
|
||||
@ -310,3 +326,47 @@ pub fn build_globset(vec: &[Glob]) -> Result<GlobSet, globset::Error> {
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
|
||||
#[cfg(feature = "any-arch")]
|
||||
pub fn apply_project_options(
|
||||
diff_config: &mut crate::diff::DiffObjConfig,
|
||||
options: &ProjectOptions,
|
||||
) -> Result<()> {
|
||||
use core::str::FromStr;
|
||||
|
||||
use crate::diff::{ConfigEnum, ConfigPropertyId, ConfigPropertyKind};
|
||||
|
||||
let mut result = Ok(());
|
||||
for (key, value) in options.iter() {
|
||||
let property_id = ConfigPropertyId::from_str(key)
|
||||
.map_err(|()| anyhow!("Invalid configuration property: {key}"))?;
|
||||
let value = match value {
|
||||
ProjectOptionValue::Bool(value) => Cow::Borrowed(if *value { "true" } else { "false" }),
|
||||
ProjectOptionValue::String(value) => Cow::Borrowed(value.as_str()),
|
||||
};
|
||||
if diff_config.set_property_value_str(property_id, &value).is_err() {
|
||||
if result.is_err() {
|
||||
// Already returning an error, skip further errors
|
||||
continue;
|
||||
}
|
||||
let mut expected = String::new();
|
||||
match property_id.kind() {
|
||||
ConfigPropertyKind::Boolean => expected.push_str("true, false"),
|
||||
ConfigPropertyKind::Choice(variants) => {
|
||||
for (idx, variant) in variants.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
expected.push_str(", ");
|
||||
}
|
||||
expected.push_str(variant.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
result = Err(anyhow!(
|
||||
"Invalid value for {}. Expected one of: {}",
|
||||
property_id.name(),
|
||||
expected
|
||||
));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
51
objdiff-core/src/diff/demangler.rs
Normal file
51
objdiff-core/src/diff/demangler.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use alloc::string::String;
|
||||
|
||||
use crate::diff::Demangler;
|
||||
|
||||
#[cfg(feature = "demangler")]
|
||||
impl Demangler {
|
||||
pub fn demangle(&self, name: &str) -> Option<String> {
|
||||
match self {
|
||||
Demangler::None => None,
|
||||
Demangler::Codewarrior => Self::demangle_codewarrior(name),
|
||||
Demangler::Msvc => Self::demangle_msvc(name),
|
||||
Demangler::Itanium => Self::demangle_itanium(name),
|
||||
Demangler::GnuLegacy => Self::demangle_gnu_legacy(name),
|
||||
Demangler::Auto => {
|
||||
// Try to guess
|
||||
if name.starts_with('?') {
|
||||
Self::demangle_msvc(name)
|
||||
} else {
|
||||
Self::demangle_codewarrior(name)
|
||||
.or_else(|| Self::demangle_gnu_legacy(name))
|
||||
.or_else(|| Self::demangle_itanium(name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn demangle_codewarrior(name: &str) -> Option<String> {
|
||||
cwdemangle::demangle(name, &cwdemangle::DemangleOptions::default())
|
||||
}
|
||||
|
||||
fn demangle_msvc(name: &str) -> Option<String> {
|
||||
msvc_demangler::demangle(name, msvc_demangler::DemangleFlags::llvm()).ok()
|
||||
}
|
||||
|
||||
fn demangle_itanium(name: &str) -> Option<String> {
|
||||
let name = name.trim_start_matches('.');
|
||||
cpp_demangle::Symbol::new(name)
|
||||
.ok()
|
||||
.and_then(|s| s.demangle(&cpp_demangle::DemangleOptions::default()).ok())
|
||||
}
|
||||
|
||||
fn demangle_gnu_legacy(name: &str) -> Option<String> {
|
||||
let name = name.trim_start_matches('.');
|
||||
gnuv2_demangle::demangle(name, &gnuv2_demangle::DemangleConfig::new_no_cfilt_mimics()).ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "demangler"))]
|
||||
impl Demangler {
|
||||
pub fn demangle(&self, _name: &str) -> Option<String> { None }
|
||||
}
|
@ -22,6 +22,7 @@ use crate::{
|
||||
|
||||
pub mod code;
|
||||
pub mod data;
|
||||
pub mod demangler;
|
||||
pub mod display;
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/config.gen.rs"));
|
||||
|
401
objdiff-core/src/obj/mdebug.rs
Normal file
401
objdiff-core/src/obj/mdebug.rs
Normal file
@ -0,0 +1,401 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use anyhow::{Context, Result, bail, ensure};
|
||||
use object::{Endianness, Object, ObjectSection};
|
||||
|
||||
use super::{Section, SectionKind};
|
||||
|
||||
const HDRR_SIZE: usize = 0x60;
|
||||
const FDR_SIZE: usize = 0x48;
|
||||
const PDR_SIZE: usize = 0x34;
|
||||
const SYMR_SIZE: usize = 0x0c;
|
||||
|
||||
const ST_PROC: u8 = 6;
|
||||
const ST_STATICPROC: u8 = 14;
|
||||
const ST_END: u8 = 8;
|
||||
|
||||
pub(super) fn parse_line_info_mdebug(
|
||||
obj_file: &object::File,
|
||||
sections: &mut [Section],
|
||||
) -> Result<()> {
|
||||
let Some(section) = obj_file.section_by_name(".mdebug") else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let data = section.data().context("failed to read .mdebug contents")?;
|
||||
if data.len() < HDRR_SIZE {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let section_file_offset = section.file_range().map(|(offset, _)| offset as usize);
|
||||
|
||||
let endianness = obj_file.endianness();
|
||||
let header = Header::parse(data, endianness)?;
|
||||
|
||||
let symbols_data = slice_at(
|
||||
data,
|
||||
header.cb_sym_offset,
|
||||
header.isym_max.checked_mul(SYMR_SIZE as u32).context("symbol table size overflow")?,
|
||||
section_file_offset,
|
||||
)?;
|
||||
let symbols = parse_symbols(symbols_data, endianness)?;
|
||||
|
||||
let fdr_data = slice_at(
|
||||
data,
|
||||
header.cb_fd_offset,
|
||||
header
|
||||
.ifd_max
|
||||
.checked_mul(FDR_SIZE as u32)
|
||||
.context("file descriptor table size overflow")?,
|
||||
section_file_offset,
|
||||
)?;
|
||||
|
||||
for fdr_index in 0..header.ifd_max as usize {
|
||||
let fdr_offset = fdr_index * FDR_SIZE;
|
||||
let fdr = FileDescriptor::parse(&fdr_data[fdr_offset..fdr_offset + FDR_SIZE], endianness)?;
|
||||
if fdr.cpd == 0 || fdr.csym == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sym_base = fdr.isym_base as usize;
|
||||
let sym_end = sym_base + fdr.csym as usize;
|
||||
if sym_end > symbols.len() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(line_file_offset) = header.cb_line_offset.checked_add(fdr.cb_line_offset) else {
|
||||
continue;
|
||||
};
|
||||
let Some(line_file_base) =
|
||||
resolve_offset(line_file_offset, data.len(), section_file_offset)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some(line_file_end) = line_file_base.checked_add(fdr.cb_line as usize) else {
|
||||
continue;
|
||||
};
|
||||
if line_file_end > data.len() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for local_proc_index in 0..fdr.cpd as usize {
|
||||
let pdr_index = fdr.ipd_first as usize + local_proc_index;
|
||||
let pdr_offset = header
|
||||
.cb_pd_offset
|
||||
.checked_add((pdr_index as u32) * PDR_SIZE as u32)
|
||||
.context("procedure descriptor offset overflow")?;
|
||||
let pdr_data = match slice_at(data, pdr_offset, PDR_SIZE as u32, section_file_offset) {
|
||||
Ok(data) => data,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let pdr = ProcDescriptor::parse(pdr_data, endianness)?;
|
||||
if pdr.isym as usize >= fdr.csym as usize {
|
||||
continue;
|
||||
}
|
||||
let global_sym_index = sym_base + pdr.isym as usize;
|
||||
let Some(start_symbol) = symbols.get(global_sym_index) else {
|
||||
continue;
|
||||
};
|
||||
if start_symbol.st != ST_PROC && start_symbol.st != ST_STATICPROC {
|
||||
continue;
|
||||
}
|
||||
|
||||
let local_index = pdr.isym as u32;
|
||||
let mut end_address = None;
|
||||
for sym in &symbols[global_sym_index..sym_end] {
|
||||
if sym.st == ST_END && sym.index == local_index {
|
||||
end_address = Some(sym.value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let Some(end_address) = end_address else {
|
||||
continue;
|
||||
};
|
||||
let Some(size) = end_address.checked_sub(start_symbol.value) else {
|
||||
continue;
|
||||
};
|
||||
if size == 0 || size % 4 != 0 {
|
||||
continue;
|
||||
}
|
||||
let word_count = (size / 4) as usize;
|
||||
if word_count == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(mut cursor) = line_file_base.checked_add(pdr.cb_line_offset as usize) else {
|
||||
continue;
|
||||
};
|
||||
if cursor >= line_file_end {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut line_number = pdr.ln_low as i32;
|
||||
let mut lines = Vec::with_capacity(word_count);
|
||||
while lines.len() < word_count && cursor < line_file_end {
|
||||
let b0 = data[cursor];
|
||||
cursor += 1;
|
||||
let count = (b0 & 0x0f) as usize + 1;
|
||||
let delta = decode_delta(endianness, b0 >> 4, data, &mut cursor, line_file_end)?;
|
||||
line_number = line_number.wrapping_add(delta as i32);
|
||||
for _ in 0..count {
|
||||
if lines.len() == word_count {
|
||||
break;
|
||||
}
|
||||
lines.push(line_number);
|
||||
}
|
||||
}
|
||||
|
||||
if lines.len() != word_count {
|
||||
continue;
|
||||
}
|
||||
|
||||
assign_lines(sections, fdr.adr as u64 + pdr.addr as u64, &lines);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn assign_lines(sections: &mut [Section], base_address: u64, lines: &[i32]) {
|
||||
let mut address = base_address;
|
||||
for &line in lines {
|
||||
if line >= 0
|
||||
&& let Some(section) = find_code_section(sections, address)
|
||||
{
|
||||
section.line_info.insert(address, line as u32);
|
||||
}
|
||||
address = address.wrapping_add(4);
|
||||
}
|
||||
}
|
||||
|
||||
fn find_code_section(sections: &mut [Section], address: u64) -> Option<&mut Section> {
|
||||
sections.iter_mut().find(|section| {
|
||||
section.kind == SectionKind::Code
|
||||
&& address >= section.address
|
||||
&& address < section.address + section.size
|
||||
})
|
||||
}
|
||||
|
||||
fn decode_delta(
|
||||
endianness: Endianness,
|
||||
nibble: u8,
|
||||
data: &[u8],
|
||||
cursor: &mut usize,
|
||||
end: usize,
|
||||
) -> Result<i32> {
|
||||
if nibble == 8 {
|
||||
ensure!(*cursor + 2 <= end, "extended delta out of range");
|
||||
let bytes: [u8; 2] = data[*cursor..*cursor + 2].try_into().unwrap();
|
||||
*cursor += 2;
|
||||
Ok(match endianness {
|
||||
Endianness::Big => i16::from_be_bytes(bytes) as i32,
|
||||
Endianness::Little => i16::from_le_bytes(bytes) as i32,
|
||||
})
|
||||
} else {
|
||||
let mut value = (nibble & 0x0f) as i32;
|
||||
if value & 0x8 != 0 {
|
||||
value -= 0x10;
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn slice_at(
|
||||
data: &[u8],
|
||||
offset: u32,
|
||||
size: u32,
|
||||
section_file_offset: Option<usize>,
|
||||
) -> Result<&[u8]> {
|
||||
let size = size as usize;
|
||||
if size == 0 {
|
||||
ensure!(
|
||||
resolve_offset(offset, data.len(), section_file_offset).is_some(),
|
||||
"offset outside of .mdebug section"
|
||||
);
|
||||
return Ok(&data[0..0]);
|
||||
}
|
||||
let Some(offset) = resolve_offset(offset, data.len(), section_file_offset) else {
|
||||
bail!("offset outside of .mdebug section");
|
||||
};
|
||||
let end = offset.checked_add(size).context("range overflow")?;
|
||||
ensure!(end <= data.len(), "range exceeds .mdebug size");
|
||||
Ok(&data[offset..end])
|
||||
}
|
||||
|
||||
fn resolve_offset(
|
||||
offset: u32,
|
||||
data_len: usize,
|
||||
section_file_offset: Option<usize>,
|
||||
) -> Option<usize> {
|
||||
let offset = offset as usize;
|
||||
if offset <= data_len {
|
||||
Some(offset)
|
||||
} else if let Some(file_offset) = section_file_offset {
|
||||
offset.checked_sub(file_offset).filter(|rel| *rel <= data_len)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Header {
|
||||
cb_line_offset: u32,
|
||||
cb_pd_offset: u32,
|
||||
cb_sym_offset: u32,
|
||||
cb_fd_offset: u32,
|
||||
isym_max: u32,
|
||||
ifd_max: u32,
|
||||
}
|
||||
|
||||
impl Header {
|
||||
fn parse(data: &[u8], endianness: Endianness) -> Result<Self> {
|
||||
ensure!(HDRR_SIZE <= data.len(), ".mdebug header truncated");
|
||||
let mut cursor = 0;
|
||||
let magic = read_u16(data, &mut cursor, endianness)?;
|
||||
let _vstamp = read_u16(data, &mut cursor, endianness)?;
|
||||
ensure!(magic == 0x7009, "unexpected .mdebug magic: {magic:#x}");
|
||||
let _iline_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_line = read_u32(data, &mut cursor, endianness)?;
|
||||
let cb_line_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _idn_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_dn_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _ipd_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let cb_pd_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let isym_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let cb_sym_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iopt_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_opt_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iaux_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_aux_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iss_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_ss_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iss_ext_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_ss_ext_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let ifd_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let cb_fd_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _crfd = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_rfd_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iext_max = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_ext_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
|
||||
Ok(Header { cb_line_offset, cb_pd_offset, cb_sym_offset, cb_fd_offset, isym_max, ifd_max })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct FileDescriptor {
|
||||
adr: u32,
|
||||
isym_base: u32,
|
||||
csym: u32,
|
||||
ipd_first: u16,
|
||||
cpd: u16,
|
||||
cb_line_offset: u32,
|
||||
cb_line: u32,
|
||||
}
|
||||
|
||||
impl FileDescriptor {
|
||||
fn parse(data: &[u8], endianness: Endianness) -> Result<Self> {
|
||||
ensure!(data.len() >= FDR_SIZE, "FDR truncated");
|
||||
let mut cursor = 0;
|
||||
let adr = read_u32(data, &mut cursor, endianness)?;
|
||||
let _rss = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iss_base = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cb_ss = read_u32(data, &mut cursor, endianness)?;
|
||||
let isym_base = read_u32(data, &mut cursor, endianness)?;
|
||||
let csym = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iline_base = read_u32(data, &mut cursor, endianness)?;
|
||||
let _cline = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iopt_base = read_u32(data, &mut cursor, endianness)?;
|
||||
let _copt = read_u32(data, &mut cursor, endianness)?;
|
||||
let ipd_first = read_u16(data, &mut cursor, endianness)?;
|
||||
let cpd = read_u16(data, &mut cursor, endianness)?;
|
||||
let _iaux_base = read_u32(data, &mut cursor, endianness)?;
|
||||
let _caux = read_u32(data, &mut cursor, endianness)?;
|
||||
let _rfd_base = read_u32(data, &mut cursor, endianness)?;
|
||||
let _crfd = read_u32(data, &mut cursor, endianness)?;
|
||||
let _bits = read_u32(data, &mut cursor, endianness)?;
|
||||
let cb_line_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
let cb_line = read_u32(data, &mut cursor, endianness)?;
|
||||
|
||||
Ok(FileDescriptor { adr, isym_base, csym, ipd_first, cpd, cb_line_offset, cb_line })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ProcDescriptor {
|
||||
addr: u32,
|
||||
isym: u32,
|
||||
ln_low: i32,
|
||||
cb_line_offset: u32,
|
||||
}
|
||||
|
||||
impl ProcDescriptor {
|
||||
fn parse(data: &[u8], endianness: Endianness) -> Result<Self> {
|
||||
ensure!(data.len() >= PDR_SIZE, "PDR truncated");
|
||||
let mut cursor = 0;
|
||||
let addr = read_u32(data, &mut cursor, endianness)?;
|
||||
let isym = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iline = read_u32(data, &mut cursor, endianness)?;
|
||||
let _regmask = read_u32(data, &mut cursor, endianness)?;
|
||||
let _regoffset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _iopt = read_u32(data, &mut cursor, endianness)?;
|
||||
let _fregmask = read_u32(data, &mut cursor, endianness)?;
|
||||
let _fregoffset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _frameoffset = read_u32(data, &mut cursor, endianness)?;
|
||||
let _framereg = read_u16(data, &mut cursor, endianness)?;
|
||||
let _pcreg = read_u16(data, &mut cursor, endianness)?;
|
||||
let ln_low = read_i32(data, &mut cursor, endianness)?;
|
||||
let _ln_high = read_i32(data, &mut cursor, endianness)?;
|
||||
let cb_line_offset = read_u32(data, &mut cursor, endianness)?;
|
||||
|
||||
Ok(ProcDescriptor { addr, isym, ln_low, cb_line_offset })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct SymbolEntry {
|
||||
value: u32,
|
||||
st: u8,
|
||||
index: u32,
|
||||
}
|
||||
|
||||
fn parse_symbols(data: &[u8], endianness: Endianness) -> Result<Vec<SymbolEntry>> {
|
||||
ensure!(data.len().is_multiple_of(SYMR_SIZE), "symbol table misaligned");
|
||||
let mut symbols = Vec::with_capacity(data.len() / SYMR_SIZE);
|
||||
let mut cursor = 0;
|
||||
while cursor + SYMR_SIZE <= data.len() {
|
||||
let _iss = read_u32(data, &mut cursor, endianness)?;
|
||||
let value = read_u32(data, &mut cursor, endianness)?;
|
||||
let bits = read_u32(data, &mut cursor, endianness)?;
|
||||
let (st, index) = match endianness {
|
||||
Endianness::Big => (((bits >> 26) & 0x3f) as u8, bits & 0x000f_ffff),
|
||||
Endianness::Little => (((bits & 0x3f) as u8), (bits >> 12) & 0x000f_ffff),
|
||||
};
|
||||
symbols.push(SymbolEntry { value, st, index });
|
||||
}
|
||||
Ok(symbols)
|
||||
}
|
||||
|
||||
fn read_u16(data: &[u8], cursor: &mut usize, endianness: Endianness) -> Result<u16> {
|
||||
ensure!(*cursor + 2 <= data.len(), "unexpected EOF while reading u16");
|
||||
let bytes: [u8; 2] = data[*cursor..*cursor + 2].try_into().unwrap();
|
||||
*cursor += 2;
|
||||
Ok(match endianness {
|
||||
Endianness::Big => u16::from_be_bytes(bytes),
|
||||
Endianness::Little => u16::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
fn read_u32(data: &[u8], cursor: &mut usize, endianness: Endianness) -> Result<u32> {
|
||||
ensure!(*cursor + 4 <= data.len(), "unexpected EOF while reading u32");
|
||||
let bytes: [u8; 4] = data[*cursor..*cursor + 4].try_into().unwrap();
|
||||
*cursor += 4;
|
||||
Ok(match endianness {
|
||||
Endianness::Big => u32::from_be_bytes(bytes),
|
||||
Endianness::Little => u32::from_le_bytes(bytes),
|
||||
})
|
||||
}
|
||||
|
||||
fn read_i32(data: &[u8], cursor: &mut usize, endianness: Endianness) -> Result<i32> {
|
||||
Ok(read_u32(data, cursor, endianness)? as i32)
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
#[cfg(feature = "dwarf")]
|
||||
mod dwarf2;
|
||||
mod mdebug;
|
||||
pub mod read;
|
||||
pub mod split_meta;
|
||||
|
||||
@ -107,32 +108,33 @@ impl Section {
|
||||
// The alignment to use when "Combine data/text sections" is enabled.
|
||||
pub fn combined_alignment(&self) -> u64 {
|
||||
const MIN_ALIGNMENT: u64 = 4;
|
||||
self.align.map(|align| align.get().max(MIN_ALIGNMENT)).unwrap_or(MIN_ALIGNMENT)
|
||||
self.align.map_or(MIN_ALIGNMENT, |align| align.get().max(MIN_ALIGNMENT))
|
||||
}
|
||||
|
||||
pub fn relocation_at<'obj>(
|
||||
&'obj self,
|
||||
obj: &'obj Object,
|
||||
ins_ref: InstructionRef,
|
||||
) -> Option<ResolvedRelocation<'obj>> {
|
||||
match self.relocations.binary_search_by_key(&ins_ref.address, |r| r.address) {
|
||||
pub fn relocation_at(&self, address: u64, size: u8) -> Option<&Relocation> {
|
||||
match self.relocations.binary_search_by_key(&address, |r| r.address) {
|
||||
Ok(mut i) => {
|
||||
// Find the first relocation at the address
|
||||
while i
|
||||
.checked_sub(1)
|
||||
.and_then(|n| self.relocations.get(n))
|
||||
.is_some_and(|r| r.address == ins_ref.address)
|
||||
.is_some_and(|r| r.address == address)
|
||||
{
|
||||
i -= 1;
|
||||
}
|
||||
self.relocations.get(i)
|
||||
}
|
||||
Err(i) => self
|
||||
.relocations
|
||||
.get(i)
|
||||
.filter(|r| r.address < ins_ref.address + ins_ref.size as u64),
|
||||
Err(i) => self.relocations.get(i).filter(|r| r.address < address + size as u64),
|
||||
}
|
||||
.and_then(|relocation| {
|
||||
}
|
||||
|
||||
pub fn resolve_relocation_at<'obj>(
|
||||
&'obj self,
|
||||
obj: &'obj Object,
|
||||
address: u64,
|
||||
size: u8,
|
||||
) -> Option<ResolvedRelocation<'obj>> {
|
||||
self.relocation_at(address, size).and_then(|relocation| {
|
||||
let symbol = obj.symbols.get(relocation.target_symbol)?;
|
||||
Some(ResolvedRelocation { relocation, symbol })
|
||||
})
|
||||
@ -316,7 +318,7 @@ impl Object {
|
||||
let section = self.sections.get(section_index)?;
|
||||
let offset = ins_ref.address.checked_sub(section.address)?;
|
||||
let code = section.data.get(offset as usize..offset as usize + ins_ref.size as usize)?;
|
||||
let relocation = section.relocation_at(self, ins_ref);
|
||||
let relocation = section.resolve_relocation_at(self, ins_ref.address, ins_ref.size);
|
||||
Some(ResolvedInstructionRef {
|
||||
ins_ref,
|
||||
symbol_index,
|
||||
|
@ -41,6 +41,7 @@ fn map_symbol(
|
||||
symbol: &object::Symbol,
|
||||
section_indices: &[usize],
|
||||
split_meta: Option<&SplitMeta>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<Symbol> {
|
||||
let mut name = symbol.name().context("Failed to process symbol name")?.to_string();
|
||||
let mut size = symbol.size();
|
||||
@ -90,7 +91,7 @@ fn map_symbol(
|
||||
_ => SymbolKind::Unknown,
|
||||
};
|
||||
let address = arch.symbol_address(symbol.address(), kind);
|
||||
let demangled_name = arch.demangle(&name);
|
||||
let demangled_name = config.demangler.demangle(&name);
|
||||
// Find the virtual address for the symbol if available
|
||||
let virtual_address = split_meta
|
||||
.and_then(|m| m.virtual_addresses.as_ref())
|
||||
@ -116,6 +117,7 @@ fn map_symbols(
|
||||
sections: &[Section],
|
||||
section_indices: &[usize],
|
||||
split_meta: Option<&SplitMeta>,
|
||||
config: &DiffObjConfig,
|
||||
) -> Result<(Vec<Symbol>, Vec<usize>)> {
|
||||
let symbol_count = obj_file.symbols().count();
|
||||
let mut symbols = Vec::<Symbol>::with_capacity(symbol_count + obj_file.sections().count());
|
||||
@ -124,7 +126,7 @@ fn map_symbols(
|
||||
if symbol_indices.len() <= obj_symbol.index().0 {
|
||||
symbol_indices.resize(obj_symbol.index().0 + 1, usize::MAX);
|
||||
}
|
||||
let symbol = map_symbol(arch, obj_file, &obj_symbol, section_indices, split_meta)?;
|
||||
let symbol = map_symbol(arch, obj_file, &obj_symbol, section_indices, split_meta, config)?;
|
||||
symbol_indices[obj_symbol.index().0] = symbols.len();
|
||||
symbols.push(symbol);
|
||||
}
|
||||
@ -652,6 +654,10 @@ fn parse_line_info(
|
||||
log::warn!("Failed to parse COFF line info: {e}");
|
||||
}
|
||||
|
||||
if let Err(e) = super::mdebug::parse_line_info_mdebug(obj_file, sections) {
|
||||
log::warn!("Failed to parse MIPS mdebug line info: {e}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -997,8 +1003,14 @@ pub fn parse(data: &[u8], config: &DiffObjConfig, diff_side: DiffSide) -> Result
|
||||
let split_meta = parse_split_meta(&obj_file)?;
|
||||
let (mut sections, section_indices) =
|
||||
map_sections(arch.as_ref(), &obj_file, split_meta.as_ref())?;
|
||||
let (mut symbols, symbol_indices) =
|
||||
map_symbols(arch.as_ref(), &obj_file, §ions, §ion_indices, split_meta.as_ref())?;
|
||||
let (mut symbols, symbol_indices) = map_symbols(
|
||||
arch.as_ref(),
|
||||
&obj_file,
|
||||
§ions,
|
||||
§ion_indices,
|
||||
split_meta.as_ref(),
|
||||
config,
|
||||
)?;
|
||||
map_relocations(arch.as_ref(), &obj_file, &mut sections, §ion_indices, &symbol_indices)?;
|
||||
parse_line_info(&obj_file, &mut sections, §ion_indices, data)?;
|
||||
if config.combine_data_sections || config.combine_text_sections {
|
||||
|
@ -56,3 +56,20 @@ fn combine_text_sections() {
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "arm")]
|
||||
fn trim_trailing_hword() {
|
||||
let diff_config = diff::DiffObjConfig::default();
|
||||
let obj = obj::read::parse(
|
||||
include_object!("data/arm/issue_253.o"),
|
||||
&diff_config,
|
||||
diff::DiffSide::Base,
|
||||
)
|
||||
.unwrap();
|
||||
let symbol_idx = obj.symbols.iter().position(|s| s.name == "sub_8030F20").unwrap();
|
||||
let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap();
|
||||
insta::assert_debug_snapshot!(diff.instruction_rows);
|
||||
let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config);
|
||||
insta::assert_snapshot!(output);
|
||||
}
|
||||
|
@ -62,3 +62,21 @@ fn filter_non_matching() {
|
||||
.unwrap();
|
||||
insta::assert_debug_snapshot!(obj.symbols);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "mips")]
|
||||
fn ido_mdebug_line_numbers() {
|
||||
let diff_config = diff::DiffObjConfig::default();
|
||||
let obj = obj::read::parse(
|
||||
include_object!("data/mips/ido_lines_example.o"),
|
||||
&diff_config,
|
||||
diff::DiffSide::Base,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let text_section = obj.sections.iter().find(|s| s.name == ".text").unwrap();
|
||||
assert_eq!(text_section.line_info.get(&0), Some(&6));
|
||||
assert_eq!(text_section.line_info.get(&12), Some(&7));
|
||||
assert_eq!(text_section.line_info.get(&56), Some(&9));
|
||||
assert_eq!(text_section.line_info.len(), 66);
|
||||
}
|
||||
|
BIN
objdiff-core/tests/data/arm/issue_253.o
Normal file
BIN
objdiff-core/tests/data/arm/issue_253.o
Normal file
Binary file not shown.
BIN
objdiff-core/tests/data/mips/ido_lines_example.o
Normal file
BIN
objdiff-core/tests/data/mips/ido_lines_example.o
Normal file
Binary file not shown.
@ -4,4 +4,4 @@ expression: output
|
||||
---
|
||||
[(Line(90), Dim, 5), (Address(0), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r12")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(8), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(90), Dim, 5), (Address(4), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bx", 32779), Normal, 10), (Argument(Opaque("r12")), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(90), Dim, 5), (Address(8), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65535), Normal, 10), (Symbol(Symbol { name: "esEnemyDraw", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Line(90), Dim, 5), (Address(8), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Symbol(Symbol { name: "esEnemyDraw", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
|
@ -36,7 +36,7 @@ expression: diff.instruction_rows
|
||||
InstructionRef {
|
||||
address: 84,
|
||||
size: 4,
|
||||
opcode: 65535,
|
||||
opcode: 65534,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
|
@ -1829,7 +1829,7 @@ expression: diff.instruction_rows
|
||||
InstructionRef {
|
||||
address: 460,
|
||||
size: 4,
|
||||
opcode: 65535,
|
||||
opcode: 65534,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
@ -1843,7 +1843,7 @@ expression: diff.instruction_rows
|
||||
InstructionRef {
|
||||
address: 464,
|
||||
size: 4,
|
||||
opcode: 65535,
|
||||
opcode: 65534,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
@ -1864,7 +1864,7 @@ expression: diff.instruction_rows
|
||||
InstructionRef {
|
||||
address: 468,
|
||||
size: 4,
|
||||
opcode: 65535,
|
||||
opcode: 65534,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
|
@ -107,6 +107,6 @@ expression: output
|
||||
[(Address(408), Dim, 5), (Basic(" ~> "), Rotating(16), 0), (Opcode("mov", 32818), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Unsigned(0)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(412), Dim, 5), (Spacing(4), Normal, 0), (Opcode("strb", 32899), Normal, 10), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(38)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(416), Dim, 5), (Spacing(4), Normal, 0), (Opcode("ldmia", 32793), Normal, 10), (Argument(Opaque("sp")), Normal, 0), (Argument(Opaque("!")), Normal, 0), (Basic(", "), Normal, 0), (Basic("{"), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic("}"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(420), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65535), Normal, 10), (Symbol(Symbol { name: "data_027e103c", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(424), Dim, 5), (Basic(" ~> "), Rotating(8), 0), (Opcode(".word", 65535), Normal, 10), (Symbol(Symbol { name: "data_027e1098", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(428), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65535), Normal, 10), (Symbol(Symbol { name: "gPlayerControl", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(420), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Symbol(Symbol { name: "data_027e103c", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(424), Dim, 5), (Basic(" ~> "), Rotating(8), 0), (Opcode(".word", 65534), Normal, 10), (Symbol(Symbol { name: "data_027e1098", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Address(428), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Symbol(Symbol { name: "gPlayerControl", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global | Weak), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)]
|
||||
|
@ -1512,7 +1512,7 @@ expression: diff.instruction_rows
|
||||
InstructionRef {
|
||||
address: 216,
|
||||
size: 4,
|
||||
opcode: 65535,
|
||||
opcode: 65534,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
@ -1526,7 +1526,7 @@ expression: diff.instruction_rows
|
||||
InstructionRef {
|
||||
address: 220,
|
||||
size: 4,
|
||||
opcode: 65535,
|
||||
opcode: 65534,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
@ -1540,7 +1540,7 @@ expression: diff.instruction_rows
|
||||
InstructionRef {
|
||||
address: 224,
|
||||
size: 4,
|
||||
opcode: 65535,
|
||||
opcode: 65534,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
@ -1554,7 +1554,7 @@ expression: diff.instruction_rows
|
||||
InstructionRef {
|
||||
address: 228,
|
||||
size: 4,
|
||||
opcode: 65535,
|
||||
opcode: 65534,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
@ -1568,7 +1568,7 @@ expression: diff.instruction_rows
|
||||
InstructionRef {
|
||||
address: 232,
|
||||
size: 4,
|
||||
opcode: 65535,
|
||||
opcode: 65534,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
@ -1582,7 +1582,7 @@ expression: diff.instruction_rows
|
||||
InstructionRef {
|
||||
address: 236,
|
||||
size: 4,
|
||||
opcode: 65535,
|
||||
opcode: 65534,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
@ -1596,7 +1596,7 @@ expression: diff.instruction_rows
|
||||
InstructionRef {
|
||||
address: 240,
|
||||
size: 4,
|
||||
opcode: 65535,
|
||||
opcode: 65534,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
|
@ -101,10 +101,10 @@ expression: output
|
||||
[(Line(81), Dim, 5), (Address(208), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bl", 19), Normal, 10), (Symbol(Symbol { name: "HEManager_PopState", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Addend(-4), Bright, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(212), Dim, 5), (Basic(" ~> "), Rotating(0), 0), (Opcode("add", 7), Normal, 10), (Argument(Opaque("sp")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(16)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(214), Dim, 5), (Spacing(4), Normal, 0), (Opcode("pop", 55), Normal, 10), (Basic("{"), Normal, 0), (Argument(Opaque("r3")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r4")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r5")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r6")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("r7")), Normal, 0), (Basic(", "), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic("}"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(216), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65535), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(285)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(220), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65535), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(1192)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(224), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65535), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(7544)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(228), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65535), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(9103)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(232), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65535), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(1930)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(236), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65535), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(4294901760)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(240), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65535), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(9129)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(216), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(285)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(220), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(1192)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(224), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(7544)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(228), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(9103)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(232), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(1930)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(236), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(4294901760)), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Line(86), Dim, 5), (Address(240), Dim, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65534), Normal, 10), (Basic("#"), Normal, 0), (Argument(Unsigned(9129)), Normal, 0), (Eol, Normal, 0)]
|
||||
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_arm.rs
|
||||
expression: output
|
||||
---
|
||||
[(Address(0), Dim, 5), (Spacing(4), Normal, 0), (Opcode("str", 64), Normal, 10), (Argument(Opaque("r1")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(36)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(2), Dim, 5), (Spacing(4), Normal, 0), (Opcode("str", 64), Normal, 10), (Argument(Opaque("r2")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("r0")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(40)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)]
|
||||
[(Address(4), Dim, 5), (Spacing(4), Normal, 0), (Opcode("bx", 23), Normal, 10), (Argument(Opaque("lr")), Normal, 0), (Eol, Normal, 0)]
|
@ -0,0 +1,48 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_arm.rs
|
||||
expression: diff.instruction_rows
|
||||
---
|
||||
[
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 0,
|
||||
size: 2,
|
||||
opcode: 64,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 2,
|
||||
size: 2,
|
||||
opcode: 64,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
InstructionDiffRow {
|
||||
ins_ref: Some(
|
||||
InstructionRef {
|
||||
address: 4,
|
||||
size: 2,
|
||||
opcode: 23,
|
||||
branch_dest: None,
|
||||
},
|
||||
),
|
||||
kind: None,
|
||||
branch_from: None,
|
||||
branch_to: None,
|
||||
arg_diff: [],
|
||||
},
|
||||
]
|
@ -1,12 +1,12 @@
|
||||
---
|
||||
source: objdiff-core/tests/arch_mips.rs
|
||||
assertion_line: 10
|
||||
assertion_line: 12
|
||||
expression: obj
|
||||
---
|
||||
Object {
|
||||
arch: ArchMips {
|
||||
endianness: Little,
|
||||
abi: N32,
|
||||
abi: EABI64,
|
||||
isa_extension: Some(
|
||||
R5900EE,
|
||||
),
|
||||
|
@ -32,6 +32,7 @@ cwdemangle = "1.0"
|
||||
dirs = "6.0"
|
||||
egui = "0.32"
|
||||
egui_extras = "0.32"
|
||||
egui-notify = "0.20"
|
||||
filetime = "0.2"
|
||||
float-ord = "0.3"
|
||||
font-kit = "0.14"
|
||||
|
@ -11,14 +11,15 @@ use std::{
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use egui::text::LayoutJob;
|
||||
use filetime::FileTime;
|
||||
use globset::Glob;
|
||||
use objdiff_core::{
|
||||
build::watcher::{Watcher, create_watcher},
|
||||
config::{
|
||||
ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, build_globset,
|
||||
default_ignore_patterns, default_watch_patterns, path::platform_path_serde_option,
|
||||
save_project_config,
|
||||
ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, apply_project_options,
|
||||
build_globset, default_ignore_patterns, default_watch_patterns,
|
||||
path::platform_path_serde_option, save_project_config,
|
||||
},
|
||||
diff::DiffObjConfig,
|
||||
jobs::{Job, JobQueue, JobResult},
|
||||
@ -164,7 +165,7 @@ pub struct AppState {
|
||||
pub selecting_left: Option<String>,
|
||||
/// The left object symbol name that we're selecting a right symbol for
|
||||
pub selecting_right: Option<String>,
|
||||
pub config_error: Option<String>,
|
||||
pub top_left_toasts: egui_notify::Toasts,
|
||||
}
|
||||
|
||||
impl Default for AppState {
|
||||
@ -183,11 +184,23 @@ impl Default for AppState {
|
||||
last_mod_check: Instant::now(),
|
||||
selecting_left: None,
|
||||
selecting_right: None,
|
||||
config_error: None,
|
||||
top_left_toasts: create_toasts(egui_notify::Anchor::TopLeft),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_toasts(anchor: egui_notify::Anchor) -> egui_notify::Toasts {
|
||||
egui_notify::Toasts::default()
|
||||
.with_anchor(anchor)
|
||||
.with_margin(egui::vec2(10.0, 32.0))
|
||||
.with_shadow(egui::Shadow {
|
||||
offset: [0, 0],
|
||||
blur: 0,
|
||||
spread: 1,
|
||||
color: egui::Color32::GRAY,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Deserialize, serde::Serialize)]
|
||||
pub struct AppConfig {
|
||||
// TODO: https://github.com/ron-rs/ron/pull/455
|
||||
@ -321,6 +334,24 @@ impl AppState {
|
||||
self.selecting_right = None;
|
||||
}
|
||||
|
||||
pub fn effective_diff_config(&self) -> DiffObjConfig {
|
||||
let mut config = self.config.diff_obj_config.clone();
|
||||
if let Some(project) = self.current_project_config.as_ref() {
|
||||
if let Some(options) = project.options.as_ref() {
|
||||
// Ignore errors here, we display them when loading the project config
|
||||
let _ = apply_project_options(&mut config, options);
|
||||
}
|
||||
if let Some(selected) = self.config.selected_obj.as_ref()
|
||||
&& let Some(units) = project.units.as_deref()
|
||||
&& let Some(unit) = units.iter().find(|unit| unit.name() == selected.name)
|
||||
&& let Some(options) = unit.options()
|
||||
{
|
||||
let _ = apply_project_options(&mut config, options);
|
||||
}
|
||||
}
|
||||
config
|
||||
}
|
||||
|
||||
pub fn set_selecting_left(&mut self, right: &str) {
|
||||
let Some(object) = self.config.selected_obj.as_mut() else {
|
||||
return;
|
||||
@ -401,11 +432,22 @@ impl AppState {
|
||||
match save_project_config(config, info) {
|
||||
Ok(new_info) => *info = new_info,
|
||||
Err(e) => {
|
||||
log::error!("Failed to save project config: {e}");
|
||||
self.config_error = Some(format!("Failed to save project config: {e}"));
|
||||
log::error!("Failed to save project config: {e:#}");
|
||||
self.show_error_toast("Failed to save project config", &e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_error_toast(&mut self, context: &str, e: &anyhow::Error) {
|
||||
let mut job = LayoutJob::default();
|
||||
job.append(context, 0.0, Default::default());
|
||||
job.append("\n", 0.0, Default::default());
|
||||
job.append(&format!("{e:#}"), 0.0, egui::TextFormat {
|
||||
color: egui::Color32::LIGHT_RED,
|
||||
..Default::default()
|
||||
});
|
||||
self.top_left_toasts.error(job).closable(true).duration(None);
|
||||
}
|
||||
}
|
||||
|
||||
pub type AppStateRef = Arc<RwLock<AppState>>;
|
||||
@ -548,12 +590,9 @@ impl App {
|
||||
|
||||
if state.config_change {
|
||||
state.config_change = false;
|
||||
match load_project_config(state) {
|
||||
Ok(()) => state.config_error = None,
|
||||
Err(e) => {
|
||||
log::error!("Failed to load project config: {e}");
|
||||
state.config_error = Some(e.to_string());
|
||||
}
|
||||
if let Err(e) = load_project_config(state) {
|
||||
log::error!("Failed to load project config: {e:#}");
|
||||
state.show_error_toast("Failed to load project config", &e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -579,7 +618,10 @@ impl App {
|
||||
.map_err(anyhow::Error::new)
|
||||
}) {
|
||||
Ok(watcher) => self.watcher = Some(watcher),
|
||||
Err(e) => log::error!("Failed to create watcher: {e}"),
|
||||
Err(e) => {
|
||||
log::error!("Failed to create watcher: {e:#}");
|
||||
state.show_error_toast("Failed to create file watcher", &e);
|
||||
}
|
||||
}
|
||||
state.watcher_change = false;
|
||||
}
|
||||
@ -806,7 +848,7 @@ impl eframe::App for App {
|
||||
let mut action = None;
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
let state = state.read().unwrap();
|
||||
action = diff_view_ui(ui, diff_state, appearance, &state.config.diff_obj_config);
|
||||
action = diff_view_ui(ui, diff_state, appearance, &state.effective_diff_config());
|
||||
});
|
||||
|
||||
project_window(ctx, state, show_project_config, config_state, appearance);
|
||||
@ -818,6 +860,10 @@ impl eframe::App for App {
|
||||
graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance);
|
||||
jobs_window(ctx, show_jobs, jobs, appearance);
|
||||
|
||||
if let Ok(mut state) = self.state.write() {
|
||||
state.top_left_toasts.show(ctx);
|
||||
}
|
||||
|
||||
self.post_update(ctx, action);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,11 @@
|
||||
use anyhow::Result;
|
||||
use globset::Glob;
|
||||
use objdiff_core::config::{default_ignore_patterns, default_watch_patterns, try_project_config};
|
||||
use objdiff_core::{
|
||||
config::{
|
||||
apply_project_options, default_ignore_patterns, default_watch_patterns, try_project_config,
|
||||
},
|
||||
diff::DiffObjConfig,
|
||||
};
|
||||
use typed_path::{Utf8UnixComponent, Utf8UnixPath};
|
||||
|
||||
use crate::app::{AppState, ObjectConfig};
|
||||
@ -124,6 +129,38 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> {
|
||||
state.object_nodes = build_nodes(&mut state.objects);
|
||||
state.current_project_config = Some(project_config);
|
||||
state.project_config_info = Some(info);
|
||||
if let Some(options) =
|
||||
state.current_project_config.as_ref().and_then(|project| project.options.as_ref())
|
||||
{
|
||||
let mut diff_config = DiffObjConfig::default();
|
||||
if let Err(e) = apply_project_options(&mut diff_config, options) {
|
||||
log::error!("Failed to apply project config options: {e:#}");
|
||||
state.show_error_toast("Failed to apply project config options", &e);
|
||||
}
|
||||
}
|
||||
if let Some(project) = state.current_project_config.as_ref()
|
||||
&& let Some(units) = project.units.as_deref()
|
||||
{
|
||||
let mut unit_option_errors = Vec::new();
|
||||
for unit in units {
|
||||
if let Some(options) = unit.options() {
|
||||
let mut diff_config = DiffObjConfig::default();
|
||||
if let Err(e) = apply_project_options(&mut diff_config, options) {
|
||||
unit_option_errors.push((unit.name().to_string(), e));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (unit_name, error) in unit_option_errors {
|
||||
log::error!(
|
||||
"Failed to apply project config options for unit {}: {error:#}",
|
||||
unit_name
|
||||
);
|
||||
state.show_error_toast(
|
||||
&format!("Failed to apply project config options for unit {unit_name}"),
|
||||
&error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Reload selected object
|
||||
if let Some(selected_obj) = &state.config.selected_obj {
|
||||
|
@ -106,7 +106,7 @@ pub fn create_objdiff_config(state: &AppState) -> objdiff::ObjDiffConfig {
|
||||
.as_ref()
|
||||
.and_then(|obj| obj.base_path.as_ref())
|
||||
.cloned(),
|
||||
diff_obj_config: state.config.diff_obj_config.clone(),
|
||||
diff_obj_config: state.effective_diff_config(),
|
||||
mapping_config: MappingConfig {
|
||||
mappings: state
|
||||
.config
|
||||
|
@ -519,8 +519,7 @@ fn format_path(path: &Option<Utf8PlatformPathBuf>, appearance: &Appearance) -> R
|
||||
RichText::new(text).color(color).family(FontFamily::Monospace)
|
||||
}
|
||||
|
||||
pub const CONFIG_DISABLED_TEXT: &str =
|
||||
"Option disabled because it's set by the project configuration file.";
|
||||
pub const CONFIG_DISABLED_TEXT: &str = "Value is overridden by the project configuration.";
|
||||
|
||||
fn pick_folder_ui(
|
||||
ui: &mut egui::Ui,
|
||||
@ -533,8 +532,13 @@ fn pick_folder_ui(
|
||||
let response = ui.horizontal(|ui| {
|
||||
subheading(ui, label, appearance);
|
||||
ui.link(HELP_ICON).on_hover_ui(tooltip);
|
||||
ui.add_enabled(enabled, egui::Button::new("Select"))
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT)
|
||||
let button = ui
|
||||
.add_enabled(enabled, egui::Button::new("Select"))
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
|
||||
if !enabled {
|
||||
project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT);
|
||||
}
|
||||
button
|
||||
});
|
||||
ui.label(format_path(dir, appearance));
|
||||
response.inner
|
||||
@ -552,17 +556,6 @@ pub fn project_window(
|
||||
egui::Window::new("Project").open(show).show(ctx, |ui| {
|
||||
split_obj_config_ui(ui, &mut state_guard, config_state, appearance);
|
||||
});
|
||||
|
||||
if let Some(error) = &state_guard.config_error {
|
||||
let mut open = true;
|
||||
egui::Window::new("Error").open(&mut open).show(ctx, |ui| {
|
||||
ui.label("Failed to load project config:");
|
||||
ui.colored_label(appearance.delete_color, error);
|
||||
});
|
||||
if !open {
|
||||
state_guard.config_error = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn split_obj_config_ui(
|
||||
@ -623,6 +616,9 @@ fn split_obj_config_ui(
|
||||
job.append(".", 0.0, text_format.clone());
|
||||
ui.label(job);
|
||||
});
|
||||
if state.project_config_info.is_some() {
|
||||
project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT);
|
||||
}
|
||||
});
|
||||
let mut custom_make_str = state.config.custom_make.clone().unwrap_or_default();
|
||||
if ui
|
||||
@ -831,6 +827,9 @@ fn patterns_ui(
|
||||
*patterns = on_reset();
|
||||
change = true;
|
||||
}
|
||||
if has_project_config {
|
||||
project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT);
|
||||
}
|
||||
});
|
||||
let mut remove_at: Option<usize> = None;
|
||||
for (idx, glob) in patterns.iter().enumerate() {
|
||||
@ -885,20 +884,64 @@ pub fn arch_config_window(
|
||||
});
|
||||
}
|
||||
|
||||
fn project_override_badge(ui: &mut egui::Ui) -> egui::Response {
|
||||
ui.add(egui::Label::new(RichText::new("⛭").color(ui.visuals().warn_fg_color)).selectable(false))
|
||||
}
|
||||
|
||||
fn config_property_ui(
|
||||
ui: &mut egui::Ui,
|
||||
state: &mut AppState,
|
||||
property_id: ConfigPropertyId,
|
||||
) -> bool {
|
||||
let mut changed = false;
|
||||
let current_value = state.config.diff_obj_config.get_property_value(property_id);
|
||||
match (property_id.kind(), current_value) {
|
||||
(ConfigPropertyKind::Boolean, ConfigPropertyValue::Boolean(mut checked)) => {
|
||||
let mut response = ui.checkbox(&mut checked, property_id.name());
|
||||
if let Some(description) = property_id.description() {
|
||||
response = response.on_hover_text(description);
|
||||
}
|
||||
if response.changed() {
|
||||
let is_overridden = state.current_project_config.as_ref().is_some_and(|config| {
|
||||
let key = property_id.name();
|
||||
if let Some(selected) = state.config.selected_obj.as_ref()
|
||||
&& let Some(units) = config.units.as_deref()
|
||||
&& let Some(unit) = units.iter().find(|unit| unit.name() == selected.name)
|
||||
&& let Some(options) = unit.options()
|
||||
&& options.contains_key(key)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if let Some(options) = config.options.as_ref()
|
||||
&& options.contains_key(key)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
false
|
||||
});
|
||||
let override_value =
|
||||
is_overridden.then(|| state.effective_diff_config().get_property_value(property_id));
|
||||
let base_value = state.config.diff_obj_config.get_property_value(property_id);
|
||||
match (property_id.kind(), base_value, override_value) {
|
||||
(
|
||||
ConfigPropertyKind::Boolean,
|
||||
ConfigPropertyValue::Boolean(base_checked),
|
||||
override_value,
|
||||
) => {
|
||||
let mut checked = match override_value {
|
||||
Some(ConfigPropertyValue::Boolean(value)) => value,
|
||||
_ => base_checked,
|
||||
};
|
||||
let response = ui
|
||||
.horizontal(|ui| {
|
||||
let mut response = ui
|
||||
.add_enabled(
|
||||
!is_overridden,
|
||||
egui::widgets::Checkbox::new(&mut checked, property_id.name()),
|
||||
)
|
||||
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
|
||||
if let Some(description) = property_id.description() {
|
||||
response = response.on_hover_text(description);
|
||||
}
|
||||
if is_overridden {
|
||||
project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT);
|
||||
}
|
||||
response
|
||||
})
|
||||
.inner;
|
||||
if !is_overridden && response.changed() {
|
||||
state
|
||||
.config
|
||||
.diff_obj_config
|
||||
@ -907,7 +950,11 @@ fn config_property_ui(
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
(ConfigPropertyKind::Choice(variants), ConfigPropertyValue::Choice(selected)) => {
|
||||
(
|
||||
ConfigPropertyKind::Choice(variants),
|
||||
ConfigPropertyValue::Choice(base_selected),
|
||||
override_value,
|
||||
) => {
|
||||
fn variant_name(variant: &ConfigEnumVariantInfo) -> String {
|
||||
if variant.is_default {
|
||||
format!("{} (default)", variant.name)
|
||||
@ -915,36 +962,51 @@ fn config_property_ui(
|
||||
variant.name.to_string()
|
||||
}
|
||||
}
|
||||
let display_selected = match override_value {
|
||||
Some(ConfigPropertyValue::Choice(value)) => value,
|
||||
_ => base_selected,
|
||||
};
|
||||
let selected_variant = variants
|
||||
.iter()
|
||||
.find(|v| v.value == selected)
|
||||
.find(|v| v.value == display_selected)
|
||||
.or_else(|| variants.iter().find(|v| v.is_default))
|
||||
.expect("Invalid choice variant");
|
||||
let response = egui::ComboBox::new(property_id.name(), property_id.name())
|
||||
.selected_text(variant_name(selected_variant))
|
||||
.show_ui(ui, |ui| {
|
||||
for variant in variants {
|
||||
let mut response =
|
||||
ui.selectable_label(selected == variant.value, variant_name(variant));
|
||||
if let Some(description) = variant.description {
|
||||
response = response.on_hover_text(description);
|
||||
}
|
||||
if response.clicked() {
|
||||
state
|
||||
.config
|
||||
.diff_obj_config
|
||||
.set_property_value(
|
||||
property_id,
|
||||
ConfigPropertyValue::Choice(variant.value),
|
||||
)
|
||||
.expect("Failed to set property value");
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
})
|
||||
.response;
|
||||
if let Some(description) = property_id.description() {
|
||||
response.on_hover_text(description);
|
||||
let mut new_value: Option<&'static str> = None;
|
||||
ui.horizontal(|ui| {
|
||||
let inner = ui.add_enabled_ui(!is_overridden, |ui| {
|
||||
egui::ComboBox::new(property_id.name(), property_id.name())
|
||||
.selected_text(variant_name(selected_variant))
|
||||
.show_ui(ui, |ui| {
|
||||
for variant in variants {
|
||||
let mut response = ui.selectable_label(
|
||||
display_selected == variant.value,
|
||||
variant_name(variant),
|
||||
);
|
||||
if let Some(description) = variant.description {
|
||||
response = response.on_hover_text(description);
|
||||
}
|
||||
if response.clicked() {
|
||||
new_value = Some(variant.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
let mut response = inner.response.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
|
||||
if let Some(description) = property_id.description() {
|
||||
response = response.on_hover_text(description);
|
||||
}
|
||||
if is_overridden {
|
||||
project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT);
|
||||
}
|
||||
response
|
||||
});
|
||||
if !is_overridden && let Some(value) = new_value {
|
||||
state
|
||||
.config
|
||||
.diff_obj_config
|
||||
.set_property_value(property_id, ConfigPropertyValue::Choice(value))
|
||||
.expect("Failed to set property value");
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
_ => panic!("Incompatible property kind and value"),
|
||||
|
@ -1,10 +1,12 @@
|
||||
use egui::TextStyle;
|
||||
use objdiff_core::diff::{ConfigEnum, Demangler};
|
||||
|
||||
use crate::views::appearance::Appearance;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DemangleViewState {
|
||||
pub text: String,
|
||||
pub demangler: Demangler,
|
||||
}
|
||||
|
||||
pub fn demangle_window(
|
||||
@ -14,9 +16,23 @@ pub fn demangle_window(
|
||||
appearance: &Appearance,
|
||||
) {
|
||||
egui::Window::new("Demangle").open(show).show(ctx, |ui| {
|
||||
egui::ComboBox::from_label("Demangler")
|
||||
.selected_text(state.demangler.name().to_string())
|
||||
.show_ui(ui, |ui| {
|
||||
for demangler in Demangler::variants() {
|
||||
if *demangler != Demangler::None {
|
||||
let response =
|
||||
ui.selectable_value(&mut state.demangler, *demangler, demangler.name());
|
||||
if let Some(description) = demangler.description() {
|
||||
response.on_hover_text(description);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.separator();
|
||||
ui.text_edit_singleline(&mut state.text);
|
||||
ui.add_space(10.0);
|
||||
if let Some(demangled) = cwdemangle::demangle(&state.text, &Default::default()) {
|
||||
if let Some(demangled) = state.demangler.demangle(&state.text) {
|
||||
ui.scope(|ui| {
|
||||
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
|
||||
ui.colored_label(appearance.replace_color, &demangled);
|
||||
|
4
objdiff-wasm/package-lock.json
generated
4
objdiff-wasm/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "objdiff-wasm",
|
||||
"version": "3.2.1",
|
||||
"version": "3.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "objdiff-wasm",
|
||||
"version": "3.2.1",
|
||||
"version": "3.3.0",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.3",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "objdiff-wasm",
|
||||
"version": "3.2.1",
|
||||
"version": "3.3.0",
|
||||
"description": "A local diffing tool for decompilation projects.",
|
||||
"author": {
|
||||
"name": "Luke Street",
|
||||
|
Loading…
x
Reference in New Issue
Block a user