Compare commits

..

13 Commits

Author SHA1 Message Date
e202c3ef95 Version v2.7.1 2025-01-21 22:59:20 -07:00
LagoLunatic
b7730b3d00 Refactor data relocation diffing to improve accuracy and fix bugs (#157)
* Data reloc hover tooltip: Show relocation source address

* Refactor data relocation diffing to improve accuracy and fix bugs
2025-01-21 22:54:31 -07:00
LagoLunatic
a4fdb61f04 Implement diffing relocations within data sections (#154)
* Data view: Show data bytes with differing relocations as a diff

* Data view: Show differing relocations on hover

* Symbol list view: Adjust symbol/section match %s when relocations differ

* Improve data reloc diffing logic

* Don't make reloc diffs cause bytes to show as red or green

* Properly detect byte size of each relocation

* Data view: Add context menu for copying relocation target symbols

* Also show already-matching relocations on hover/right click

* Change font color for nonmatching relocs on hover
2025-01-18 16:20:07 -07:00
LagoLunatic
2876be37a3 Show relocation diffs in function view when the data's content differs (#153)
* Show reloc diff in func view when data content differs

* Add "Relax shifted data diffs" option

* Display fake pool relocations at end of line

* Diff reloc data by display string instead of raw bytes

This is to handle data symbols that contain multiple values in them at once, such as stringBase. If you compare the target symbol's bytes directly, then any part of the symbol having different bytes will cause *all* relocations to that symbol to show as a diff, even if the specific string being accessed is the same.

* Fix weak stripped symbols showing as a false diff

Fixed this by showing extern symbols correctly instead of skipping them.

* Add "Relax shifted data diffs" option to objdiff-cli

Includes both a command line argument and a keyboard shortcut (S).

* Remove addi string data hack and ... pool name hack

* Clippy fix

* PPC: Clear relocs from GPRs when overwritten

* PPC: Follow branches to improve pool detection accuracy

* PPC: Handle following bctr jump table control flow

* Clippy fixes

* PPC: Fix extern relocations not having their addend copied

* Add option to disable func data value diffing

* PPC: Handle lmw when clearing GPRs

* PPC: Handle moving reloc address with `add` inst

* Combine "relax reloc diffs" with other reloc diff options

* Add v3 config and migrate from v2

---------

Co-authored-by: Luke Street <luke@street.dev>
2025-01-18 16:18:05 -07:00
11171763eb Use cargo-deny-action@v2 2025-01-18 16:16:12 -07:00
6037a79ba2 Update all dependencies 2025-01-18 15:58:38 -07:00
f7efe5fdff cargo update 2025-01-04 21:29:29 -07:00
0692deac59 Use ObjInsArgValue::loose_eq in arg_eq 2025-01-04 21:02:54 -07:00
c3e3d175c5 Create schema for diff config properties 2025-01-04 21:02:54 -07:00
c45f4bbc99 Diff schema updates & WASM updates 2025-01-04 21:02:54 -07:00
b0c5431ac5 Add version to notify deps 2025-01-04 21:02:54 -07:00
LagoLunatic
9ab246367b Add buttons to collapse or expand all sections in the symbol list view for an object simultaneously (#149)
* Add buttons to expand/collapse all sections to symbol list view

* Add buttons to expand/collapse all sections to the split view
2025-01-01 20:48:25 -07:00
NWPlayer123
dcafe51eda Update Dependencies (#150)
* Update Dependencies

* Fix non-WGPU builds

---------

Co-authored-by: NWPlayer123 <NWPlayer123@users.noreply.github.com>
2025-01-01 20:45:48 -07:00
41 changed files with 2663 additions and 1184 deletions

View File

@@ -65,7 +65,7 @@ jobs:
continue-on-error: ${{ matrix.checks == 'advisories' }} continue-on-error: ${{ matrix.checks == 'advisories' }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1 - uses: EmbarkStudios/cargo-deny-action@v2
with: with:
command: check ${{ matrix.checks }} command: check ${{ matrix.checks }}

938
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,9 +13,9 @@ strip = "debuginfo"
codegen-units = 1 codegen-units = 1
[workspace.package] [workspace.package]
version = "2.5.0" version = "2.7.1"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
edition = "2021" edition = "2021"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff" repository = "https://github.com/encounter/objdiff"
rust-version = "1.74" rust-version = "1.81"

View File

@@ -73,7 +73,6 @@ ignore = [
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" }, #{ 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 #"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" }, #{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained indirect dependency" },
] ]
# If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library. # If this is false, then it uses a built-in git library.
@@ -240,7 +239,7 @@ allow-git = []
[sources.allow-org] [sources.allow-org]
# github.com organizations to allow git sources for # github.com organizations to allow git sources for
github = ["notify-rs"] github = []
# gitlab.com organizations to allow git sources for # gitlab.com organizations to allow git sources for
gitlab = [] gitlab = []
# bitbucket.org organizations to allow git sources for # bitbucket.org organizations to allow git sources for

View File

@@ -12,7 +12,7 @@ use std::{
time::Duration, time::Duration,
}; };
use anyhow::{bail, Context, Result}; use anyhow::{anyhow, bail, Context, Result};
use argp::FromArgs; use argp::FromArgs;
use crossterm::{ use crossterm::{
event, event,
@@ -27,9 +27,11 @@ use objdiff_core::{
watcher::{create_watcher, Watcher}, watcher::{create_watcher, Watcher},
BuildConfig, BuildConfig,
}, },
config::{build_globset, default_watch_patterns, ProjectConfig, ProjectObject}, config::{build_globset, ProjectConfig, ProjectObject},
diff, diff,
diff::ObjDiff, diff::{
ConfigEnum, ConfigPropertyId, ConfigPropertyKind, DiffObjConfig, MappingConfig, ObjDiff,
},
jobs::{ jobs::{
objdiff::{start_build, ObjDiffConfig}, objdiff::{start_build, ObjDiffConfig},
Job, JobQueue, JobResult, Job, JobQueue, JobResult,
@@ -63,9 +65,6 @@ pub struct Args {
#[argp(option, short = 'u')] #[argp(option, short = 'u')]
/// Unit name within project /// Unit name within project
unit: Option<String>, unit: Option<String>,
#[argp(switch, short = 'x')]
/// Relax relocation diffs
relax_reloc_diffs: bool,
#[argp(option, short = 'o')] #[argp(option, short = 'o')]
/// Output file (one-shot mode) ("-" for stdout) /// Output file (one-shot mode) ("-" for stdout)
output: Option<PathBuf>, output: Option<PathBuf>,
@@ -75,6 +74,18 @@ pub struct Args {
#[argp(positional)] #[argp(positional)]
/// Function symbol to diff /// Function symbol to diff
symbol: Option<String>, symbol: Option<String>,
#[argp(option, short = 'c')]
/// Configuration property (key=value)
config: Vec<String>,
#[argp(option, short = 'm')]
/// Symbol mapping (target=base)
mapping: Vec<String>,
#[argp(option)]
/// Left symbol name for selection
selecting_left: Option<String>,
#[argp(option)]
/// Right symbol name for selection
selecting_right: Option<String>,
} }
pub fn run(args: Args) -> Result<()> { pub fn run(args: Args) -> Result<()> {
@@ -84,7 +95,9 @@ pub fn run(args: Args) -> Result<()> {
&args.project, &args.project,
&args.unit, &args.unit,
) { ) {
(Some(t), Some(b), None, None) => (Some(t.clone()), Some(b.clone()), None), (Some(_), Some(_), None, None)
| (Some(_), None, None, None)
| (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None),
(None, None, p, u) => { (None, None, p, u) => {
let project = match p { let project = match p {
Some(project) => project.clone(), Some(project) => project.clone(),
@@ -193,6 +206,43 @@ pub fn run(args: Args) -> Result<()> {
} }
} }
fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)> {
let mut diff_config = DiffObjConfig::default();
for config in &args.config {
let (key, value) = config.split_once('=').context("--config expects \"key=value\"")?;
let property_id = ConfigPropertyId::from_str(key)
.map_err(|()| anyhow!("Invalid configuration property: {}", key))?;
diff_config.set_property_value_str(property_id, value).map_err(|()| {
let mut options = String::new();
match property_id.kind() {
ConfigPropertyKind::Boolean => {
options = "true, false".to_string();
}
ConfigPropertyKind::Choice(variants) => {
for (i, variant) in variants.iter().enumerate() {
if i > 0 {
options.push_str(", ");
}
options.push_str(variant.value);
}
}
}
anyhow!("Invalid value for {}. Expected one of: {}", property_id.name(), options)
})?;
}
let mut mapping_config = MappingConfig {
mappings: Default::default(),
selecting_left: args.selecting_left.clone(),
selecting_right: args.selecting_right.clone(),
};
for mapping in &args.mapping {
let (target, base) =
mapping.split_once('=').context("--mapping expects \"target=base\"")?;
mapping_config.mappings.insert(target.to_string(), base.to_string());
}
Ok((diff_config, mapping_config))
}
fn run_oneshot( fn run_oneshot(
args: &Args, args: &Args,
output: &Path, output: &Path,
@@ -200,17 +250,19 @@ fn run_oneshot(
base_path: Option<&Path>, base_path: Option<&Path>,
) -> Result<()> { ) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?; let output_format = OutputFormat::from_option(args.format.as_deref())?;
let config = diff::DiffObjConfig { let (diff_config, mapping_config) = build_config_from_args(args)?;
relax_reloc_diffs: args.relax_reloc_diffs,
..Default::default() // TODO
};
let target = target_path let target = target_path
.map(|p| obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display()))) .map(|p| {
obj::read::read(p, &diff_config).with_context(|| format!("Loading {}", p.display()))
})
.transpose()?; .transpose()?;
let base = base_path let base = base_path
.map(|p| obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display()))) .map(|p| {
obj::read::read(p, &diff_config).with_context(|| format!("Loading {}", p.display()))
})
.transpose()?; .transpose()?;
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?; let result =
diff::diff_objs(&diff_config, &mapping_config, target.as_ref(), base.as_ref(), None)?;
let left = target.as_ref().and_then(|o| result.left.as_ref().map(|d| (o, d))); let left = target.as_ref().and_then(|o| result.left.as_ref().map(|d| (o, d)));
let right = base.as_ref().and_then(|o| result.right.as_ref().map(|d| (o, d))); let right = base.as_ref().and_then(|o| result.right.as_ref().map(|d| (o, d)));
write_output(&DiffResult::new(left, right), Some(output), output_format)?; write_output(&DiffResult::new(left, right), Some(output), output_format)?;
@@ -229,9 +281,10 @@ pub struct AppState {
pub prev_obj: Option<(ObjInfo, ObjDiff)>, pub prev_obj: Option<(ObjInfo, ObjDiff)>,
pub reload_time: Option<time::OffsetDateTime>, pub reload_time: Option<time::OffsetDateTime>,
pub time_format: Vec<time::format_description::FormatItem<'static>>, pub time_format: Vec<time::format_description::FormatItem<'static>>,
pub relax_reloc_diffs: bool,
pub watcher: Option<Watcher>, pub watcher: Option<Watcher>,
pub modified: Arc<AtomicBool>, pub modified: Arc<AtomicBool>,
pub diff_obj_config: DiffObjConfig,
pub mapping_config: MappingConfig,
} }
fn create_objdiff_config(state: &AppState) -> ObjDiffConfig { fn create_objdiff_config(state: &AppState) -> ObjDiffConfig {
@@ -257,13 +310,8 @@ fn create_objdiff_config(state: &AppState) -> ObjDiffConfig {
.is_some_and(|p| p.build_target.unwrap_or(false)), .is_some_and(|p| p.build_target.unwrap_or(false)),
target_path: state.target_path.clone(), target_path: state.target_path.clone(),
base_path: state.base_path.clone(), base_path: state.base_path.clone(),
diff_obj_config: diff::DiffObjConfig { diff_obj_config: state.diff_obj_config.clone(),
relax_reloc_diffs: state.relax_reloc_diffs, mapping_config: state.mapping_config.clone(),
..Default::default() // TODO
},
symbol_mappings: Default::default(),
selecting_left: None,
selecting_right: None,
} }
} }
@@ -314,6 +362,7 @@ fn run_interactive(
let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") }; 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]") let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]")
.context("Failed to parse time format")?; .context("Failed to parse time format")?;
let (diff_obj_config, mapping_config) = build_config_from_args(&args)?;
let mut state = AppState { let mut state = AppState {
jobs: Default::default(), jobs: Default::default(),
waker: Default::default(), waker: Default::default(),
@@ -326,17 +375,13 @@ fn run_interactive(
prev_obj: None, prev_obj: None,
reload_time: None, reload_time: None,
time_format, time_format,
relax_reloc_diffs: args.relax_reloc_diffs,
watcher: None, watcher: None,
modified: Default::default(), modified: Default::default(),
diff_obj_config,
mapping_config,
}; };
if let Some(project_dir) = &state.project_dir { if let (Some(project_dir), Some(project_config)) = (&state.project_dir, &state.project_config) {
let watch_patterns = state let watch_patterns = project_config.build_watch_patterns()?;
.project_config
.as_ref()
.and_then(|c| c.watch_patterns.as_ref())
.cloned()
.unwrap_or_else(default_watch_patterns);
state.watcher = Some(create_watcher( state.watcher = Some(create_watcher(
state.modified.clone(), state.modified.clone(),
project_dir, project_dir,

View File

@@ -169,22 +169,29 @@ fn report_object(
} }
_ => {} _ => {}
} }
let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() }; let diff_config = diff::DiffObjConfig {
function_reloc_diffs: diff::FunctionRelocDiffs::None,
..Default::default()
};
let mapping_config = diff::MappingConfig::default();
let target = object let target = object
.target_path .target_path
.as_ref() .as_ref()
.map(|p| { .map(|p| {
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display())) obj::read::read(p, &diff_config)
.with_context(|| format!("Failed to open {}", p.display()))
}) })
.transpose()?; .transpose()?;
let base = object let base = object
.base_path .base_path
.as_ref() .as_ref()
.map(|p| { .map(|p| {
obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display())) obj::read::read(p, &diff_config)
.with_context(|| format!("Failed to open {}", p.display()))
}) })
.transpose()?; .transpose()?;
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?; let result =
diff::diff_objs(&diff_config, &mapping_config, target.as_ref(), base.as_ref(), None)?;
let metadata = ReportUnitMetadata { let metadata = ReportUnitMetadata {
complete: object.complete(), complete: object.complete(),

View File

@@ -3,7 +3,7 @@ use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton,
use objdiff_core::{ use objdiff_core::{
diff::{ diff::{
display::{display_diff, DiffText, HighlightKind}, display::{display_diff, DiffText, HighlightKind},
ObjDiff, ObjInsDiffKind, ObjSymbolDiff, FunctionRelocDiffs, ObjDiff, ObjInsDiffKind, ObjSymbolDiff,
}, },
obj::{ObjInfo, ObjSectionKind, ObjSymbol, SymbolRef}, obj::{ObjInfo, ObjSectionKind, ObjSymbol, SymbolRef},
}; };
@@ -368,9 +368,15 @@ impl UiView for FunctionDiffUi {
self.scroll_x = self.scroll_x.saturating_sub(1); self.scroll_x = self.scroll_x.saturating_sub(1);
result.redraw = true; result.redraw = true;
} }
// Toggle relax relocation diffs // Cycle through function relocation diff mode
KeyCode::Char('x') => { KeyCode::Char('x') => {
state.relax_reloc_diffs = !state.relax_reloc_diffs; state.diff_obj_config.function_reloc_diffs =
match state.diff_obj_config.function_reloc_diffs {
FunctionRelocDiffs::None => FunctionRelocDiffs::NameAddress,
FunctionRelocDiffs::NameAddress => FunctionRelocDiffs::DataValue,
FunctionRelocDiffs::DataValue => FunctionRelocDiffs::All,
FunctionRelocDiffs::All => FunctionRelocDiffs::None,
};
result.redraw = true; result.redraw = true;
return EventControlFlow::Reload; return EventControlFlow::Reload;
} }

View File

@@ -16,18 +16,104 @@ documentation = "https://docs.rs/objdiff-core"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features] [features]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "arm64", "bindings", "build"] all = [
any-arch = ["config", "dep:bimap", "dep:strum", "dep:similar", "dep:flagset", "dep:log", "dep:memmap2", "dep:byteorder", "dep:num-traits"] # Implicit, used to check if any arch is enabled # Features
bindings = ["dep:serde_json", "dep:prost", "dep:pbjson", "dep:serde", "dep:prost-build", "dep:pbjson-build"] "bindings",
build = ["dep:shell-escape", "dep:path-slash", "dep:winapi", "dep:notify", "dep:notify-debouncer-full", "dep:reqwest", "dep:self_update", "dep:tempfile", "dep:time"] "build",
config = ["dep:bimap", "dep:globset", "dep:semver", "dep:serde_json", "dep:serde_yaml", "dep:serde", "dep:filetime"] "config",
"dwarf",
# Architectures
"mips",
"ppc",
"x86",
"arm",
"arm64",
]
# Implicit, used to check if any arch is enabled
any-arch = [
"config",
"dep:bimap",
"dep:byteorder",
"dep:flagset",
"dep:heck",
"dep:log",
"dep:memmap2",
"dep:num-traits",
"dep:prettyplease",
"dep:proc-macro2",
"dep:quote",
"dep:serde",
"dep:serde_json",
"dep:similar",
"dep:strum",
"dep:syn",
]
bindings = [
"dep:pbjson",
"dep:pbjson-build",
"dep:prost",
"dep:prost-build",
"dep:serde",
"dep:serde_json",
]
build = [
"dep:notify",
"dep:notify-debouncer-full",
"dep:path-slash",
"dep:reqwest",
"dep:self_update",
"dep:shell-escape",
"dep:tempfile",
"dep:time",
"dep:winapi",
]
config = [
"dep:bimap",
"dep:filetime",
"dep:globset",
"dep:semver",
"dep:serde",
"dep:serde_json",
"dep:serde_yaml",
]
dwarf = ["dep:gimli"] dwarf = ["dep:gimli"]
mips = ["any-arch", "dep:rabbitizer"] mips = [
ppc = ["any-arch", "dep:cwdemangle", "dep:cwextab", "dep:ppc750cl"] "any-arch",
x86 = ["any-arch", "dep:cpp_demangle", "dep:iced-x86", "dep:msvc-demangler"] "dep:rabbitizer",
arm = ["any-arch", "dep:cpp_demangle", "dep:unarm", "dep:arm-attr"] ]
arm64 = ["any-arch", "dep:cpp_demangle", "dep:yaxpeax-arch", "dep:yaxpeax-arm"] ppc = [
wasm = ["bindings", "any-arch", "dep:console_error_panic_hook", "dep:console_log", "dep:wasm-bindgen", "dep:tsify-next", "dep:log"] "any-arch",
"dep:cwdemangle",
"dep:cwextab",
"dep:ppc750cl",
]
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",
]
wasm = [
"any-arch",
"bindings",
"dep:console_error_panic_hook",
"dep:console_log",
"dep:log",
"dep:tsify-next",
"dep:wasm-bindgen",
]
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = ["all"] features = ["all"]
@@ -63,7 +149,7 @@ gimli = { version = "0.31", default-features = false, features = ["read-all"], o
# ppc # ppc
cwdemangle = { version = "1.0", optional = true } cwdemangle = { version = "1.0", optional = true }
cwextab = { version = "1.0.2", optional = true } cwextab = { version = "1.0", optional = true }
ppc750cl = { version = "0.3", optional = true } ppc750cl = { version = "0.3", optional = true }
# mips # mips
@@ -83,10 +169,10 @@ yaxpeax-arch = { version = "0.3", default-features = false, features = ["std"],
yaxpeax-arm = { version = "0.3", default-features = false, features = ["std"], optional = true } yaxpeax-arm = { version = "0.3", default-features = false, features = ["std"], optional = true }
# build # build
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2", optional = true } notify = { version = "8.0.0", optional = true }
notify-debouncer-full = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2", optional = true } notify-debouncer-full = { version = "0.5.0", optional = true }
shell-escape = { version = "0.1", optional = true } shell-escape = { version = "0.1", optional = true }
tempfile = { version = "3.14", optional = true } tempfile = { version = "3.15", optional = true }
time = { version = "0.3", optional = true } time = { version = "0.3", optional = true }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
@@ -96,13 +182,20 @@ winapi = { version = "0.3", optional = true }
# For Linux static binaries, use rustls # For Linux static binaries, use rustls
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"], optional = true } reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"], optional = true }
self_update = { version = "0.41", default-features = false, features = ["rustls"], optional = true } self_update = { version = "0.42", default-features = false, features = ["rustls"], optional = true }
# For all other platforms, use native TLS # For all other platforms, use native TLS
[target.'cfg(not(target_os = "linux"))'.dependencies] [target.'cfg(not(target_os = "linux"))'.dependencies]
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"], optional = true } reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"], optional = true }
self_update = { version = "0.41", optional = true } self_update = { version = "0.42", optional = true }
[build-dependencies] [build-dependencies]
prost-build = { version = "0.13", optional = true } heck = { version = "0.5", optional = true }
pbjson-build = { version = "0.7", optional = true } pbjson-build = { version = "0.7", optional = true }
prettyplease = { version = "0.2", optional = true }
proc-macro2 = { version = "1.0", optional = true }
prost-build = { version = "0.13", optional = true }
quote = { version = "1.0", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }
syn = { version = "2.0", optional = true }

View File

@@ -1,6 +1,11 @@
#[cfg(feature = "any-arch")]
mod config_gen;
fn main() { fn main() {
#[cfg(feature = "bindings")] #[cfg(feature = "bindings")]
compile_protos(); compile_protos();
#[cfg(feature = "any-arch")]
config_gen::generate_diff_config();
} }
#[cfg(feature = "bindings")] #[cfg(feature = "bindings")]

View File

@@ -0,0 +1,248 @@
{
"properties": [
{
"id": "functionRelocDiffs",
"type": "choice",
"default": "name_address",
"name": "Function relocation diffs",
"description": "How relocation targets will be diffed in the function view.",
"items": [
{
"value": "none",
"name": "None"
},
{
"value": "name_address",
"name": "Name or address"
},
{
"value": "data_value",
"name": "Data value"
},
{
"value": "all",
"name": "Name or address, data value"
}
]
},
{
"id": "spaceBetweenArgs",
"type": "boolean",
"default": true,
"name": "Space between args",
"description": "Adds a space between arguments in the diff output."
},
{
"id": "combineDataSections",
"type": "boolean",
"default": false,
"name": "Combine data sections",
"description": "Combines data sections with equal names."
},
{
"id": "arm.archVersion",
"type": "choice",
"default": "auto",
"name": "Architecture version",
"description": "ARM architecture version to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "v4t",
"name": "ARMv4T (GBA)"
},
{
"value": "v5te",
"name": "ARMv5TE (DS)"
},
{
"value": "v6k",
"name": "ARMv6K (3DS)"
}
]
},
{
"id": "arm.unifiedSyntax",
"type": "boolean",
"default": false,
"name": "Unified syntax",
"description": "Disassemble as unified assembly language (UAL)."
},
{
"id": "arm.avRegisters",
"type": "boolean",
"default": false,
"name": "Use A/V registers",
"description": "Display R0-R3 as A1-A4 and R4-R11 as V1-V8."
},
{
"id": "arm.r9Usage",
"type": "choice",
"default": "generalPurpose",
"name": "Display R9 as",
"items": [
{
"value": "generalPurpose",
"name": "R9 or V6",
"description": "Use R9 as a general-purpose register."
},
{
"value": "sb",
"name": "SB (static base)",
"description": "Used for position-independent data (PID)."
},
{
"value": "tr",
"name": "TR (TLS register)",
"description": "Used for thread-local storage."
}
]
},
{
"id": "arm.slUsage",
"type": "boolean",
"default": false,
"name": "Display R10 as SL",
"description": "Used for explicit stack limits."
},
{
"id": "arm.fpUsage",
"type": "boolean",
"default": false,
"name": "Display R11 as FP",
"description": "Used for frame pointers."
},
{
"id": "arm.ipUsage",
"type": "boolean",
"default": false,
"name": "Display R12 as IP",
"description": "Used for interworking and long branches."
},
{
"id": "mips.abi",
"type": "choice",
"default": "auto",
"name": "ABI",
"description": "MIPS ABI to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "o32",
"name": "O32"
},
{
"value": "n32",
"name": "N32"
},
{
"value": "n64",
"name": "N64"
}
]
},
{
"id": "mips.instrCategory",
"type": "choice",
"default": "auto",
"name": "Instruction category",
"description": "MIPS instruction category to use for disassembly.",
"items": [
{
"value": "auto",
"name": "Auto"
},
{
"value": "cpu",
"name": "CPU"
},
{
"value": "rsp",
"name": "RSP (N64)"
},
{
"value": "r3000gte",
"name": "R3000 GTE (PS1)"
},
{
"value": "r4000allegrex",
"name": "R4000 ALLEGREX (PSP)"
},
{
"value": "r5900",
"name": "R5900 EE (PS2)"
}
]
},
{
"id": "x86.formatter",
"type": "choice",
"default": "intel",
"name": "Format",
"description": "x86 disassembly syntax.",
"items": [
{
"value": "intel",
"name": "Intel"
},
{
"value": "gas",
"name": "AT&T"
},
{
"value": "nasm",
"name": "NASM"
},
{
"value": "masm",
"name": "MASM"
}
]
}
],
"groups": [
{
"id": "general",
"name": "General",
"properties": [
"functionRelocDiffs",
"spaceBetweenArgs",
"combineDataSections"
]
},
{
"id": "arm",
"name": "ARM",
"properties": [
"arm.archVersion",
"arm.unifiedSyntax",
"arm.avRegisters",
"arm.r9Usage",
"arm.slUsage",
"arm.fpUsage",
"arm.ipUsage"
]
},
{
"id": "mips",
"name": "MIPS",
"properties": [
"mips.abi",
"mips.instrCategory"
]
},
{
"id": "x86",
"name": "x86",
"properties": [
"x86.formatter"
]
}
]
}

491
objdiff-core/config_gen.rs Normal file
View File

@@ -0,0 +1,491 @@
use std::{
fs::File,
path::{Path, PathBuf},
};
use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
#[derive(Debug, serde::Deserialize)]
pub struct ConfigSchema {
pub properties: Vec<ConfigProperty>,
pub groups: Vec<ConfigGroup>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(tag = "type")]
pub enum ConfigProperty {
#[serde(rename = "boolean")]
Boolean(ConfigPropertyBoolean),
#[serde(rename = "choice")]
Choice(ConfigPropertyChoice),
}
#[derive(Debug, serde::Deserialize)]
pub struct ConfigPropertyBase {
pub id: String,
pub name: String,
pub description: Option<String>,
}
#[derive(Debug, serde::Deserialize)]
pub struct ConfigPropertyBoolean {
#[serde(flatten)]
pub base: ConfigPropertyBase,
pub default: bool,
}
#[derive(Debug, serde::Deserialize)]
pub struct ConfigPropertyChoice {
#[serde(flatten)]
pub base: ConfigPropertyBase,
pub default: String,
pub items: Vec<ConfigPropertyChoiceItem>,
}
#[derive(Debug, serde::Deserialize)]
pub struct ConfigPropertyChoiceItem {
pub value: String,
pub name: String,
pub description: Option<String>,
}
#[derive(Debug, serde::Deserialize)]
pub struct ConfigGroup {
pub id: String,
pub name: String,
pub description: Option<String>,
pub properties: Vec<String>,
}
fn build_doc(name: &str, description: Option<&str>) -> TokenStream {
let mut doc = format!(" {}", name);
let mut out = quote! { #[doc = #doc] };
if let Some(description) = description {
doc = format!(" {}", description);
out.extend(quote! { #[doc = ""] });
out.extend(quote! { #[doc = #doc] });
}
out
}
pub fn generate_diff_config() {
let schema_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("config-schema.json");
println!("cargo:rerun-if-changed={}", schema_path.display());
let schema_file = File::open(schema_path).expect("Failed to open config schema file");
let schema: ConfigSchema =
serde_json::from_reader(schema_file).expect("Failed to parse config schema");
let mut enums = TokenStream::new();
for property in &schema.properties {
let ConfigProperty::Choice(choice) = property else {
continue;
};
let enum_ident = format_ident!("{}", choice.base.id.to_upper_camel_case());
let mut variants = TokenStream::new();
let mut full_variants = TokenStream::new();
let mut variant_info = TokenStream::new();
let mut variant_to_str = TokenStream::new();
let mut variant_to_name = TokenStream::new();
let mut variant_to_description = TokenStream::new();
let mut variant_from_str = TokenStream::new();
for item in &choice.items {
let variant_name = item.value.to_upper_camel_case();
let variant_ident = format_ident!("{}", variant_name);
let is_default = item.value == choice.default;
variants.extend(build_doc(&item.name, item.description.as_deref()));
if is_default {
variants.extend(quote! { #[default] });
}
let value = &item.value;
variants.extend(quote! {
#[serde(rename = #value, alias = #variant_name)]
#variant_ident,
});
full_variants.extend(quote! { #enum_ident::#variant_ident, });
variant_to_str.extend(quote! { #enum_ident::#variant_ident => #value, });
let name = &item.name;
variant_to_name.extend(quote! { #enum_ident::#variant_ident => #name, });
if let Some(description) = &item.description {
variant_to_description.extend(quote! {
#enum_ident::#variant_ident => Some(#description),
});
} else {
variant_to_description.extend(quote! {
#enum_ident::#variant_ident => None,
});
}
let description = if let Some(description) = &item.description {
quote! { Some(#description) }
} else {
quote! { None }
};
variant_info.extend(quote! {
ConfigEnumVariantInfo {
value: #value,
name: #name,
description: #description,
is_default: #is_default,
},
});
variant_from_str.extend(quote! {
if s.eq_ignore_ascii_case(#value) { return Ok(#enum_ident::#variant_ident); }
});
}
enums.extend(quote! {
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
pub enum #enum_ident {
#variants
}
impl ConfigEnum for #enum_ident {
#[inline]
fn variants() -> &'static [Self] {
static VARIANTS: &[#enum_ident] = &[#full_variants];
VARIANTS
}
#[inline]
fn variant_info() -> &'static [ConfigEnumVariantInfo] {
static VARIANT_INFO: &[ConfigEnumVariantInfo] = &[
#variant_info
];
VARIANT_INFO
}
fn as_str(&self) -> &'static str {
match self {
#variant_to_str
}
}
fn name(&self) -> &'static str {
match self {
#variant_to_name
}
}
fn description(&self) -> Option<&'static str> {
match self {
#variant_to_description
}
}
}
impl std::str::FromStr for #enum_ident {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
#variant_from_str
Err(())
}
}
});
}
let mut groups = TokenStream::new();
let mut group_idents = Vec::new();
for group in &schema.groups {
let ident = format_ident!("CONFIG_GROUP_{}", group.id.to_shouty_snake_case());
let id = &group.id;
let name = &group.name;
let description = if let Some(description) = &group.description {
quote! { Some(#description) }
} else {
quote! { None }
};
let properties =
group.properties.iter().map(|p| format_ident!("{}", p.to_upper_camel_case()));
groups.extend(quote! {
ConfigPropertyGroup {
id: #id,
name: #name,
description: #description,
properties: &[#(ConfigPropertyId::#properties,)*],
},
});
group_idents.push(ident);
}
let mut property_idents = Vec::new();
let mut property_variants = TokenStream::new();
let mut variant_info = TokenStream::new();
let mut config_property_id_to_str = TokenStream::new();
let mut config_property_id_to_name = TokenStream::new();
let mut config_property_id_to_description = TokenStream::new();
let mut config_property_id_to_kind = TokenStream::new();
let mut property_fields = TokenStream::new();
let mut default_fields = TokenStream::new();
let mut get_property_value_variants = TokenStream::new();
let mut set_property_value_variants = TokenStream::new();
let mut set_property_value_str_variants = TokenStream::new();
let mut config_property_id_from_str = TokenStream::new();
for property in &schema.properties {
let base = match property {
ConfigProperty::Boolean(b) => &b.base,
ConfigProperty::Choice(c) => &c.base,
};
let id = &base.id;
let enum_ident = format_ident!("{}", id.to_upper_camel_case());
property_idents.push(enum_ident.clone());
config_property_id_to_str.extend(quote! { Self::#enum_ident => #id, });
let name = &base.name;
config_property_id_to_name.extend(quote! { Self::#enum_ident => #name, });
if let Some(description) = &base.description {
config_property_id_to_description.extend(quote! {
Self::#enum_ident => Some(#description),
});
} else {
config_property_id_to_description.extend(quote! {
Self::#enum_ident => None,
});
}
let doc = build_doc(name, base.description.as_deref());
property_variants.extend(quote! { #doc #enum_ident, });
property_fields.extend(doc);
let field_ident = format_ident!("{}", id.to_snake_case());
match property {
ConfigProperty::Boolean(b) => {
let default = b.default;
if default {
property_fields.extend(quote! {
#[serde(default = "default_true")]
});
}
property_fields.extend(quote! {
pub #field_ident: bool,
});
default_fields.extend(quote! {
#field_ident: #default,
});
}
ConfigProperty::Choice(_) => {
property_fields.extend(quote! {
pub #field_ident: #enum_ident,
});
default_fields.extend(quote! {
#field_ident: #enum_ident::default(),
});
}
}
let property_value = match property {
ConfigProperty::Boolean(_) => {
quote! { ConfigPropertyValue::Boolean(self.#field_ident) }
}
ConfigProperty::Choice(_) => {
quote! { ConfigPropertyValue::Choice(self.#field_ident.as_str()) }
}
};
get_property_value_variants.extend(quote! {
ConfigPropertyId::#enum_ident => #property_value,
});
match property {
ConfigProperty::Boolean(_) => {
set_property_value_variants.extend(quote! {
ConfigPropertyId::#enum_ident => {
if let ConfigPropertyValue::Boolean(value) = value {
self.#field_ident = value;
Ok(())
} else {
Err(())
}
},
});
set_property_value_str_variants.extend(quote! {
ConfigPropertyId::#enum_ident => {
if let Ok(value) = value.parse() {
self.#field_ident = value;
Ok(())
} else {
Err(())
}
},
});
}
ConfigProperty::Choice(_) => {
set_property_value_variants.extend(quote! {
ConfigPropertyId::#enum_ident => {
if let ConfigPropertyValue::Choice(value) = value {
if let Ok(value) = value.parse() {
self.#field_ident = value;
Ok(())
} else {
Err(())
}
} else {
Err(())
}
},
});
set_property_value_str_variants.extend(quote! {
ConfigPropertyId::#enum_ident => {
if let Ok(value) = value.parse() {
self.#field_ident = value;
Ok(())
} else {
Err(())
}
},
});
}
}
let description = if let Some(description) = &base.description {
quote! { Some(#description) }
} else {
quote! { None }
};
variant_info.extend(quote! {
ConfigEnumVariantInfo {
value: #id,
name: #name,
description: #description,
is_default: false,
},
});
match property {
ConfigProperty::Boolean(_) => {
config_property_id_to_kind.extend(quote! {
Self::#enum_ident => ConfigPropertyKind::Boolean,
});
}
ConfigProperty::Choice(_) => {
config_property_id_to_kind.extend(quote! {
Self::#enum_ident => ConfigPropertyKind::Choice(#enum_ident::variant_info()),
});
}
}
let snake_id = id.to_snake_case();
config_property_id_from_str.extend(quote! {
if s.eq_ignore_ascii_case(#id) || s.eq_ignore_ascii_case(#snake_id) {
return Ok(Self::#enum_ident);
}
});
}
let tokens = quote! {
pub trait ConfigEnum: Sized {
fn variants() -> &'static [Self];
fn variant_info() -> &'static [ConfigEnumVariantInfo];
fn as_str(&self) -> &'static str;
fn name(&self) -> &'static str;
fn description(&self) -> Option<&'static str>;
}
#[derive(Clone, Debug)]
pub struct ConfigEnumVariantInfo {
pub value: &'static str,
pub name: &'static str,
pub description: Option<&'static str>,
pub is_default: bool,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum ConfigPropertyId {
#property_variants
}
impl ConfigEnum for ConfigPropertyId {
#[inline]
fn variants() -> &'static [Self] {
static VARIANTS: &[ConfigPropertyId] = &[#(ConfigPropertyId::#property_idents,)*];
VARIANTS
}
#[inline]
fn variant_info() -> &'static [ConfigEnumVariantInfo] {
static VARIANT_INFO: &[ConfigEnumVariantInfo] = &[
#variant_info
];
VARIANT_INFO
}
fn as_str(&self) -> &'static str {
match self {
#config_property_id_to_str
}
}
fn name(&self) -> &'static str {
match self {
#config_property_id_to_name
}
}
fn description(&self) -> Option<&'static str> {
match self {
#config_property_id_to_description
}
}
}
impl ConfigPropertyId {
pub fn kind(&self) -> ConfigPropertyKind {
match self {
#config_property_id_to_kind
}
}
}
impl std::str::FromStr for ConfigPropertyId {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
#config_property_id_from_str
Err(())
}
}
#[derive(Clone, Debug)]
pub struct ConfigPropertyGroup {
pub id: &'static str,
pub name: &'static str,
pub description: Option<&'static str>,
pub properties: &'static [ConfigPropertyId],
}
pub static CONFIG_GROUPS: &[ConfigPropertyGroup] = &[#groups];
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum ConfigPropertyValue {
Boolean(bool),
Choice(&'static str),
}
impl ConfigPropertyValue {
pub fn to_json(&self) -> serde_json::Value {
match self {
ConfigPropertyValue::Boolean(value) => serde_json::Value::Bool(*value),
ConfigPropertyValue::Choice(value) => serde_json::Value::String(value.to_string()),
}
}
}
#[derive(Clone, Debug)]
pub enum ConfigPropertyKind {
Boolean,
Choice(&'static [ConfigEnumVariantInfo]),
}
#enums
#[inline(always)]
fn default_true() -> bool { true }
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
#[serde(default)]
pub struct DiffObjConfig {
#property_fields
}
impl Default for DiffObjConfig {
fn default() -> Self {
Self {
#default_fields
}
}
}
impl DiffObjConfig {
pub fn get_property_value(&self, id: ConfigPropertyId) -> ConfigPropertyValue {
match id {
#get_property_value_variants
}
}
#[allow(clippy::result_unit_err)]
pub fn set_property_value(&mut self, id: ConfigPropertyId, value: ConfigPropertyValue) -> Result<(), ()> {
match id {
#set_property_value_variants
}
}
#[allow(clippy::result_unit_err)]
pub fn set_property_value_str(&mut self, id: ConfigPropertyId, value: &str) -> Result<(), ()> {
match id {
#set_property_value_str_variants
}
}
}
};
let file = syn::parse2(tokens).unwrap();
let formatted = prettyplease::unparse(&file);
std::fs::write(
PathBuf::from(std::env::var_os("OUT_DIR").unwrap()).join("config.gen.rs"),
formatted,
)
.unwrap();
}

View File

@@ -21,9 +21,9 @@ enum SymbolFlag {
SYMBOL_NONE = 0; SYMBOL_NONE = 0;
SYMBOL_GLOBAL = 1; SYMBOL_GLOBAL = 1;
SYMBOL_LOCAL = 2; SYMBOL_LOCAL = 2;
SYMBOL_WEAK = 3; SYMBOL_WEAK = 4;
SYMBOL_COMMON = 4; SYMBOL_COMMON = 8;
SYMBOL_HIDDEN = 5; SYMBOL_HIDDEN = 16;
} }
// A single parsed instruction // A single parsed instruction
@@ -122,10 +122,17 @@ message InstructionBranchTo {
uint32 branch_index = 2; uint32 branch_index = 2;
} }
message FunctionDiff { message SymbolRef {
optional uint32 section_index = 1;
uint32 symbol_index = 2;
}
message SymbolDiff {
Symbol symbol = 1; Symbol symbol = 1;
repeated InstructionDiff instructions = 2; repeated InstructionDiff instructions = 2;
optional float match_percent = 3; optional float match_percent = 3;
// The symbol ref in the _other_ object that this symbol was diffed against
optional SymbolRef target = 5;
} }
message DataDiff { message DataDiff {
@@ -140,7 +147,7 @@ message SectionDiff {
SectionKind kind = 2; SectionKind kind = 2;
uint64 size = 3; uint64 size = 3;
uint64 address = 4; uint64 address = 4;
repeated FunctionDiff functions = 5; repeated SymbolDiff symbols = 5;
repeated DataDiff data = 6; repeated DataDiff data = 6;
optional float match_percent = 7; optional float match_percent = 7;
} }

View File

@@ -139,9 +139,9 @@ impl ObjArch for ObjArchArm {
let version = match config.arm_arch_version { let version = match config.arm_arch_version {
ArmArchVersion::Auto => self.detected_version.unwrap_or(ArmVersion::V5Te), ArmArchVersion::Auto => self.detected_version.unwrap_or(ArmVersion::V5Te),
ArmArchVersion::V4T => ArmVersion::V4T, ArmArchVersion::V4t => ArmVersion::V4T,
ArmArchVersion::V5TE => ArmVersion::V5Te, ArmArchVersion::V5te => ArmVersion::V5Te,
ArmArchVersion::V6K => ArmVersion::V6K, ArmArchVersion::V6k => ArmVersion::V6K,
}; };
let endian = match self.endianness { let endian = match self.endianness {
object::Endianness::Little => unarm::Endian::Little, object::Endianness::Little => unarm::Endian::Little,
@@ -276,6 +276,19 @@ impl ObjArch for ObjArchArm {
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> { fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str> {
Cow::Owned(format!("<{flags:?}>")) Cow::Owned(format!("<{flags:?}>"))
} }
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_ARM_ABS32 => 4,
elf::R_ARM_REL32 => 4,
elf::R_ARM_ABS16 => 2,
elf::R_ARM_ABS8 => 1,
_ => 1,
},
_ => 1,
}
}
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]

View File

@@ -173,6 +173,21 @@ impl ObjArch for ObjArchArm64 {
_ => Cow::Owned(format!("<{flags:?}>")), _ => Cow::Owned(format!("<{flags:?}>")),
} }
} }
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_AARCH64_ABS64 => 8,
elf::R_AARCH64_ABS32 => 4,
elf::R_AARCH64_ABS16 => 2,
elf::R_AARCH64_PREL64 => 8,
elf::R_AARCH64_PREL32 => 4,
elf::R_AARCH64_PREL16 => 2,
_ => 1,
},
_ => 1,
}
}
} }
struct DisplayCtx<'a> { struct DisplayCtx<'a> {

View File

@@ -99,8 +99,8 @@ impl ObjArch for ObjArchMips {
MipsInstrCategory::Auto => self.instr_category, MipsInstrCategory::Auto => self.instr_category,
MipsInstrCategory::Cpu => InstrCategory::CPU, MipsInstrCategory::Cpu => InstrCategory::CPU,
MipsInstrCategory::Rsp => InstrCategory::RSP, MipsInstrCategory::Rsp => InstrCategory::RSP,
MipsInstrCategory::R3000Gte => InstrCategory::R3000GTE, MipsInstrCategory::R3000gte => InstrCategory::R3000GTE,
MipsInstrCategory::R4000Allegrex => InstrCategory::R4000ALLEGREX, MipsInstrCategory::R4000allegrex => InstrCategory::R4000ALLEGREX,
MipsInstrCategory::R5900 => InstrCategory::R5900, MipsInstrCategory::R5900 => InstrCategory::R5900,
}; };
@@ -271,6 +271,17 @@ impl ObjArch for ObjArchMips {
_ => Cow::Owned(format!("<{flags:?}>")), _ => Cow::Owned(format!("<{flags:?}>")),
} }
} }
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_MIPS_16 => 2,
elf::R_MIPS_32 => 4,
_ => 1,
},
_ => 1,
}
}
} }
fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> { fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {

View File

@@ -148,6 +148,8 @@ pub trait ObjArch: Send + Sync {
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>; fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize;
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() } fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None } fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None }
@@ -156,6 +158,17 @@ pub trait ObjArch: Send + Sync {
Some(format!("Bytes: {:#x?}", bytes)) Some(format!("Bytes: {:#x?}", bytes))
} }
fn display_ins_data(&self, ins: &ObjIns) -> Option<String> {
let reloc = ins.reloc.as_ref()?;
if reloc.addend >= 0 && reloc.target.bytes.len() > reloc.addend as usize {
self.guess_data_type(ins).and_then(|ty| {
self.display_data_type(ty, &reloc.target.bytes[reloc.addend as usize..])
})
} else {
None
}
}
// Downcast methods // Downcast methods
#[cfg(feature = "ppc")] #[cfg(feature = "ppc")]
fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None } fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None }

View File

@@ -1,6 +1,6 @@
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap, HashSet},
}; };
use anyhow::{bail, ensure, Result}; use anyhow::{bail, ensure, Result};
@@ -10,7 +10,7 @@ use object::{
elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget, elf, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, RelocationTarget,
Symbol, SymbolKind, Symbol, SymbolKind,
}; };
use ppc750cl::{Argument, InsIter, Opcode, ParsedIns, GPR}; use ppc750cl::{Argument, Arguments, Ins, InsIter, Opcode, ParsedIns, GPR};
use crate::{ use crate::{
arch::{DataType, ObjArch, ProcessCodeResult}, arch::{DataType, ObjArch, ProcessCodeResult},
@@ -143,6 +143,15 @@ impl ObjArch for ObjArchPpc {
} }
} }
if reloc.is_none() {
if let Some(fake_pool_reloc) = fake_pool_reloc_for_addr.get(&cur_addr) {
// If this instruction has a fake pool relocation, show it as a fake argument
// at the end of the line.
args.push(ObjInsArg::PlainText(" ".into()));
push_reloc(&mut args, fake_pool_reloc)?;
}
}
ops.push(ins.op as u16); ops.push(ins.op as u16);
let line = line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b); let line = line_info.range(..=cur_addr as u64).last().map(|(_, &b)| b);
insts.push(ObjIns { insts.push(ObjIns {
@@ -193,24 +202,23 @@ impl ObjArch for ObjArchPpc {
} }
} }
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize {
match flags {
RelocationFlags::Elf { r_type } => match r_type {
elf::R_PPC_ADDR32 => 4,
elf::R_PPC_UADDR32 => 4,
_ => 1,
},
_ => 1,
}
}
fn guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> { fn guess_data_type(&self, instruction: &ObjIns) -> Option<super::DataType> {
if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) { if instruction.reloc.as_ref().is_some_and(|r| r.target.name.starts_with("@stringBase")) {
return Some(DataType::String); return Some(DataType::String);
} }
let op = Opcode::from(instruction.op as u8); guess_data_type_from_load_store_inst_op(Opcode::from(instruction.op as u8))
if let Some(ty) = guess_data_type_from_load_store_inst_op(op) {
Some(ty)
} else if op == Opcode::Addi {
// Assume that any addi instruction that references a local symbol is loading a string.
// This hack is not ideal and results in tons of false positives where it will show
// garbage strings (e.g. misinterpreting arrays, float literals, etc).
// But not all strings are in the @stringBase pool, so the condition above that checks
// the target symbol name would miss some.
Some(DataType::String)
} else {
None
}
} }
fn display_data_type(&self, ty: DataType, bytes: &[u8]) -> Option<String> { fn display_data_type(&self, ty: DataType, bytes: &[u8]) -> Option<String> {
@@ -248,6 +256,12 @@ fn push_reloc(args: &mut Vec<ObjInsArg>, reloc: &ObjReloc) -> Result<()> {
elf::R_PPC_ADDR32 | elf::R_PPC_UADDR32 | elf::R_PPC_REL24 | elf::R_PPC_REL14 => { elf::R_PPC_ADDR32 | elf::R_PPC_UADDR32 | elf::R_PPC_REL24 | elf::R_PPC_REL14 => {
args.push(ObjInsArg::Reloc); args.push(ObjInsArg::Reloc);
} }
elf::R_PPC_NONE => {
// Fake pool relocation.
args.push(ObjInsArg::PlainText("<".into()));
args.push(ObjInsArg::Reloc);
args.push(ObjInsArg::PlainText(">".into()));
}
_ => bail!("Unsupported ELF PPC relocation type {r_type}"), _ => bail!("Unsupported ELF PPC relocation type {r_type}"),
}, },
flags => bail!("Unsupported PPC relocation kind: {flags:?}"), flags => bail!("Unsupported PPC relocation kind: {flags:?}"),
@@ -442,16 +456,43 @@ fn get_offset_and_addr_gpr_for_possible_pool_reference(
Argument::Simm(simm), Argument::Simm(simm),
) => Some((simm.0, addr_src_gpr, Some(addr_dst_gpr))), ) => Some((simm.0, addr_src_gpr, Some(addr_dst_gpr))),
( (
// `mr` or `mr.`
Opcode::Or, Opcode::Or,
Argument::GPR(addr_dst_gpr), Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr), Argument::GPR(addr_src_gpr),
Argument::None, Argument::None,
) => Some((0, addr_src_gpr, Some(addr_dst_gpr))), // `mr` or `mr.` ) => Some((0, addr_src_gpr, Some(addr_dst_gpr))),
(
Opcode::Add,
Argument::GPR(addr_dst_gpr),
Argument::GPR(addr_src_gpr),
Argument::GPR(_offset_gpr),
) => Some((0, addr_src_gpr, Some(addr_dst_gpr))),
_ => None, _ => None,
} }
} }
} }
// Remove the relocation we're keeping track of in a particular register when an instruction reuses
// that register to hold some other value, unrelated to pool relocation addresses.
fn clear_overwritten_gprs(ins: Ins, gpr_pool_relocs: &mut HashMap<u8, ObjReloc>) {
let mut def_args = Arguments::default();
ins.parse_defs(&mut def_args);
for arg in def_args {
if let Argument::GPR(gpr) = arg {
if ins.op == Opcode::Lmw {
// `lmw` overwrites all registers from rd to r31.
// ppc750cl only returns rd itself, so we manually clear the rest of them.
for reg in gpr.0..31 {
gpr_pool_relocs.remove(&reg);
}
break;
}
gpr_pool_relocs.remove(&gpr.0);
}
}
}
// We create a fake relocation for an instruction, vaguely simulating what the actual relocation // We create a fake relocation for an instruction, vaguely simulating what the actual relocation
// might have looked like if it wasn't pooled. This is so minimal changes are needed to display // might have looked like if it wasn't pooled. This is so minimal changes are needed to display
// pooled accesses vs non-pooled accesses. We set the relocation type to R_PPC_NONE to indicate that // pooled accesses vs non-pooled accesses. We set the relocation type to R_PPC_NONE to indicate that
@@ -461,14 +502,17 @@ fn get_offset_and_addr_gpr_for_possible_pool_reference(
fn make_fake_pool_reloc(offset: i16, cur_addr: u32, pool_reloc: &ObjReloc) -> Option<ObjReloc> { fn make_fake_pool_reloc(offset: i16, cur_addr: u32, pool_reloc: &ObjReloc) -> Option<ObjReloc> {
let offset_from_pool = pool_reloc.addend + offset as i64; let offset_from_pool = pool_reloc.addend + offset as i64;
let target_address = pool_reloc.target.address.checked_add_signed(offset_from_pool)?; let target_address = pool_reloc.target.address.checked_add_signed(offset_from_pool)?;
let orig_section_index = pool_reloc.target.orig_section_index?; let target;
// We also need to create a fake target symbol to go inside our fake relocation. let addend;
// This is because we don't have access to list of all symbols in this section, so we can't find if pool_reloc.target.orig_section_index.is_some() {
// the real symbol yet. Instead we make a placeholder that has the correct `orig_section_index` // If the target symbol is within this current object, then we also need to create a fake
// and `address` fields, and then later on when this information is displayed to the user, we // target symbol to go inside our fake relocation. This is because we don't have access to
// can find the real symbol by searching through the object's section's symbols for one that // list of all symbols in this section, so we can't find the real symbol within the pool
// contains this address. // based on its address yet. Instead we make a placeholder that has the correct
let fake_target_symbol = ObjSymbol { // `orig_section_index` and `address` fields, and then later on when this information is
// displayed to the user, we can find the real symbol by searching through the object's
// section's symbols for one that contains this address.
target = ObjSymbol {
name: "".to_string(), name: "".to_string(),
demangled_name: None, demangled_name: None,
address: target_address, address: target_address,
@@ -477,19 +521,32 @@ fn make_fake_pool_reloc(offset: i16, cur_addr: u32, pool_reloc: &ObjReloc) -> Op
size_known: false, size_known: false,
kind: Default::default(), kind: Default::default(),
flags: Default::default(), flags: Default::default(),
orig_section_index: Some(orig_section_index), orig_section_index: pool_reloc.target.orig_section_index,
virtual_address: None, virtual_address: None,
original_index: None, original_index: None,
bytes: vec![], bytes: vec![],
}; };
// The addend is also fake because we don't know yet if the `target_address` here is the exact // The addend is also fake because we don't know yet if the `target_address` here is the exact
// start of the symbol or if it's in the middle of it. // start of the symbol or if it's in the middle of it.
let fake_addend = 0; addend = 0;
} else {
// But if the target symbol is in a different object (extern), then we simply copy the pool
// relocation's target. This is because it won't be possible to locate the actual symbol
// later on based only off of an offset without knowing the object or section it's in. And
// doing that for external symbols would also be unnecessary, because when the compiler
// generates an instruction that accesses an external "pool" plus some offset, that won't be
// a normal pool that contains other symbols within it that we want to display. It will be
// something like a vtable for a class with multiple inheritance (for example, dCcD_Cyl in
// The Wind Waker). So just showing that vtable symbol plus an addend to represent the
// offset into it works fine in this case, no fake symbol to hold an address is necessary.
target = pool_reloc.target.clone();
addend = pool_reloc.addend;
};
Some(ObjReloc { Some(ObjReloc {
flags: RelocationFlags::Elf { r_type: elf::R_PPC_NONE }, flags: RelocationFlags::Elf { r_type: elf::R_PPC_NONE },
address: cur_addr as u64, address: cur_addr as u64,
target: fake_target_symbol, target,
addend: fake_addend, addend,
}) })
} }
@@ -497,64 +554,137 @@ fn make_fake_pool_reloc(offset: i16, cur_addr: u32, pool_reloc: &ObjReloc) -> Op
// of pooled data relocations in them, finding which instructions load data from those addresses, // of pooled data relocations in them, finding which instructions load data from those addresses,
// and constructing a mapping of the address of that instruction to a "fake pool relocation" that // and constructing a mapping of the address of that instruction to a "fake pool relocation" that
// simulates what that instruction's relocation would look like if data hadn't been pooled. // simulates what that instruction's relocation would look like if data hadn't been pooled.
// Limitations: This method currently only goes through the instructions in a function in linear // This method tries to follow the function's proper control flow. It keeps track of a queue of
// order, from start to finish. It does *not* follow any branches. This means that it could have // states it hasn't traversed yet, where each state holds an instruction address and a HashMap of
// false positives or false negatives in determining which relocation is currently loaded in which // which registers hold which pool relocations at that point.
// register at any given point in the function, as control flow is not respected. // When a conditional or unconditional branch is encountered, the destination of the branch is added
// There are currently no known examples of this method producing inaccurate results in reality, but // to the queue. Conditional branches will traverse both the path where the branch is taken and the
// if examples are found, it may be possible to update this method to also follow all branches so // one where it's not. Unconditional branches only follow the branch, ignoring any code immediately
// that it produces more accurate results. // after the branch instruction.
// Limitations: This method cannot read jump tables. This is because the jump tables are located in
// the .data section, but ObjArch.process_code only has access to the .text section. In order to
// work around this limitation and avoid completely missing most code inside switch statements that
// use jump tables, we instead guess that any parts of a function we missed were switch cases, and
// traverse them as if the last `bctr` before that address had branched there. This should be fairly
// accurate in practice - in testing the only instructions it seems to miss are double branches that
// the compiler generates in error which can never be reached during normal execution anyway.
fn generate_fake_pool_reloc_for_addr_mapping( fn generate_fake_pool_reloc_for_addr_mapping(
address: u64, func_address: u64,
code: &[u8], code: &[u8],
relocations: &[ObjReloc], relocations: &[ObjReloc],
) -> HashMap<u32, ObjReloc> { ) -> HashMap<u32, ObjReloc> {
let mut active_pool_relocs = HashMap::new(); let mut visited_ins_addrs = HashSet::new();
let mut pool_reloc_for_addr = HashMap::new(); let mut pool_reloc_for_addr = HashMap::new();
for (cur_addr, ins) in InsIter::new(code, address as u32) { let mut ins_iters_with_gpr_state =
let simplified = ins.simplified(); vec![(InsIter::new(code, func_address as u32), HashMap::new())];
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr); let mut gpr_state_at_bctr = BTreeMap::new();
while let Some((ins_iter, mut gpr_pool_relocs)) = ins_iters_with_gpr_state.pop() {
for (cur_addr, ins) in ins_iter {
if visited_ins_addrs.contains(&cur_addr) {
// Avoid getting stuck in an infinite loop when following looping branches.
break;
}
visited_ins_addrs.insert(cur_addr);
let simplified = ins.simplified();
// First handle traversing the function's control flow.
let mut branch_dest = None;
for arg in simplified.args_iter() {
if let Argument::BranchDest(dest) = arg {
let dest = cur_addr.wrapping_add_signed(dest.0);
branch_dest = Some(dest);
break;
}
}
if let Some(branch_dest) = branch_dest {
if branch_dest >= func_address as u32
&& (branch_dest - func_address as u32) < code.len() as u32
{
let dest_offset_into_func = branch_dest - func_address as u32;
let dest_code_slice = &code[dest_offset_into_func as usize..];
match ins.op {
Opcode::Bc => {
// Conditional branch.
// Add the branch destination to the queue to do later.
ins_iters_with_gpr_state.push((
InsIter::new(dest_code_slice, branch_dest),
gpr_pool_relocs.clone(),
));
// Then continue on with the current iterator.
}
Opcode::B => {
if simplified.mnemonic != "bl" {
// Unconditional branch.
// Add the branch destination to the queue.
ins_iters_with_gpr_state.push((
InsIter::new(dest_code_slice, branch_dest),
gpr_pool_relocs.clone(),
));
// Break out of the current iterator so we can do the newly added one.
break;
}
}
_ => unreachable!(),
}
}
}
if let Opcode::Bcctr = ins.op {
if simplified.mnemonic == "bctr" {
// Unconditional branch to count register.
// Likely a jump table.
gpr_state_at_bctr.insert(cur_addr, gpr_pool_relocs.clone());
}
}
// Then handle keeping track of which GPR contains which pool relocation.
let reloc = relocations.iter().find(|r| (r.address as u32 & !3) == cur_addr);
if let Some(reloc) = reloc { if let Some(reloc) = reloc {
// This instruction has a real relocation, so it may be a pool load we want to keep // This instruction has a real relocation, so it may be a pool load we want to keep
// track of. // track of.
let args = &simplified.args; let args = &simplified.args;
match (ins.op, args[0], args[1], args[2]) { match (ins.op, args[0], args[1], args[2]) {
( (
// `lis` + `addi`
Opcode::Addi, Opcode::Addi,
Argument::GPR(addr_dst_gpr), Argument::GPR(addr_dst_gpr),
Argument::GPR(_addr_src_gpr), Argument::GPR(_addr_src_gpr),
Argument::Simm(_simm), Argument::Simm(_simm),
) => { ) => {
active_pool_relocs.insert(addr_dst_gpr.0, reloc.clone()); // `lis` + `addi` gpr_pool_relocs.insert(addr_dst_gpr.0, reloc.clone());
} }
( (
// `lis` + `ori`
Opcode::Ori, Opcode::Ori,
Argument::GPR(addr_dst_gpr), Argument::GPR(addr_dst_gpr),
Argument::GPR(_addr_src_gpr), Argument::GPR(_addr_src_gpr),
Argument::Uimm(_uimm), Argument::Uimm(_uimm),
) => { ) => {
active_pool_relocs.insert(addr_dst_gpr.0, reloc.clone()); // `lis` + `ori` gpr_pool_relocs.insert(addr_dst_gpr.0, reloc.clone());
} }
(Opcode::B, _, _, _) => { (Opcode::B, _, _, _) => {
if simplified.mnemonic == "bl" { if simplified.mnemonic == "bl" {
// When encountering a function call, clear any active pool relocations from // When encountering a function call, clear any active pool relocations from
// the volatile registers (r0, r3-r12), but not the nonvolatile registers. // the volatile registers (r0, r3-r12), but not the nonvolatile registers.
active_pool_relocs.remove(&0); gpr_pool_relocs.remove(&0);
for gpr in 3..12 { for gpr in 3..12 {
active_pool_relocs.remove(&gpr); gpr_pool_relocs.remove(&gpr);
} }
} }
} }
_ => {} _ => {
clear_overwritten_gprs(ins, &mut gpr_pool_relocs);
}
} }
} else if let Some((offset, addr_src_gpr, addr_dst_gpr)) = } else if let Some((offset, addr_src_gpr, addr_dst_gpr)) =
get_offset_and_addr_gpr_for_possible_pool_reference(ins.op, &simplified) get_offset_and_addr_gpr_for_possible_pool_reference(ins.op, &simplified)
{ {
// This instruction doesn't have a real relocation, so it may be a reference to one of // This instruction doesn't have a real relocation, so it may be a reference to one of
// the already-loaded pools. // the already-loaded pools.
if let Some(pool_reloc) = active_pool_relocs.get(&addr_src_gpr.0) { if let Some(pool_reloc) = gpr_pool_relocs.get(&addr_src_gpr.0) {
if let Some(fake_pool_reloc) = make_fake_pool_reloc(offset, cur_addr, pool_reloc) { if let Some(fake_pool_reloc) =
make_fake_pool_reloc(offset, cur_addr, pool_reloc)
{
pool_reloc_for_addr.insert(cur_addr, fake_pool_reloc); pool_reloc_for_addr.insert(cur_addr, fake_pool_reloc);
} }
if let Some(addr_dst_gpr) = addr_dst_gpr { if let Some(addr_dst_gpr) = addr_dst_gpr {
@@ -568,7 +698,40 @@ fn generate_fake_pool_reloc_for_addr_mapping(
// Then the body of the loop will `lwzx` one of the array elements from r21. // Then the body of the loop will `lwzx` one of the array elements from r21.
let mut new_reloc = pool_reloc.clone(); let mut new_reloc = pool_reloc.clone();
new_reloc.addend += offset as i64; new_reloc.addend += offset as i64;
active_pool_relocs.insert(addr_dst_gpr.0, new_reloc); gpr_pool_relocs.insert(addr_dst_gpr.0, new_reloc);
} else {
clear_overwritten_gprs(ins, &mut gpr_pool_relocs);
}
} else {
clear_overwritten_gprs(ins, &mut gpr_pool_relocs);
}
} else {
clear_overwritten_gprs(ins, &mut gpr_pool_relocs);
}
}
// Finally, if we're about to finish the outer loop and don't have any more control flow to
// follow, we check if there are any instruction addresses in this function that we missed.
// If so, and if there were any `bctr` instructions before those points in this function,
// then we try to traverse those missing spots as switch cases.
if ins_iters_with_gpr_state.is_empty() {
let unseen_addrs = (func_address as u32..func_address as u32 + code.len() as u32)
.step_by(4)
.filter(|addr| !visited_ins_addrs.contains(addr));
for unseen_addr in unseen_addrs {
let prev_bctr_gpr_state = gpr_state_at_bctr
.iter()
.filter(|(&addr, _)| addr < unseen_addr)
.min_by_key(|(&addr, _)| addr)
.map(|(_, gpr_state)| gpr_state);
if let Some(gpr_pool_relocs) = prev_bctr_gpr_state {
let dest_offset_into_func = unseen_addr - func_address as u32;
let dest_code_slice = &code[dest_offset_into_func as usize..];
ins_iters_with_gpr_state.push((
InsIter::new(dest_code_slice, unseen_addr),
gpr_pool_relocs.clone(),
));
break;
} }
} }
} }

View File

@@ -162,6 +162,19 @@ impl ObjArch for ObjArchX86 {
_ => Cow::Owned(format!("<{flags:?}>")), _ => Cow::Owned(format!("<{flags:?}>")),
} }
} }
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize {
match flags {
RelocationFlags::Coff { typ } => match typ {
pe::IMAGE_REL_I386_DIR16 => 2,
pe::IMAGE_REL_I386_REL16 => 2,
pe::IMAGE_REL_I386_DIR32 => 4,
pe::IMAGE_REL_I386_REL32 => 4,
_ => 1,
},
_ => 1,
}
}
} }
fn replace_arg( fn replace_arg(

View File

@@ -4,6 +4,7 @@ use crate::{
ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo,
ObjInsDiff, ObjInsDiffKind, ObjSectionDiff, ObjSymbolDiff, ObjInsDiff, ObjInsDiffKind, ObjSectionDiff, ObjSymbolDiff,
}, },
obj,
obj::{ obj::{
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSectionKind, ObjSymbol, ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSectionKind, ObjSymbol,
ObjSymbolFlagSet, ObjSymbolFlags, ObjSymbolFlagSet, ObjSymbolFlags,
@@ -39,14 +40,15 @@ impl ObjectDiff {
impl SectionDiff { impl SectionDiff {
pub fn new(obj: &ObjInfo, section_index: usize, section_diff: &ObjSectionDiff) -> Self { pub fn new(obj: &ObjInfo, section_index: usize, section_diff: &ObjSectionDiff) -> Self {
let section = &obj.sections[section_index]; let section = &obj.sections[section_index];
let functions = section_diff.symbols.iter().map(|d| FunctionDiff::new(obj, d)).collect(); let symbols = section_diff.symbols.iter().map(|d| SymbolDiff::new(obj, d)).collect();
let data = section_diff.data_diff.iter().map(|d| DataDiff::new(obj, d)).collect(); let data = section_diff.data_diff.iter().map(|d| DataDiff::new(obj, d)).collect();
// TODO: section_diff.reloc_diff
Self { Self {
name: section.name.to_string(), name: section.name.to_string(),
kind: SectionKind::from(section.kind) as i32, kind: SectionKind::from(section.kind) as i32,
size: section.size, size: section.size,
address: section.address, address: section.address,
functions, symbols,
data, data,
match_percent: section_diff.match_percent, match_percent: section_diff.match_percent,
} }
@@ -64,13 +66,22 @@ impl From<ObjSectionKind> for SectionKind {
} }
} }
impl FunctionDiff { impl From<obj::SymbolRef> for SymbolRef {
fn from(value: obj::SymbolRef) -> Self {
Self {
section_index: if value.section_idx == obj::SECTION_COMMON {
None
} else {
Some(value.section_idx as u32)
},
symbol_index: value.symbol_idx as u32,
}
}
}
impl SymbolDiff {
pub fn new(object: &ObjInfo, symbol_diff: &ObjSymbolDiff) -> Self { pub fn new(object: &ObjInfo, symbol_diff: &ObjSymbolDiff) -> Self {
let (_section, symbol) = object.section_symbol(symbol_diff.symbol_ref); let (_section, symbol) = object.section_symbol(symbol_diff.symbol_ref);
// let diff_symbol = symbol_diff.diff_symbol.map(|symbol_ref| {
// let (_section, symbol) = object.section_symbol(symbol_ref);
// Symbol::from(symbol)
// });
let instructions = symbol_diff let instructions = symbol_diff
.instructions .instructions
.iter() .iter()
@@ -78,9 +89,9 @@ impl FunctionDiff {
.collect(); .collect();
Self { Self {
symbol: Some(Symbol::new(symbol)), symbol: Some(Symbol::new(symbol)),
// diff_symbol,
instructions, instructions,
match_percent: symbol_diff.match_percent, match_percent: symbol_diff.match_percent,
target: symbol_diff.target_symbol.map(SymbolRef::from),
} }
} }
} }
@@ -110,7 +121,7 @@ impl Symbol {
fn symbol_flags(value: ObjSymbolFlagSet) -> u32 { fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
let mut flags = 0u32; let mut flags = 0u32;
if value.0.contains(ObjSymbolFlags::Global) { if value.0.contains(ObjSymbolFlags::Global) {
flags |= SymbolFlag::SymbolNone as u32; flags |= SymbolFlag::SymbolGlobal as u32;
} }
if value.0.contains(ObjSymbolFlags::Local) { if value.0.contains(ObjSymbolFlags::Local) {
flags |= SymbolFlag::SymbolLocal as u32; flags |= SymbolFlag::SymbolLocal as u32;

View File

@@ -13,20 +13,22 @@ fn parse_object(
fn parse_and_run_diff( fn parse_and_run_diff(
left: Option<Box<[u8]>>, left: Option<Box<[u8]>>,
right: Option<Box<[u8]>>, right: Option<Box<[u8]>>,
config: diff::DiffObjConfig, diff_config: diff::DiffObjConfig,
mapping_config: diff::MappingConfig,
) -> Result<DiffResult, JsError> { ) -> Result<DiffResult, JsError> {
let target = parse_object(left, &config)?; let target = parse_object(left, &diff_config)?;
let base = parse_object(right, &config)?; let base = parse_object(right, &diff_config)?;
run_diff(target.as_ref(), base.as_ref(), config) run_diff(target.as_ref(), base.as_ref(), diff_config, mapping_config)
} }
fn run_diff( fn run_diff(
left: Option<&obj::ObjInfo>, left: Option<&obj::ObjInfo>,
right: Option<&obj::ObjInfo>, right: Option<&obj::ObjInfo>,
config: diff::DiffObjConfig, diff_config: diff::DiffObjConfig,
mapping_config: diff::MappingConfig,
) -> Result<DiffResult, JsError> { ) -> Result<DiffResult, JsError> {
log::debug!("Running diff with config: {:?}", config); log::debug!("Running diff with config: {:?}", diff_config);
let result = diff::diff_objs(&config, left, right, None).to_js()?; let result = diff::diff_objs(&diff_config, &mapping_config, left, right, None).to_js()?;
let left = left.and_then(|o| result.left.as_ref().map(|d| (o, d))); let left = left.and_then(|o| result.left.as_ref().map(|d| (o, d)));
let right = right.and_then(|o| result.right.as_ref().map(|d| (o, d))); let right = right.and_then(|o| result.right.as_ref().map(|d| (o, d)));
Ok(DiffResult::new(left, right)) Ok(DiffResult::new(left, right))
@@ -46,9 +48,10 @@ fn run_diff(
pub fn run_diff_proto( pub fn run_diff_proto(
left: Option<Box<[u8]>>, left: Option<Box<[u8]>>,
right: Option<Box<[u8]>>, right: Option<Box<[u8]>>,
config: diff::DiffObjConfig, diff_config: diff::DiffObjConfig,
mapping_config: diff::MappingConfig,
) -> Result<Box<[u8]>, JsError> { ) -> Result<Box<[u8]>, JsError> {
let out = parse_and_run_diff(left, right, config)?; let out = parse_and_run_diff(left, right, diff_config, mapping_config)?;
Ok(out.encode_to_vec().into_boxed_slice()) Ok(out.encode_to_vec().into_boxed_slice())
} }

View File

@@ -1,4 +1,5 @@
use std::{ use std::{
collections::BTreeMap,
fs, fs,
fs::File, fs::File,
io::{BufReader, BufWriter, Read}, io::{BufReader, BufWriter, Read},
@@ -6,11 +7,11 @@ use std::{
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use bimap::BiBTreeMap;
use filetime::FileTime; use filetime::FileTime;
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] #[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
pub struct ProjectConfig { pub struct ProjectConfig {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub min_version: Option<String>, pub min_version: Option<String>,
@@ -27,7 +28,7 @@ pub struct ProjectConfig {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build_target: Option<bool>, pub build_target: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub watch_patterns: Option<Vec<Glob>>, pub watch_patterns: Option<Vec<String>>,
#[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")] #[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")]
pub units: Option<Vec<ProjectObject>>, pub units: Option<Vec<ProjectObject>>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -52,9 +53,21 @@ impl ProjectConfig {
pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> { pub fn progress_categories_mut(&mut self) -> &mut Vec<ProjectProgressCategory> {
self.progress_categories.get_or_insert_with(Vec::new) self.progress_categories.get_or_insert_with(Vec::new)
} }
pub fn build_watch_patterns(&self) -> Result<Vec<Glob>, globset::Error> {
Ok(if let Some(watch_patterns) = &self.watch_patterns {
watch_patterns
.iter()
.map(|s| Glob::new(s))
.collect::<Result<Vec<Glob>, globset::Error>>()?
} else {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
})
}
} }
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] #[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
pub struct ProjectObject { pub struct ProjectObject {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>, pub name: Option<String>,
@@ -78,9 +91,11 @@ pub struct ProjectObject {
pub symbol_mappings: Option<SymbolMappings>, pub symbol_mappings: Option<SymbolMappings>,
} }
pub type SymbolMappings = BiBTreeMap<String, String>; #[cfg_attr(feature = "wasm", tsify_next::declare)]
pub type SymbolMappings = BTreeMap<String, String>;
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] #[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
pub struct ProjectObjectMetadata { pub struct ProjectObjectMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub complete: Option<bool>, pub complete: Option<bool>,
@@ -95,6 +110,7 @@ pub struct ProjectObjectMetadata {
} }
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] #[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
pub struct ProjectProgressCategory { pub struct ProjectProgressCategory {
#[serde(default)] #[serde(default)]
pub id: String, pub id: String,
@@ -154,6 +170,7 @@ impl ProjectObject {
} }
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
pub struct ScratchConfig { pub struct ScratchConfig {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub platform: Option<String>, pub platform: Option<String>,

View File

@@ -3,13 +3,17 @@ use std::{cmp::max, collections::BTreeMap};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use similar::{capture_diff_slices_deadline, Algorithm}; use similar::{capture_diff_slices_deadline, Algorithm};
use super::FunctionRelocDiffs;
use crate::{ use crate::{
arch::ProcessCodeResult, arch::ProcessCodeResult,
diff::{ diff::{
DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind, DiffObjConfig, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjInsDiff, ObjInsDiffKind,
ObjSymbolDiff, ObjSymbolDiff,
}, },
obj::{ObjInfo, ObjInsArg, ObjReloc, ObjSection, ObjSymbol, ObjSymbolFlags, SymbolRef}, obj::{
ObjInfo, ObjIns, ObjInsArg, ObjReloc, ObjSection, ObjSymbol, ObjSymbolFlags, ObjSymbolKind,
SymbolRef,
},
}; };
pub fn process_code_symbol( pub fn process_code_symbol(
@@ -192,7 +196,7 @@ fn address_eq(left: &ObjReloc, right: &ObjReloc) -> bool {
left.target.address as i64 + left.addend == right.target.address as i64 + right.addend left.target.address as i64 + left.addend == right.target.address as i64 + right.addend
} }
fn section_name_eq( pub fn section_name_eq(
left_obj: &ObjInfo, left_obj: &ObjInfo,
right_obj: &ObjInfo, right_obj: &ObjInfo,
left_orig_section_index: usize, left_orig_section_index: usize,
@@ -215,16 +219,19 @@ fn reloc_eq(
config: &DiffObjConfig, config: &DiffObjConfig,
left_obj: &ObjInfo, left_obj: &ObjInfo,
right_obj: &ObjInfo, right_obj: &ObjInfo,
left_reloc: Option<&ObjReloc>, left_ins: Option<&ObjIns>,
right_reloc: Option<&ObjReloc>, right_ins: Option<&ObjIns>,
) -> bool { ) -> bool {
let (Some(left), Some(right)) = (left_reloc, right_reloc) else { let (Some(left_ins), Some(right_ins)) = (left_ins, right_ins) else {
return false;
};
let (Some(left), Some(right)) = (&left_ins.reloc, &right_ins.reloc) else {
return false; return false;
}; };
if left.flags != right.flags { if left.flags != right.flags {
return false; return false;
} }
if config.relax_reloc_diffs { if config.function_reloc_diffs == FunctionRelocDiffs::None {
return true; return true;
} }
@@ -233,7 +240,13 @@ fn reloc_eq(
(Some(sl), Some(sr)) => { (Some(sl), Some(sr)) => {
// Match if section and name or address match // Match if section and name or address match
section_name_eq(left_obj, right_obj, *sl, *sr) section_name_eq(left_obj, right_obj, *sl, *sr)
&& (symbol_name_matches || address_eq(left, right)) && (config.function_reloc_diffs == FunctionRelocDiffs::DataValue
|| symbol_name_matches
|| address_eq(left, right))
&& (config.function_reloc_diffs == FunctionRelocDiffs::NameAddress
|| left.target.kind != ObjSymbolKind::Object
|| left_obj.arch.display_ins_data(left_ins)
== left_obj.arch.display_ins_data(right_ins))
} }
(Some(_), None) => false, (Some(_), None) => false,
(None, Some(_)) => { (None, Some(_)) => {
@@ -259,10 +272,10 @@ fn arg_eq(
_ => false, _ => false,
}, },
ObjInsArg::Arg(l) => match right { ObjInsArg::Arg(l) => match right {
ObjInsArg::Arg(r) => l == r, ObjInsArg::Arg(r) => l.loose_eq(r),
// If relocations are relaxed, match if left is a constant and right is a reloc // If relocations are relaxed, match if left is a constant and right is a reloc
// Useful for instances where the target object is created without relocations // Useful for instances where the target object is created without relocations
ObjInsArg::Reloc => config.relax_reloc_diffs, ObjInsArg::Reloc => config.function_reloc_diffs == FunctionRelocDiffs::None,
_ => false, _ => false,
}, },
ObjInsArg::Reloc => { ObjInsArg::Reloc => {
@@ -271,8 +284,8 @@ fn arg_eq(
config, config,
left_obj, left_obj,
right_obj, right_obj,
left_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()), left_diff.ins.as_ref(),
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()), right_diff.ins.as_ref(),
) )
} }
ObjInsArg::BranchDest(_) => match right { ObjInsArg::BranchDest(_) => match right {
@@ -283,7 +296,7 @@ fn arg_eq(
} }
// If relocations are relaxed, match if left is a constant and right is a reloc // If relocations are relaxed, match if left is a constant and right is a reloc
// Useful for instances where the target object is created without relocations // Useful for instances where the target object is created without relocations
ObjInsArg::Reloc => config.relax_reloc_diffs, ObjInsArg::Reloc => config.function_reloc_diffs == FunctionRelocDiffs::None,
_ => false, _ => false,
}, },
} }

View File

@@ -1,11 +1,15 @@
use std::cmp::{max, min, Ordering}; use std::{
cmp::{max, min, Ordering},
ops::Range,
};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use similar::{capture_diff_slices_deadline, get_diff_ratio, Algorithm}; use similar::{capture_diff_slices_deadline, get_diff_ratio, Algorithm};
use super::code::section_name_eq;
use crate::{ use crate::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjSectionDiff, ObjSymbolDiff}, diff::{ObjDataDiff, ObjDataDiffKind, ObjDataRelocDiff, ObjSectionDiff, ObjSymbolDiff},
obj::{ObjInfo, ObjSection, SymbolRef}, obj::{ObjInfo, ObjReloc, ObjSection, ObjSymbolFlags, SymbolRef},
}; };
pub fn diff_bss_symbol( pub fn diff_bss_symbol(
@@ -37,8 +41,110 @@ pub fn no_diff_symbol(_obj: &ObjInfo, symbol_ref: SymbolRef) -> ObjSymbolDiff {
ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: vec![], match_percent: None } ObjSymbolDiff { symbol_ref, target_symbol: None, instructions: vec![], match_percent: None }
} }
fn address_eq(left: &ObjReloc, right: &ObjReloc) -> bool {
if right.target.size == 0 && left.target.size != 0 {
// The base relocation is against a pool but the target relocation isn't.
// This can happen in rare cases where the compiler will generate a pool+addend relocation
// in the base, but the one detected in the target is direct with no addend.
// Just check that the final address is the same so these count as a match.
left.target.address as i64 + left.addend == right.target.address as i64 + right.addend
} else {
// But otherwise, if the compiler isn't using a pool, we're more strict and check that the
// target symbol address and relocation addend both match exactly.
left.target.address == right.target.address && left.addend == right.addend
}
}
fn reloc_eq(left_obj: &ObjInfo, right_obj: &ObjInfo, left: &ObjReloc, right: &ObjReloc) -> bool {
if left.flags != right.flags {
return false;
}
let symbol_name_matches = left.target.name == right.target.name;
match (&left.target.orig_section_index, &right.target.orig_section_index) {
(Some(sl), Some(sr)) => {
// Match if section and name+addend or address match
section_name_eq(left_obj, right_obj, *sl, *sr)
&& ((symbol_name_matches && left.addend == right.addend) || address_eq(left, right))
}
(Some(_), None) => false,
(None, Some(_)) => {
// Match if possibly stripped weak symbol
(symbol_name_matches && left.addend == right.addend)
&& right.target.flags.0.contains(ObjSymbolFlags::Weak)
}
(None, None) => symbol_name_matches,
}
}
/// Compares relocations contained with a certain data range.
/// The ObjDataDiffKind for each diff will either be `None`` (if the relocation matches),
/// or `Replace` (if a relocation was changed, added, or removed).
/// `Insert` and `Delete` are not used when a relocation is added or removed to avoid confusing diffs
/// where it looks like the bytes themselves were changed but actually only the relocations changed.
fn diff_data_relocs_for_range(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left: &ObjSection,
right: &ObjSection,
left_range: Range<usize>,
right_range: Range<usize>,
) -> Vec<(ObjDataDiffKind, Option<ObjReloc>, Option<ObjReloc>)> {
let mut diffs = Vec::new();
for left_reloc in left.relocations.iter() {
if !left_range.contains(&(left_reloc.address as usize)) {
continue;
}
let left_offset = left_reloc.address as usize - left_range.start;
let Some(right_reloc) = right.relocations.iter().find(|r| {
if !right_range.contains(&(r.address as usize)) {
return false;
}
let right_offset = r.address as usize - right_range.start;
right_offset == left_offset
}) else {
diffs.push((ObjDataDiffKind::Delete, Some(left_reloc.clone()), None));
continue;
};
if reloc_eq(left_obj, right_obj, left_reloc, right_reloc) {
diffs.push((
ObjDataDiffKind::None,
Some(left_reloc.clone()),
Some(right_reloc.clone()),
));
} else {
diffs.push((
ObjDataDiffKind::Replace,
Some(left_reloc.clone()),
Some(right_reloc.clone()),
));
}
}
for right_reloc in right.relocations.iter() {
if !right_range.contains(&(right_reloc.address as usize)) {
continue;
}
let right_offset = right_reloc.address as usize - right_range.start;
let Some(_) = left.relocations.iter().find(|r| {
if !left_range.contains(&(r.address as usize)) {
return false;
}
let left_offset = r.address as usize - left_range.start;
left_offset == right_offset
}) else {
diffs.push((ObjDataDiffKind::Insert, None, Some(right_reloc.clone())));
continue;
};
// No need to check the cases for relocations being deleted or matching again.
// They were already handled in the loop over the left relocs.
}
diffs
}
/// Compare the data sections of two object files. /// Compare the data sections of two object files.
pub fn diff_data_section( pub fn diff_data_section(
left_obj: &ObjInfo,
right_obj: &ObjInfo,
left: &ObjSection, left: &ObjSection,
right: &ObjSection, right: &ObjSection,
left_section_diff: &ObjSectionDiff, left_section_diff: &ObjSectionDiff,
@@ -121,17 +227,45 @@ pub fn diff_data_section(
} }
} }
let mut left_reloc_diffs = Vec::new();
let mut right_reloc_diffs = Vec::new();
for (diff_kind, left_reloc, right_reloc) in diff_data_relocs_for_range(
left_obj,
right_obj,
left,
right,
0..left_max as usize,
0..right_max as usize,
) {
if let Some(left_reloc) = left_reloc {
let len = left_obj.arch.get_reloc_byte_size(left_reloc.flags);
let range = left_reloc.address as usize..left_reloc.address as usize + len;
left_reloc_diffs.push(ObjDataRelocDiff { reloc: left_reloc, kind: diff_kind, range });
}
if let Some(right_reloc) = right_reloc {
let len = right_obj.arch.get_reloc_byte_size(right_reloc.flags);
let range = right_reloc.address as usize..right_reloc.address as usize + len;
right_reloc_diffs.push(ObjDataRelocDiff { reloc: right_reloc, kind: diff_kind, range });
}
}
let (mut left_section_diff, mut right_section_diff) = let (mut left_section_diff, mut right_section_diff) =
diff_generic_section(left, right, left_section_diff, right_section_diff)?; diff_generic_section(left, right, left_section_diff, right_section_diff)?;
let all_left_relocs_match = left_reloc_diffs.iter().all(|d| d.kind == ObjDataDiffKind::None);
left_section_diff.data_diff = left_diff; left_section_diff.data_diff = left_diff;
right_section_diff.data_diff = right_diff; right_section_diff.data_diff = right_diff;
left_section_diff.reloc_diff = left_reloc_diffs;
right_section_diff.reloc_diff = right_reloc_diffs;
if all_left_relocs_match {
// Use the highest match percent between two options: // Use the highest match percent between two options:
// - Left symbols matching right symbols by name // - Left symbols matching right symbols by name
// - Diff of the data itself // - Diff of the data itself
// We only do this when all relocations on the left side match.
if left_section_diff.match_percent.unwrap_or(-1.0) < match_percent { if left_section_diff.match_percent.unwrap_or(-1.0) < match_percent {
left_section_diff.match_percent = Some(match_percent); left_section_diff.match_percent = Some(match_percent);
right_section_diff.match_percent = Some(match_percent); right_section_diff.match_percent = Some(match_percent);
} }
}
Ok((left_section_diff, right_section_diff)) Ok((left_section_diff, right_section_diff))
} }
@@ -147,13 +281,54 @@ pub fn diff_data_symbol(
let left_section = left_section.ok_or_else(|| anyhow!("Data symbol section not found"))?; let left_section = left_section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
let right_section = right_section.ok_or_else(|| anyhow!("Data symbol section not found"))?; let right_section = right_section.ok_or_else(|| anyhow!("Data symbol section not found"))?;
let left_data = &left_section.data[left_symbol.section_address as usize let left_range = left_symbol.section_address as usize
..(left_symbol.section_address + left_symbol.size) as usize]; ..(left_symbol.section_address + left_symbol.size) as usize;
let right_data = &right_section.data[right_symbol.section_address as usize let right_range = right_symbol.section_address as usize
..(right_symbol.section_address + right_symbol.size) as usize]; ..(right_symbol.section_address + right_symbol.size) as usize;
let left_data = &left_section.data[left_range.clone()];
let right_data = &right_section.data[right_range.clone()];
let reloc_diffs = diff_data_relocs_for_range(
left_obj,
right_obj,
left_section,
right_section,
left_range,
right_range,
);
let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None); let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None);
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0; let bytes_match_ratio = get_diff_ratio(&ops, left_data.len(), right_data.len());
let mut match_ratio = bytes_match_ratio;
if !reloc_diffs.is_empty() {
let mut total_reloc_bytes = 0;
let mut matching_reloc_bytes = 0;
for (diff_kind, left_reloc, right_reloc) in reloc_diffs {
let reloc_diff_len = match (left_reloc, right_reloc) {
(None, None) => unreachable!(),
(None, Some(right_reloc)) => right_obj.arch.get_reloc_byte_size(right_reloc.flags),
(Some(left_reloc), _) => left_obj.arch.get_reloc_byte_size(left_reloc.flags),
};
total_reloc_bytes += reloc_diff_len;
if diff_kind == ObjDataDiffKind::None {
matching_reloc_bytes += reloc_diff_len;
}
}
if total_reloc_bytes > 0 {
let relocs_match_ratio = matching_reloc_bytes as f32 / total_reloc_bytes as f32;
// Adjust the overall match ratio to include relocation differences.
// We calculate it so that bytes that contain a relocation are counted twice: once for the
// byte's raw value, and once for its relocation.
// e.g. An 8 byte symbol that has 8 matching raw bytes and a single 4 byte relocation that
// doesn't match would show as 66% (weighted average of 100% and 0%).
match_ratio = ((bytes_match_ratio * (left_data.len() as f32))
+ (relocs_match_ratio * total_reloc_bytes as f32))
/ (left_data.len() + total_reloc_bytes) as f32;
}
}
let match_percent = match_ratio * 100.0;
Ok(( Ok((
ObjSymbolDiff { ObjSymbolDiff {
@@ -190,8 +365,18 @@ pub fn diff_generic_section(
/ left.size as f32 / left.size as f32
}; };
Ok(( Ok((
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) }, ObjSectionDiff {
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) }, symbols: vec![],
data_diff: vec![],
reloc_diff: vec![],
match_percent: Some(match_percent),
},
ObjSectionDiff {
symbols: vec![],
data_diff: vec![],
reloc_diff: vec![],
match_percent: Some(match_percent),
},
)) ))
} }
@@ -216,7 +401,17 @@ pub fn diff_bss_section(
} }
Ok(( Ok((
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) }, ObjSectionDiff {
ObjSectionDiff { symbols: vec![], data_diff: vec![], match_percent: Some(match_percent) }, symbols: vec![],
data_diff: vec![],
reloc_diff: vec![],
match_percent: Some(match_percent),
},
ObjSectionDiff {
symbols: vec![],
data_diff: vec![],
reloc_diff: vec![],
match_percent: Some(match_percent),
},
)) ))
} }

View File

@@ -1,4 +1,4 @@
use std::collections::HashSet; use std::{collections::HashSet, ops::Range};
use anyhow::Result; use anyhow::Result;
@@ -11,195 +11,16 @@ use crate::{
diff_generic_section, no_diff_symbol, diff_generic_section, no_diff_symbol,
}, },
}, },
obj::{ObjInfo, ObjIns, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef, SECTION_COMMON}, obj::{
ObjInfo, ObjIns, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef, SECTION_COMMON,
},
}; };
pub mod code; pub mod code;
pub mod data; pub mod data;
pub mod display; pub mod display;
#[derive( include!(concat!(env!("OUT_DIR"), "/config.gen.rs"));
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum X86Formatter {
#[default]
#[strum(message = "Intel (default)")]
Intel,
#[strum(message = "AT&T")]
Gas,
#[strum(message = "NASM")]
Nasm,
#[strum(message = "MASM")]
Masm,
}
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum MipsAbi {
#[default]
#[strum(message = "Auto (default)")]
Auto,
#[strum(message = "O32")]
O32,
#[strum(message = "N32")]
N32,
#[strum(message = "N64")]
N64,
}
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum MipsInstrCategory {
#[default]
#[strum(message = "Auto (default)")]
Auto,
#[strum(message = "CPU")]
Cpu,
#[strum(message = "RSP (N64)")]
Rsp,
#[strum(message = "R3000 GTE (PS1)")]
R3000Gte,
#[strum(message = "R4000 ALLEGREX (PSP)")]
R4000Allegrex,
#[strum(message = "R5900 EE (PS2)")]
R5900,
}
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum ArmArchVersion {
#[default]
#[strum(message = "Auto (default)")]
Auto,
#[strum(message = "ARMv4T (GBA)")]
V4T,
#[strum(message = "ARMv5TE (DS)")]
V5TE,
#[strum(message = "ARMv6K (3DS)")]
V6K,
}
#[derive(
Debug,
Copy,
Clone,
Default,
Eq,
PartialEq,
serde::Deserialize,
serde::Serialize,
strum::VariantArray,
strum::EnumMessage,
)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
pub enum ArmR9Usage {
#[default]
#[strum(
message = "R9 or V6 (default)",
detailed_message = "Use R9 as a general-purpose register."
)]
GeneralPurpose,
#[strum(
message = "SB (static base)",
detailed_message = "Used for position-independent data (PID)."
)]
Sb,
#[strum(message = "TR (TLS register)", detailed_message = "Used for thread-local storage.")]
Tr,
}
#[inline]
const fn default_true() -> bool { true }
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))]
#[cfg_attr(feature = "wasm", tsify(from_wasm_abi))]
#[serde(default)]
pub struct DiffObjConfig {
pub relax_reloc_diffs: bool,
#[serde(default = "default_true")]
pub space_between_args: bool,
pub combine_data_sections: bool,
#[serde(default)]
pub symbol_mappings: MappingConfig,
// x86
pub x86_formatter: X86Formatter,
// MIPS
pub mips_abi: MipsAbi,
pub mips_instr_category: MipsInstrCategory,
// ARM
pub arm_arch_version: ArmArchVersion,
pub arm_unified_syntax: bool,
pub arm_av_registers: bool,
pub arm_r9_usage: ArmR9Usage,
pub arm_sl_usage: bool,
pub arm_fp_usage: bool,
pub arm_ip_usage: bool,
}
impl Default for DiffObjConfig {
fn default() -> Self {
Self {
relax_reloc_diffs: false,
space_between_args: true,
combine_data_sections: false,
symbol_mappings: Default::default(),
x86_formatter: Default::default(),
mips_abi: Default::default(),
mips_instr_category: Default::default(),
arm_arch_version: Default::default(),
arm_unified_syntax: true,
arm_av_registers: false,
arm_r9_usage: Default::default(),
arm_sl_usage: false,
arm_fp_usage: false,
arm_ip_usage: false,
}
}
}
impl DiffObjConfig { impl DiffObjConfig {
pub fn separator(&self) -> &'static str { pub fn separator(&self) -> &'static str {
@@ -215,6 +36,7 @@ impl DiffObjConfig {
pub struct ObjSectionDiff { pub struct ObjSectionDiff {
pub symbols: Vec<ObjSymbolDiff>, pub symbols: Vec<ObjSymbolDiff>,
pub data_diff: Vec<ObjDataDiff>, pub data_diff: Vec<ObjDataDiff>,
pub reloc_diff: Vec<ObjDataRelocDiff>,
pub match_percent: Option<f32>, pub match_percent: Option<f32>,
} }
@@ -222,6 +44,7 @@ impl ObjSectionDiff {
fn merge(&mut self, other: ObjSectionDiff) { fn merge(&mut self, other: ObjSectionDiff) {
// symbols ignored // symbols ignored
self.data_diff = other.data_diff; self.data_diff = other.data_diff;
self.reloc_diff = other.reloc_diff;
self.match_percent = other.match_percent; self.match_percent = other.match_percent;
} }
} }
@@ -268,6 +91,13 @@ pub struct ObjDataDiff {
pub symbol: String, pub symbol: String,
} }
#[derive(Debug, Clone)]
pub struct ObjDataRelocDiff {
pub reloc: ObjReloc,
pub kind: ObjDataDiffKind,
pub range: Range<usize>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub enum ObjDataDiffKind { pub enum ObjDataDiffKind {
#[default] #[default]
@@ -335,6 +165,7 @@ impl ObjDiff {
len: section.data.len(), len: section.data.len(),
symbol: section.name.clone(), symbol: section.name.clone(),
}], }],
reloc_diff: vec![],
match_percent: None, match_percent: None,
}); });
} }
@@ -386,12 +217,13 @@ pub struct DiffObjsResult {
} }
pub fn diff_objs( pub fn diff_objs(
config: &DiffObjConfig, diff_config: &DiffObjConfig,
mapping_config: &MappingConfig,
left: Option<&ObjInfo>, left: Option<&ObjInfo>,
right: Option<&ObjInfo>, right: Option<&ObjInfo>,
prev: Option<&ObjInfo>, prev: Option<&ObjInfo>,
) -> Result<DiffObjsResult> { ) -> Result<DiffObjsResult> {
let symbol_matches = matching_symbols(left, right, prev, &config.symbol_mappings)?; let symbol_matches = matching_symbols(left, right, prev, mapping_config)?;
let section_matches = matching_sections(left, right)?; let section_matches = matching_sections(left, right)?;
let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p))); let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p)));
let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p))); let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p)));
@@ -409,8 +241,10 @@ pub fn diff_objs(
let (right_obj, right_out) = right.as_mut().unwrap(); let (right_obj, right_out) = right.as_mut().unwrap();
match section_kind { match section_kind {
ObjSectionKind::Code => { ObjSectionKind::Code => {
let left_code = process_code_symbol(left_obj, left_symbol_ref, config)?; let left_code =
let right_code = process_code_symbol(right_obj, right_symbol_ref, config)?; process_code_symbol(left_obj, left_symbol_ref, diff_config)?;
let right_code =
process_code_symbol(right_obj, right_symbol_ref, diff_config)?;
let (left_diff, right_diff) = diff_code( let (left_diff, right_diff) = diff_code(
left_obj, left_obj,
right_obj, right_obj,
@@ -418,14 +252,15 @@ pub fn diff_objs(
&right_code, &right_code,
left_symbol_ref, left_symbol_ref,
right_symbol_ref, right_symbol_ref,
config, diff_config,
)?; )?;
*left_out.symbol_diff_mut(left_symbol_ref) = left_diff; *left_out.symbol_diff_mut(left_symbol_ref) = left_diff;
*right_out.symbol_diff_mut(right_symbol_ref) = right_diff; *right_out.symbol_diff_mut(right_symbol_ref) = right_diff;
if let Some(prev_symbol_ref) = prev_symbol_ref { if let Some(prev_symbol_ref) = prev_symbol_ref {
let (prev_obj, prev_out) = prev.as_mut().unwrap(); let (prev_obj, prev_out) = prev.as_mut().unwrap();
let prev_code = process_code_symbol(prev_obj, prev_symbol_ref, config)?; let prev_code =
process_code_symbol(prev_obj, prev_symbol_ref, diff_config)?;
let (_, prev_diff) = diff_code( let (_, prev_diff) = diff_code(
left_obj, left_obj,
right_obj, right_obj,
@@ -433,7 +268,7 @@ pub fn diff_objs(
&prev_code, &prev_code,
right_symbol_ref, right_symbol_ref,
prev_symbol_ref, prev_symbol_ref,
config, diff_config,
)?; )?;
*prev_out.symbol_diff_mut(prev_symbol_ref) = prev_diff; *prev_out.symbol_diff_mut(prev_symbol_ref) = prev_diff;
} }
@@ -464,7 +299,7 @@ pub fn diff_objs(
let (left_obj, left_out) = left.as_mut().unwrap(); let (left_obj, left_out) = left.as_mut().unwrap();
match section_kind { match section_kind {
ObjSectionKind::Code => { ObjSectionKind::Code => {
let code = process_code_symbol(left_obj, left_symbol_ref, config)?; let code = process_code_symbol(left_obj, left_symbol_ref, diff_config)?;
*left_out.symbol_diff_mut(left_symbol_ref) = *left_out.symbol_diff_mut(left_symbol_ref) =
no_diff_code(&code, left_symbol_ref)?; no_diff_code(&code, left_symbol_ref)?;
} }
@@ -478,7 +313,7 @@ pub fn diff_objs(
let (right_obj, right_out) = right.as_mut().unwrap(); let (right_obj, right_out) = right.as_mut().unwrap();
match section_kind { match section_kind {
ObjSectionKind::Code => { ObjSectionKind::Code => {
let code = process_code_symbol(right_obj, right_symbol_ref, config)?; let code = process_code_symbol(right_obj, right_symbol_ref, diff_config)?;
*right_out.symbol_diff_mut(right_symbol_ref) = *right_out.symbol_diff_mut(right_symbol_ref) =
no_diff_code(&code, right_symbol_ref)?; no_diff_code(&code, right_symbol_ref)?;
} }
@@ -522,6 +357,8 @@ pub fn diff_objs(
let left_section_diff = left_out.section_diff(left_section_idx); let left_section_diff = left_out.section_diff(left_section_idx);
let right_section_diff = right_out.section_diff(right_section_idx); let right_section_diff = right_out.section_diff(right_section_idx);
let (left_diff, right_diff) = diff_data_section( let (left_diff, right_diff) = diff_data_section(
left_obj,
right_obj,
left_section, left_section,
right_section, right_section,
left_section_diff, left_section_diff,
@@ -549,11 +386,11 @@ pub fn diff_objs(
if let (Some((right_obj, right_out)), Some((left_obj, left_out))) = if let (Some((right_obj, right_out)), Some((left_obj, left_out))) =
(right.as_mut(), left.as_mut()) (right.as_mut(), left.as_mut())
{ {
if let Some(right_name) = &config.symbol_mappings.selecting_left { if let Some(right_name) = &mapping_config.selecting_left {
generate_mapping_symbols(right_obj, right_name, left_obj, left_out, config)?; generate_mapping_symbols(right_obj, right_name, left_obj, left_out, diff_config)?;
} }
if let Some(left_name) = &config.symbol_mappings.selecting_right { if let Some(left_name) = &mapping_config.selecting_right {
generate_mapping_symbols(left_obj, left_name, right_obj, right_out, config)?; generate_mapping_symbols(left_obj, left_name, right_obj, right_out, diff_config)?;
} }
} }
@@ -636,7 +473,9 @@ struct SectionMatch {
section_kind: ObjSectionKind, section_kind: ObjSectionKind,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)] #[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
#[serde(default)]
pub struct MappingConfig { pub struct MappingConfig {
/// Manual symbol mappings /// Manual symbol mappings
pub mappings: SymbolMappings, pub mappings: SymbolMappings,

View File

@@ -5,7 +5,6 @@ use time::OffsetDateTime;
use crate::{ use crate::{
build::{run_make, BuildConfig, BuildStatus}, build::{run_make, BuildConfig, BuildStatus},
config::SymbolMappings,
diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff}, diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff},
jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, jobs::{start_job, update_status, Job, JobContext, JobResult, JobState},
obj::{read, ObjInfo}, obj::{read, ObjInfo},
@@ -18,9 +17,7 @@ pub struct ObjDiffConfig {
pub target_path: Option<PathBuf>, pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>, pub base_path: Option<PathBuf>,
pub diff_obj_config: DiffObjConfig, pub diff_obj_config: DiffObjConfig,
pub symbol_mappings: SymbolMappings, pub mapping_config: MappingConfig,
pub selecting_left: Option<String>,
pub selecting_right: Option<String>,
} }
pub struct ObjDiffResult { pub struct ObjDiffResult {
@@ -34,15 +31,8 @@ pub struct ObjDiffResult {
fn run_build( fn run_build(
context: &JobContext, context: &JobContext,
cancel: Receiver<()>, cancel: Receiver<()>,
mut config: ObjDiffConfig, config: ObjDiffConfig,
) -> Result<Box<ObjDiffResult>> { ) -> Result<Box<ObjDiffResult>> {
// Use the per-object symbol mappings, we don't set mappings globally
config.diff_obj_config.symbol_mappings = MappingConfig {
mappings: config.symbol_mappings,
selecting_left: config.selecting_left,
selecting_right: config.selecting_right,
};
let mut target_path_rel = None; let mut target_path_rel = None;
let mut base_path_rel = None; let mut base_path_rel = None;
if config.build_target || config.build_base { if config.build_target || config.build_base {
@@ -180,7 +170,13 @@ fn run_build(
update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?; update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?;
step_idx += 1; step_idx += 1;
let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?; let result = diff_objs(
&config.diff_obj_config,
&config.mapping_config,
first_obj.as_ref(),
second_obj.as_ref(),
None,
)?;
update_status(context, "Complete".to_string(), step_idx, total, &cancel)?; update_status(context, "Complete".to_string(), step_idx, total, &cancel)?;
Ok(Box::new(ObjDiffResult { Ok(Box::new(ObjDiffResult {

View File

@@ -28,7 +28,7 @@ flags! {
HasExtra, HasExtra,
} }
} }
#[derive(Debug, Copy, Clone, Default)] #[derive(Debug, Copy, Clone, Default, PartialEq)]
pub struct ObjSymbolFlagSet(pub FlagSet<ObjSymbolFlags>); pub struct ObjSymbolFlagSet(pub FlagSet<ObjSymbolFlags>);
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -132,7 +132,7 @@ pub enum ObjSymbolKind {
Section, Section,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct ObjSymbol { pub struct ObjSymbol {
pub name: String, pub name: String,
pub demangled_name: Option<String>, pub demangled_name: Option<String>,
@@ -161,7 +161,7 @@ pub struct ObjInfo {
pub split_meta: Option<SplitMeta>, pub split_meta: Option<SplitMeta>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct ObjReloc { pub struct ObjReloc {
pub flags: RelocationFlags, pub flags: RelocationFlags,
pub address: u64, pub address: u64,

View File

@@ -31,8 +31,8 @@ const_format = "0.2"
cwdemangle = "1.0" cwdemangle = "1.0"
cwextab = "1.0" cwextab = "1.0"
dirs = "5.0" dirs = "5.0"
egui = "0.29" egui = "0.30"
egui_extras = "0.29" egui_extras = "0.30"
filetime = "0.2" filetime = "0.2"
float-ord = "0.3" float-ord = "0.3"
font-kit = "0.14" font-kit = "0.14"
@@ -54,7 +54,7 @@ time = { version = "0.3", features = ["formatting", "local-offset"] }
# Keep version in sync with egui # Keep version in sync with egui
[dependencies.eframe] [dependencies.eframe]
version = "0.29" version = "0.30"
features = [ features = [
"default_fonts", "default_fonts",
"persistence", "persistence",
@@ -65,7 +65,7 @@ default-features = false
# Keep version in sync with eframe # Keep version in sync with eframe
[dependencies.wgpu] [dependencies.wgpu]
version = "22.1" version = "23.0"
features = [ features = [
"dx12", "dx12",
"metal", "metal",
@@ -93,4 +93,4 @@ tracing-wasm = "0.2"
anyhow = "1.0" anyhow = "1.0"
[target.'cfg(windows)'.build-dependencies] [target.'cfg(windows)'.build-dependencies]
tauri-winres = "0.1" tauri-winres = "0.2"

View File

@@ -30,7 +30,8 @@ use crate::{
views::{ views::{
appearance::{appearance_window, Appearance}, appearance::{appearance_window, Appearance},
config::{ config::{
arch_config_window, config_ui, project_window, ConfigViewState, CONFIG_DISABLED_TEXT, arch_config_window, config_ui, general_config_ui, project_window, ConfigViewState,
CONFIG_DISABLED_TEXT,
}, },
data_diff::data_diff_ui, data_diff::data_diff_ui,
debug::debug_window, debug::debug_window,
@@ -293,7 +294,7 @@ impl AppState {
let Some(object) = self.config.selected_obj.as_mut() else { let Some(object) = self.config.selected_obj.as_mut() else {
return; return;
}; };
object.symbol_mappings.remove_by_right(right); object.symbol_mappings.retain(|_, r| r != right);
self.selecting_left = Some(right.to_string()); self.selecting_left = Some(right.to_string());
self.queue_reload = true; self.queue_reload = true;
self.save_config(); self.save_config();
@@ -303,7 +304,7 @@ impl AppState {
let Some(object) = self.config.selected_obj.as_mut() else { let Some(object) = self.config.selected_obj.as_mut() else {
return; return;
}; };
object.symbol_mappings.remove_by_left(left); object.symbol_mappings.retain(|l, _| l != left);
self.selecting_right = Some(left.to_string()); self.selecting_right = Some(left.to_string());
self.queue_reload = true; self.queue_reload = true;
self.save_config(); self.save_config();
@@ -316,10 +317,8 @@ impl AppState {
}; };
self.selecting_left = None; self.selecting_left = None;
self.selecting_right = None; self.selecting_right = None;
if left == right { object.symbol_mappings.retain(|l, r| l != &left && r != &right);
object.symbol_mappings.remove_by_left(&left); if left != right {
object.symbol_mappings.remove_by_right(&right);
} else {
object.symbol_mappings.insert(left.clone(), right.clone()); object.symbol_mappings.insert(left.clone(), right.clone());
} }
self.queue_reload = true; self.queue_reload = true;
@@ -728,37 +727,9 @@ impl eframe::App for App {
&mut diff_state.symbol_state.show_hidden_symbols, &mut diff_state.symbol_state.show_hidden_symbols,
"Show hidden symbols", "Show hidden symbols",
); );
if ui ui.separator();
.checkbox( general_config_ui(ui, &mut state);
&mut state.config.diff_obj_config.relax_reloc_diffs, ui.separator();
"Relax relocation diffs",
)
.on_hover_text(
"Ignores differences in relocation targets. (Address, name, etc)",
)
.changed()
{
state.queue_reload = true;
}
if ui
.checkbox(
&mut state.config.diff_obj_config.space_between_args,
"Space between args",
)
.changed()
{
state.queue_reload = true;
}
if ui
.checkbox(
&mut state.config.diff_obj_config.combine_data_sections,
"Combine data sections",
)
.on_hover_text("Combines data sections with equal names.")
.changed()
{
state.queue_reload = true;
}
if ui.button("Clear custom symbol mappings").clicked() { if ui.button("Clear custom symbol mappings").clicked() {
state.clear_mappings(); state.clear_mappings();
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff()); diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());

View File

@@ -3,8 +3,11 @@ use std::path::PathBuf;
use eframe::Storage; use eframe::Storage;
use globset::Glob; use globset::Glob;
use objdiff_core::{ use objdiff_core::{
config::ScratchConfig, config::{ScratchConfig, SymbolMappings},
diff::{ArmArchVersion, ArmR9Usage, DiffObjConfig, MipsAbi, MipsInstrCategory, X86Formatter}, diff::{
ArmArchVersion, ArmR9Usage, DiffObjConfig, FunctionRelocDiffs, MipsAbi, MipsInstrCategory,
X86Formatter,
},
}; };
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY}; use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
@@ -15,7 +18,7 @@ pub struct AppConfigVersion {
} }
impl Default for AppConfigVersion { impl Default for AppConfigVersion {
fn default() -> Self { Self { version: 2 } } fn default() -> Self { Self { version: 3 } }
} }
/// Deserialize the AppConfig from storage, handling upgrades from older versions. /// Deserialize the AppConfig from storage, handling upgrades from older versions.
@@ -23,7 +26,8 @@ pub fn deserialize_config(storage: &dyn Storage) -> Option<AppConfig> {
let str = storage.get_string(CONFIG_KEY)?; let str = storage.get_string(CONFIG_KEY)?;
match ron::from_str::<AppConfigVersion>(&str) { match ron::from_str::<AppConfigVersion>(&str) {
Ok(version) => match version.version { Ok(version) => match version.version {
2 => from_str::<AppConfig>(&str), 3 => from_str::<AppConfig>(&str),
2 => from_str::<AppConfigV2>(&str).map(|c| c.into_config()),
1 => from_str::<AppConfigV1>(&str).map(|c| c.into_config()), 1 => from_str::<AppConfigV1>(&str).map(|c| c.into_config()),
_ => { _ => {
log::warn!("Unknown config version: {}", version.version); log::warn!("Unknown config version: {}", version.version);
@@ -49,6 +53,119 @@ where T: serde::de::DeserializeOwned {
} }
} }
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ScratchConfigV2 {
#[serde(default)]
pub platform: Option<String>,
#[serde(default)]
pub compiler: Option<String>,
#[serde(default)]
pub c_flags: Option<String>,
#[serde(default)]
pub ctx_path: Option<PathBuf>,
#[serde(default)]
pub build_ctx: Option<bool>,
#[serde(default)]
pub preset_id: Option<u32>,
}
impl ScratchConfigV2 {
fn into_config(self) -> ScratchConfig {
ScratchConfig {
platform: self.platform,
compiler: self.compiler,
c_flags: self.c_flags,
ctx_path: self.ctx_path,
build_ctx: self.build_ctx,
preset_id: self.preset_id,
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV2 {
pub name: String,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
pub scratch: Option<ScratchConfigV2>,
pub source_path: Option<String>,
#[serde(default)]
pub symbol_mappings: SymbolMappings,
}
impl ObjectConfigV2 {
fn into_config(self) -> ObjectConfig {
ObjectConfig {
name: self.name,
target_path: self.target_path,
base_path: self.base_path,
reverse_fn_order: self.reverse_fn_order,
complete: self.complete,
scratch: self.scratch.map(|scratch| scratch.into_config()),
source_path: self.source_path,
symbol_mappings: self.symbol_mappings,
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct AppConfigV2 {
pub version: u32,
#[serde(default)]
pub custom_make: Option<String>,
#[serde(default)]
pub custom_args: Option<Vec<String>>,
#[serde(default)]
pub selected_wsl_distro: Option<String>,
#[serde(default)]
pub project_dir: Option<PathBuf>,
#[serde(default)]
pub target_obj_dir: Option<PathBuf>,
#[serde(default)]
pub base_obj_dir: Option<PathBuf>,
#[serde(default)]
pub selected_obj: Option<ObjectConfigV2>,
#[serde(default = "bool_true")]
pub build_base: bool,
#[serde(default)]
pub build_target: bool,
#[serde(default = "bool_true")]
pub rebuild_on_changes: bool,
#[serde(default)]
pub auto_update_check: bool,
#[serde(default)]
pub watch_patterns: Vec<Glob>,
#[serde(default)]
pub recent_projects: Vec<PathBuf>,
#[serde(default)]
pub diff_obj_config: DiffObjConfigV1,
}
impl AppConfigV2 {
fn into_config(self) -> AppConfig {
log::info!("Upgrading configuration from v2");
AppConfig {
custom_make: self.custom_make,
custom_args: self.custom_args,
selected_wsl_distro: self.selected_wsl_distro,
project_dir: self.project_dir,
target_obj_dir: self.target_obj_dir,
base_obj_dir: self.base_obj_dir,
selected_obj: self.selected_obj.map(|obj| obj.into_config()),
build_base: self.build_base,
build_target: self.build_target,
rebuild_on_changes: self.rebuild_on_changes,
auto_update_check: self.auto_update_check,
watch_patterns: self.watch_patterns,
recent_projects: self.recent_projects,
diff_obj_config: self.diff_obj_config.into_config(),
..Default::default()
}
}
}
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
pub struct ScratchConfigV1 { pub struct ScratchConfigV1 {
#[serde(default)] #[serde(default)]
@@ -147,7 +264,11 @@ impl Default for DiffObjConfigV1 {
impl DiffObjConfigV1 { impl DiffObjConfigV1 {
fn into_config(self) -> DiffObjConfig { fn into_config(self) -> DiffObjConfig {
DiffObjConfig { DiffObjConfig {
relax_reloc_diffs: self.relax_reloc_diffs, function_reloc_diffs: if self.relax_reloc_diffs {
FunctionRelocDiffs::None
} else {
FunctionRelocDiffs::default()
},
space_between_args: self.space_between_args, space_between_args: self.space_between_args,
combine_data_sections: self.combine_data_sections, combine_data_sections: self.combine_data_sections,
x86_formatter: self.x86_formatter, x86_formatter: self.x86_formatter,
@@ -160,7 +281,6 @@ impl DiffObjConfigV1 {
arm_sl_usage: self.arm_sl_usage, arm_sl_usage: self.arm_sl_usage,
arm_fp_usage: self.arm_fp_usage, arm_fp_usage: self.arm_fp_usage,
arm_ip_usage: self.arm_ip_usage, arm_ip_usage: self.arm_ip_usage,
..Default::default()
} }
} }
} }

View File

@@ -99,9 +99,15 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> {
state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p)); state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p));
state.config.build_base = project_config.build_base.unwrap_or(true); state.config.build_base = project_config.build_base.unwrap_or(true);
state.config.build_target = project_config.build_target.unwrap_or(false); state.config.build_target = project_config.build_target.unwrap_or(false);
state.config.watch_patterns = project_config.watch_patterns.clone().unwrap_or_else(|| { if let Some(watch_patterns) = &project_config.watch_patterns {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() state.config.watch_patterns = watch_patterns
}); .iter()
.map(|s| Glob::new(s))
.collect::<Result<Vec<Glob>, globset::Error>>()?;
} else {
state.config.watch_patterns =
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
}
state.watcher_change = true; state.watcher_change = true;
state.objects = project_config.units.clone().unwrap_or_default(); state.objects = project_config.units.clone().unwrap_or_default();
state.object_nodes = build_nodes( state.object_nodes = build_nodes(

View File

@@ -96,7 +96,7 @@ pub fn load_font_if_needed(
let default_font = family.handles.get(family.default_index).unwrap(); let default_font = family.handles.get(family.default_index).unwrap();
let default_font_data = load_font(default_font).unwrap(); let default_font_data = load_font(default_font).unwrap();
log::info!("Loaded font family '{}'", family.family_name); log::info!("Loaded font family '{}'", family.family_name);
fonts.font_data.insert(default_font_ref.full_name(), default_font_data.font_data); fonts.font_data.insert(default_font_ref.full_name(), Arc::new(default_font_data.font_data));
fonts fonts
.families .families
.entry(egui::FontFamily::Name(Arc::from(family.family_name))) .entry(egui::FontFamily::Name(Arc::from(family.family_name)))

View File

@@ -7,6 +7,7 @@ use anyhow::{bail, Result};
use jobs::create_scratch; use jobs::create_scratch;
use objdiff_core::{ use objdiff_core::{
build::BuildConfig, build::BuildConfig,
diff::MappingConfig,
jobs, jobs,
jobs::{check_update::CheckUpdateConfig, objdiff, update::UpdateConfig, Job, JobQueue}, jobs::{check_update::CheckUpdateConfig, objdiff, update::UpdateConfig, Job, JobQueue},
}; };
@@ -106,7 +107,8 @@ pub fn create_objdiff_config(state: &AppState) -> objdiff::ObjDiffConfig {
.and_then(|obj| obj.base_path.as_ref()) .and_then(|obj| obj.base_path.as_ref())
.cloned(), .cloned(),
diff_obj_config: state.config.diff_obj_config.clone(), diff_obj_config: state.config.diff_obj_config.clone(),
symbol_mappings: state mapping_config: MappingConfig {
mappings: state
.config .config
.selected_obj .selected_obj
.as_ref() .as_ref()
@@ -115,6 +117,7 @@ pub fn create_objdiff_config(state: &AppState) -> objdiff::ObjDiffConfig {
.unwrap_or_default(), .unwrap_or_default(),
selecting_left: state.selecting_left.clone(), selecting_left: state.selecting_left.clone(),
selecting_right: state.selecting_right.clone(), selecting_right: state.selecting_right.clone(),
},
} }
} }

View File

@@ -88,15 +88,30 @@ fn main() -> ExitCode {
} }
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
{ {
use eframe::egui_wgpu::wgpu::Backends; use eframe::egui_wgpu::{wgpu::Backends, WgpuSetup};
if graphics_config.desired_backend.is_supported() { if graphics_config.desired_backend.is_supported() {
native_options.wgpu_options.supported_backends = match graphics_config.desired_backend { native_options.wgpu_options.wgpu_setup = match native_options.wgpu_options.wgpu_setup {
GraphicsBackend::Auto => native_options.wgpu_options.supported_backends, WgpuSetup::CreateNew {
supported_backends: backends,
power_preference,
device_descriptor,
} => {
let backend = match graphics_config.desired_backend {
GraphicsBackend::Auto => backends,
GraphicsBackend::Dx12 => Backends::DX12, GraphicsBackend::Dx12 => Backends::DX12,
GraphicsBackend::Metal => Backends::METAL, GraphicsBackend::Metal => Backends::METAL,
GraphicsBackend::Vulkan => Backends::VULKAN, GraphicsBackend::Vulkan => Backends::VULKAN,
GraphicsBackend::OpenGL => Backends::GL, GraphicsBackend::OpenGL => Backends::GL,
}; };
WgpuSetup::CreateNew {
supported_backends: backend,
power_preference,
device_descriptor,
}
}
// WgpuConfiguration::Default is CreateNew until we call run_eframe()
_ => unreachable!(),
};
} }
} }
let mut eframe_error = None; let mut eframe_error = None;
@@ -112,6 +127,8 @@ fn main() -> ExitCode {
} }
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
if let Some(e) = eframe_error { if let Some(e) = eframe_error {
use eframe::egui_wgpu::WgpuConfiguration;
// Attempt to relaunch using wgpu auto backend if the desired backend failed // Attempt to relaunch using wgpu auto backend if the desired backend failed
#[allow(unused_mut)] #[allow(unused_mut)]
let mut should_relaunch = graphics_config.desired_backend != GraphicsBackend::Auto; let mut should_relaunch = graphics_config.desired_backend != GraphicsBackend::Auto;
@@ -123,7 +140,7 @@ fn main() -> ExitCode {
if should_relaunch { if should_relaunch {
log::warn!("Failed to launch application: {e:?}"); log::warn!("Failed to launch application: {e:?}");
log::warn!("Attempting to relaunch using auto-detected backend"); log::warn!("Attempting to relaunch using auto-detected backend");
native_options.wgpu_options.supported_backends = Default::default(); native_options.wgpu_options.wgpu_setup = WgpuConfiguration::default().wgpu_setup;
if let Err(e) = run_eframe( if let Err(e) = run_eframe(
native_options.clone(), native_options.clone(),
utc_offset, utc_offset,

View File

@@ -14,10 +14,12 @@ use egui::{
use globset::Glob; use globset::Glob;
use objdiff_core::{ use objdiff_core::{
config::{ProjectObject, DEFAULT_WATCH_PATTERNS}, config::{ProjectObject, DEFAULT_WATCH_PATTERNS},
diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter}, diff::{
ConfigEnum, ConfigEnumVariantInfo, ConfigPropertyId, ConfigPropertyKind,
ConfigPropertyValue, CONFIG_GROUPS,
},
jobs::{check_update::CheckUpdateResult, Job, JobQueue, JobResult}, jobs::{check_update::CheckUpdateResult, Job, JobQueue, JobResult},
}; };
use strum::{EnumMessage, VariantArray};
use crate::{ use crate::{
app::{AppConfig, AppState, AppStateRef, ObjectConfig}, app::{AppConfig, AppState, AppStateRef, ObjectConfig},
@@ -874,121 +876,102 @@ pub fn arch_config_window(
}); });
} }
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() {
state
.config
.diff_obj_config
.set_property_value(property_id, ConfigPropertyValue::Boolean(checked))
.expect("Failed to set property value");
changed = true;
}
}
(ConfigPropertyKind::Choice(variants), ConfigPropertyValue::Choice(selected)) => {
fn variant_name(variant: &ConfigEnumVariantInfo) -> String {
if variant.is_default {
format!("{} (default)", variant.name)
} else {
variant.name.to_string()
}
}
let selected_variant = variants
.iter()
.find(|v| v.value == 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);
}
}
_ => panic!("Incompatible property kind and value"),
}
changed
}
fn arch_config_ui(ui: &mut egui::Ui, state: &mut AppState, _appearance: &Appearance) { fn arch_config_ui(ui: &mut egui::Ui, state: &mut AppState, _appearance: &Appearance) {
ui.heading("x86"); let mut first = true;
egui::ComboBox::new("x86_formatter", "Format") let mut changed = false;
.selected_text(state.config.diff_obj_config.x86_formatter.get_message().unwrap()) for group in CONFIG_GROUPS {
.show_ui(ui, |ui| { if group.id == "general" {
for &formatter in X86Formatter::VARIANTS { continue;
if ui
.selectable_label(
state.config.diff_obj_config.x86_formatter == formatter,
formatter.get_message().unwrap(),
)
.clicked()
{
state.config.diff_obj_config.x86_formatter = formatter;
state.queue_reload = true;
} }
} if first {
}); first = false;
} else {
ui.separator(); ui.separator();
ui.heading("MIPS"); }
egui::ComboBox::new("mips_abi", "ABI") ui.heading(group.name);
.selected_text(state.config.diff_obj_config.mips_abi.get_message().unwrap()) for property_id in group.properties.iter().cloned() {
.show_ui(ui, |ui| { changed |= config_property_ui(ui, state, property_id);
for &abi in MipsAbi::VARIANTS { }
if ui }
.selectable_label( if changed {
state.config.diff_obj_config.mips_abi == abi,
abi.get_message().unwrap(),
)
.clicked()
{
state.config.diff_obj_config.mips_abi = abi;
state.queue_reload = true; state.queue_reload = true;
} }
} }
});
egui::ComboBox::new("mips_instr_category", "Instruction Category") pub fn general_config_ui(ui: &mut egui::Ui, state: &mut AppState) {
.selected_text(state.config.diff_obj_config.mips_instr_category.get_message().unwrap()) let mut changed = false;
.show_ui(ui, |ui| { let group = CONFIG_GROUPS.iter().find(|group| group.id == "general").unwrap();
for &category in MipsInstrCategory::VARIANTS { for property_id in group.properties.iter().cloned() {
if ui changed |= config_property_ui(ui, state, property_id);
.selectable_label( }
state.config.diff_obj_config.mips_instr_category == category, if changed {
category.get_message().unwrap(),
)
.clicked()
{
state.config.diff_obj_config.mips_instr_category = category;
state.queue_reload = true;
}
}
});
ui.separator();
ui.heading("ARM");
egui::ComboBox::new("arm_arch_version", "Architecture Version")
.selected_text(state.config.diff_obj_config.arm_arch_version.get_message().unwrap())
.show_ui(ui, |ui| {
for &version in ArmArchVersion::VARIANTS {
if ui
.selectable_label(
state.config.diff_obj_config.arm_arch_version == version,
version.get_message().unwrap(),
)
.clicked()
{
state.config.diff_obj_config.arm_arch_version = version;
state.queue_reload = true;
}
}
});
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_unified_syntax, "Unified syntax")
.on_hover_text("Disassemble as unified assembly language (UAL).");
if response.changed() {
state.queue_reload = true;
}
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_av_registers, "Use A/V registers")
.on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8");
if response.changed() {
state.queue_reload = true;
}
egui::ComboBox::new("arm_r9_usage", "Display R9 as")
.selected_text(state.config.diff_obj_config.arm_r9_usage.get_message().unwrap())
.show_ui(ui, |ui| {
for &usage in ArmR9Usage::VARIANTS {
if ui
.selectable_label(
state.config.diff_obj_config.arm_r9_usage == usage,
usage.get_message().unwrap(),
)
.on_hover_text(usage.get_detailed_message().unwrap())
.clicked()
{
state.config.diff_obj_config.arm_r9_usage = usage;
state.queue_reload = true;
}
}
});
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_sl_usage, "Display R10 as SL")
.on_hover_text("Used for explicit stack limits.");
if response.changed() {
state.queue_reload = true;
}
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_fp_usage, "Display R11 as FP")
.on_hover_text("Used for frame pointers.");
if response.changed() {
state.queue_reload = true;
}
let response = ui
.checkbox(&mut state.config.diff_obj_config.arm_ip_usage, "Display R12 as IP")
.on_hover_text("Used for interworking and long branches.");
if response.changed() {
state.queue_reload = true; state.queue_reload = true;
} }
} }

View File

@@ -1,8 +1,12 @@
use std::{cmp::min, default::Default, mem::take}; use std::{
cmp::{min, Ordering},
default::Default,
mem::take,
};
use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget}; use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget};
use objdiff_core::{ use objdiff_core::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff}, diff::{ObjDataDiff, ObjDataDiffKind, ObjDataRelocDiff, ObjDiff},
obj::ObjInfo, obj::ObjInfo,
}; };
use time::format_description; use time::format_description;
@@ -23,8 +27,111 @@ fn find_section(obj: &ObjInfo, section_name: &str) -> Option<usize> {
obj.sections.iter().position(|section| section.name == section_name) obj.sections.iter().position(|section| section.name == section_name)
} }
fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) { fn data_row_hover_ui(
if diffs.iter().any(|d| d.kind != ObjDataDiffKind::None) { ui: &mut egui::Ui,
obj: &ObjInfo,
diffs: &[(ObjDataDiff, Vec<ObjDataRelocDiff>)],
appearance: &Appearance,
) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
let reloc_diffs = diffs.iter().flat_map(|(_, reloc_diffs)| reloc_diffs);
let mut prev_reloc = None;
for reloc_diff in reloc_diffs {
let reloc = &reloc_diff.reloc;
if prev_reloc == Some(reloc) {
// Avoid showing consecutive duplicate relocations.
// We do this because a single relocation can span across multiple diffs if the
// bytes in the relocation changed (e.g. first byte is added, second is unchanged).
continue;
}
prev_reloc = Some(reloc);
let color = get_color_for_diff_kind(reloc_diff.kind, appearance);
// TODO: Most of this code is copy-pasted from ins_hover_ui.
// Try to separate this out into a shared function.
ui.label(format!("Relocation type: {}", obj.arch.display_reloc(reloc.flags)));
ui.label(format!("Relocation address: {:x}", reloc.address));
let addend_str = match reloc.addend.cmp(&0i64) {
Ordering::Greater => format!("+{:x}", reloc.addend),
Ordering::Less => format!("-{:x}", -reloc.addend),
_ => "".to_string(),
};
ui.colored_label(color, format!("Name: {}{}", reloc.target.name, addend_str));
if let Some(orig_section_index) = reloc.target.orig_section_index {
if let Some(section) =
obj.sections.iter().find(|s| s.orig_index == orig_section_index)
{
ui.colored_label(color, format!("Section: {}", section.name));
}
ui.colored_label(
color,
format!("Address: {:x}{}", reloc.target.address, addend_str),
);
ui.colored_label(color, format!("Size: {:x}", reloc.target.size));
if reloc.addend >= 0 && reloc.target.bytes.len() > reloc.addend as usize {}
} else {
ui.colored_label(color, "Extern".to_string());
}
}
});
}
fn data_row_context_menu(ui: &mut egui::Ui, diffs: &[(ObjDataDiff, Vec<ObjDataRelocDiff>)]) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
let reloc_diffs = diffs.iter().flat_map(|(_, reloc_diffs)| reloc_diffs);
let mut prev_reloc = None;
for reloc_diff in reloc_diffs {
let reloc = &reloc_diff.reloc;
if prev_reloc == Some(reloc) {
// Avoid showing consecutive duplicate relocations.
// We do this because a single relocation can span across multiple diffs if the
// bytes in the relocation changed (e.g. first byte is added, second is unchanged).
continue;
}
prev_reloc = Some(reloc);
// TODO: This code is copy-pasted from ins_context_menu.
// Try to separate this out into a shared function.
if let Some(name) = &reloc.target.demangled_name {
if ui.button(format!("Copy \"{name}\"")).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(name));
ui.close_menu();
}
}
if ui.button(format!("Copy \"{}\"", reloc.target.name)).clicked() {
ui.output_mut(|output| output.copied_text.clone_from(&reloc.target.name));
ui.close_menu();
}
}
});
}
fn get_color_for_diff_kind(diff_kind: ObjDataDiffKind, appearance: &Appearance) -> egui::Color32 {
match diff_kind {
ObjDataDiffKind::None => appearance.text_color,
ObjDataDiffKind::Replace => appearance.replace_color,
ObjDataDiffKind::Delete => appearance.delete_color,
ObjDataDiffKind::Insert => appearance.insert_color,
}
}
fn data_row_ui(
ui: &mut egui::Ui,
obj: Option<&ObjInfo>,
address: usize,
diffs: &[(ObjDataDiff, Vec<ObjDataRelocDiff>)],
appearance: &Appearance,
) {
if diffs.iter().any(|(dd, rds)| {
dd.kind != ObjDataDiffKind::None || rds.iter().any(|rd| rd.kind != ObjDataDiffKind::None)
}) {
ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color); ui.painter().rect_filled(ui.available_rect_before_wrap(), 0.0, ui.visuals().faint_bg_color);
} }
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
@@ -34,29 +141,34 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appeara
&mut job, &mut job,
appearance.code_font.clone(), appearance.code_font.clone(),
); );
// The offset shown on the side of the GUI, shifted by insertions/deletions.
let mut cur_addr = 0usize; let mut cur_addr = 0usize;
for diff in diffs { // The offset into the actual bytes of the section on this side, ignoring differences.
let base_color = match diff.kind { let mut cur_addr_actual = address;
ObjDataDiffKind::None => appearance.text_color, for (diff, reloc_diffs) in diffs {
ObjDataDiffKind::Replace => appearance.replace_color, let base_color = get_color_for_diff_kind(diff.kind, appearance);
ObjDataDiffKind::Delete => appearance.delete_color,
ObjDataDiffKind::Insert => appearance.insert_color,
};
if diff.data.is_empty() { if diff.data.is_empty() {
let mut str = " ".repeat(diff.len); let mut str = " ".repeat(diff.len);
str.push_str(" ".repeat(diff.len / 8).as_str()); str.push_str(" ".repeat(diff.len / 8).as_str());
write_text(str.as_str(), base_color, &mut job, appearance.code_font.clone()); write_text(str.as_str(), base_color, &mut job, appearance.code_font.clone());
cur_addr += diff.len; cur_addr += diff.len;
} else { } else {
let mut text = String::new();
for byte in &diff.data { for byte in &diff.data {
text.push_str(format!("{byte:02x} ").as_str()); let mut byte_color = base_color;
if let Some(reloc_diff) = reloc_diffs.iter().find(|reloc_diff| {
reloc_diff.kind != ObjDataDiffKind::None
&& reloc_diff.range.contains(&cur_addr_actual)
}) {
byte_color = get_color_for_diff_kind(reloc_diff.kind, appearance);
}
let byte_text = format!("{byte:02x} ");
write_text(byte_text.as_str(), byte_color, &mut job, appearance.code_font.clone());
cur_addr += 1; cur_addr += 1;
cur_addr_actual += 1;
if cur_addr % 8 == 0 { if cur_addr % 8 == 0 {
text.push(' '); write_text(" ", base_color, &mut job, appearance.code_font.clone());
} }
} }
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
} }
} }
if cur_addr < BYTES_PER_ROW { if cur_addr < BYTES_PER_ROW {
@@ -67,13 +179,8 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appeara
write_text(str.as_str(), appearance.text_color, &mut job, appearance.code_font.clone()); write_text(str.as_str(), appearance.text_color, &mut job, appearance.code_font.clone());
} }
write_text(" ", appearance.text_color, &mut job, appearance.code_font.clone()); write_text(" ", appearance.text_color, &mut job, appearance.code_font.clone());
for diff in diffs { for (diff, _) in diffs {
let base_color = match diff.kind { let base_color = get_color_for_diff_kind(diff.kind, appearance);
ObjDataDiffKind::None => appearance.text_color,
ObjDataDiffKind::Replace => appearance.replace_color,
ObjDataDiffKind::Delete => appearance.delete_color,
ObjDataDiffKind::Insert => appearance.insert_color,
};
if diff.data.is_empty() { if diff.data.is_empty() {
write_text( write_text(
" ".repeat(diff.len).as_str(), " ".repeat(diff.len).as_str(),
@@ -94,22 +201,33 @@ fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appeara
write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone()); write_text(text.as_str(), base_color, &mut job, appearance.code_font.clone());
} }
} }
Label::new(job).sense(Sense::click()).ui(ui);
// .on_hover_ui_at_pointer(|ui| ins_hover_ui(ui, ins)) let response = Label::new(job).sense(Sense::click()).ui(ui);
// .context_menu(|ui| ins_context_menu(ui, ins)); if let Some(obj) = obj {
response
.on_hover_ui_at_pointer(|ui| data_row_hover_ui(ui, obj, diffs, appearance))
.context_menu(|ui| data_row_context_menu(ui, diffs));
}
} }
fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> { fn split_diffs(
let mut split_diffs = Vec::<Vec<ObjDataDiff>>::new(); diffs: &[ObjDataDiff],
let mut row_diffs = Vec::<ObjDataDiff>::new(); reloc_diffs: &[ObjDataRelocDiff],
) -> Vec<Vec<(ObjDataDiff, Vec<ObjDataRelocDiff>)>> {
let mut split_diffs = Vec::<Vec<(ObjDataDiff, Vec<ObjDataRelocDiff>)>>::new();
let mut row_diffs = Vec::<(ObjDataDiff, Vec<ObjDataRelocDiff>)>::new();
// The offset shown on the side of the GUI, shifted by insertions/deletions.
let mut cur_addr = 0usize; let mut cur_addr = 0usize;
// The offset into the actual bytes of the section on this side, ignoring differences.
let mut cur_addr_actual = 0usize;
for diff in diffs { for diff in diffs {
let mut cur_len = 0usize; let mut cur_len = 0usize;
while cur_len < diff.len { while cur_len < diff.len {
let remaining_len = diff.len - cur_len; let remaining_len = diff.len - cur_len;
let mut remaining_in_row = BYTES_PER_ROW - (cur_addr % BYTES_PER_ROW); let mut remaining_in_row = BYTES_PER_ROW - (cur_addr % BYTES_PER_ROW);
let len = min(remaining_len, remaining_in_row); let len = min(remaining_len, remaining_in_row);
row_diffs.push(ObjDataDiff {
let data_diff = ObjDataDiff {
data: if diff.data.is_empty() { data: if diff.data.is_empty() {
Vec::new() Vec::new()
} else { } else {
@@ -117,9 +235,28 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
}, },
kind: diff.kind, kind: diff.kind,
len, len,
// TODO symbol: String::new(), // TODO
symbol: String::new(), };
}); let row_reloc_diffs: Vec<ObjDataRelocDiff> = if diff.data.is_empty() {
Vec::new()
} else {
let diff_range = cur_addr_actual + cur_len..cur_addr_actual + cur_len + len;
reloc_diffs
.iter()
.filter_map(|reloc_diff| {
if reloc_diff.range.start < diff_range.end
&& diff_range.start < reloc_diff.range.end
{
Some(reloc_diff.clone())
} else {
None
}
})
.collect()
};
let row_diff = (data_diff, row_reloc_diffs);
row_diffs.push(row_diff);
remaining_in_row -= len; remaining_in_row -= len;
cur_len += len; cur_len += len;
cur_addr += len; cur_addr += len;
@@ -127,6 +264,7 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
split_diffs.push(take(&mut row_diffs)); split_diffs.push(take(&mut row_diffs));
} }
} }
cur_addr_actual += diff.data.len();
} }
if !row_diffs.is_empty() { if !row_diffs.is_empty() {
split_diffs.push(take(&mut row_diffs)); split_diffs.push(take(&mut row_diffs));
@@ -161,6 +299,8 @@ fn data_table_ui(
right_ctx: Option<SectionDiffContext<'_>>, right_ctx: Option<SectionDiffContext<'_>>,
config: &Appearance, config: &Appearance,
) -> Option<()> { ) -> Option<()> {
let left_obj = left_ctx.map(|ctx| ctx.obj);
let right_obj = right_ctx.map(|ctx| ctx.obj);
let left_section = left_ctx let left_section = left_ctx
.and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i]))); .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i])));
let right_section = right_ctx let right_section = right_ctx
@@ -176,8 +316,10 @@ fn data_table_ui(
} }
let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1; let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1;
let left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff)); let left_diffs =
let right_diffs = right_section.map(|(_, section)| split_diffs(&section.data_diff)); left_section.map(|(_, section)| split_diffs(&section.data_diff, &section.reloc_diff));
let right_diffs =
right_section.map(|(_, section)| split_diffs(&section.data_diff, &section.reloc_diff));
hotkeys::check_scroll_hotkeys(ui, true); hotkeys::check_scroll_hotkeys(ui, true);
@@ -187,11 +329,11 @@ fn data_table_ui(
row.col(|ui| { row.col(|ui| {
if column == 0 { if column == 0 {
if let Some(left_diffs) = &left_diffs { if let Some(left_diffs) = &left_diffs {
data_row_ui(ui, address, &left_diffs[i], config); data_row_ui(ui, left_obj, address, &left_diffs[i], config);
} }
} else if column == 1 { } else if column == 1 {
if let Some(right_diffs) = &right_diffs { if let Some(right_diffs) = &right_diffs {
data_row_ui(ui, address, &right_diffs[i], config); data_row_ui(ui, right_obj, address, &right_diffs[i], config);
} }
} }
}); });

View File

@@ -1,6 +1,6 @@
use std::{cmp::Ordering, default::Default}; use std::{cmp::Ordering, default::Default};
use egui::{text::LayoutJob, Id, Label, Response, RichText, Sense, Widget}; use egui::{text::LayoutJob, Id, Label, Layout, Response, RichText, Sense, Widget};
use egui_extras::TableRow; use egui_extras::TableRow;
use objdiff_core::{ use objdiff_core::{
diff::{ diff::{
@@ -149,13 +149,9 @@ fn ins_hover_ui(
appearance.highlight_color, appearance.highlight_color,
format!("Size: {:x}", reloc.target.size), format!("Size: {:x}", reloc.target.size),
); );
if reloc.addend >= 0 && reloc.target.bytes.len() > reloc.addend as usize { if let Some(s) = obj.arch.display_ins_data(ins) {
if let Some(s) = obj.arch.guess_data_type(ins).and_then(|ty| {
obj.arch.display_data_type(ty, &reloc.target.bytes[reloc.addend as usize..])
}) {
ui.colored_label(appearance.highlight_color, s); ui.colored_label(appearance.highlight_color, s);
} }
}
} else { } else {
ui.colored_label(appearance.highlight_color, "Extern".to_string()); ui.colored_label(appearance.highlight_color, "Extern".to_string());
} }
@@ -414,6 +410,7 @@ fn asm_col_ui(
} }
#[must_use] #[must_use]
#[expect(clippy::too_many_arguments)]
fn asm_table_ui( fn asm_table_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
available_width: f32, available_width: f32,
@@ -422,6 +419,7 @@ fn asm_table_ui(
appearance: &Appearance, appearance: &Appearance,
ins_view_state: &FunctionViewState, ins_view_state: &FunctionViewState,
symbol_state: &SymbolViewState, symbol_state: &SymbolViewState,
open_sections: (Option<bool>, Option<bool>),
) -> Option<DiffViewAction> { ) -> Option<DiffViewAction> {
let mut ret = None; let mut ret = None;
let left_len = left_ctx.and_then(|ctx| { let left_len = left_ctx.and_then(|ctx| {
@@ -512,6 +510,7 @@ fn asm_table_ui(
SymbolFilter::Mapping(right_symbol_ref), SymbolFilter::Mapping(right_symbol_ref),
appearance, appearance,
column, column,
open_sections.0,
) { ) {
match action { match action {
DiffViewAction::Navigate(DiffViewNavigation { DiffViewAction::Navigate(DiffViewNavigation {
@@ -570,6 +569,7 @@ fn asm_table_ui(
SymbolFilter::Mapping(left_symbol_ref), SymbolFilter::Mapping(left_symbol_ref),
appearance, appearance,
column, column,
open_sections.1,
) { ) {
match action { match action {
DiffViewAction::Navigate(DiffViewNavigation { DiffViewAction::Navigate(DiffViewNavigation {
@@ -683,6 +683,7 @@ pub fn function_diff_ui(
// Header // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let mut open_sections = (None, None);
render_header(ui, available_width, 2, |ui, column| { render_header(ui, available_width, 2, |ui, column| {
if column == 0 { if column == 0 {
// Left column // Left column
@@ -736,11 +737,24 @@ pub fn function_diff_ui(
.font(appearance.code_font.clone()) .font(appearance.code_font.clone())
.color(appearance.replace_color), .color(appearance.replace_color),
); );
ui.horizontal(|ui| {
ui.label( ui.label(
RichText::new("Choose target symbol") RichText::new("Choose target symbol")
.font(appearance.code_font.clone()) .font(appearance.code_font.clone())
.color(appearance.highlight_color), .color(appearance.highlight_color),
); );
ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| {
if ui.small_button("").on_hover_text_at_pointer("Expand all").clicked() {
open_sections.0 = Some(true);
}
if ui.small_button("").on_hover_text_at_pointer("Collapse all").clicked()
{
open_sections.0 = Some(false);
}
})
});
} }
} else if column == 1 { } else if column == 1 {
// Right column // Right column
@@ -812,11 +826,24 @@ pub fn function_diff_ui(
.font(appearance.code_font.clone()) .font(appearance.code_font.clone())
.color(appearance.replace_color), .color(appearance.replace_color),
); );
ui.horizontal(|ui| {
ui.label( ui.label(
RichText::new("Choose base symbol") RichText::new("Choose base symbol")
.font(appearance.code_font.clone()) .font(appearance.code_font.clone())
.color(appearance.highlight_color), .color(appearance.highlight_color),
); );
ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| {
if ui.small_button("").on_hover_text_at_pointer("Expand all").clicked() {
open_sections.1 = Some(true);
}
if ui.small_button("").on_hover_text_at_pointer("Collapse all").clicked()
{
open_sections.1 = Some(false);
}
})
});
} }
} }
}); });
@@ -834,6 +861,7 @@ pub fn function_diff_ui(
appearance, appearance,
&state.function_state, &state.function_state,
&state.symbol_state, &state.symbol_state,
open_sections,
) )
}) })
.inner .inner

View File

@@ -1,8 +1,8 @@
use std::{collections::BTreeMap, mem::take, ops::Bound}; use std::{collections::BTreeMap, mem::take, ops::Bound};
use egui::{ use egui::{
style::ScrollAnimation, text::LayoutJob, CollapsingHeader, Color32, Id, OpenUrl, ScrollArea, style::ScrollAnimation, text::LayoutJob, CollapsingHeader, Color32, Id, Layout, OpenUrl,
SelectableLabel, TextEdit, Ui, Widget, ScrollArea, SelectableLabel, TextEdit, Ui, Widget,
}; };
use objdiff_core::{ use objdiff_core::{
arch::ObjArch, arch::ObjArch,
@@ -605,6 +605,7 @@ pub enum SymbolFilter<'a> {
} }
#[must_use] #[must_use]
#[expect(clippy::too_many_arguments)]
pub fn symbol_list_ui( pub fn symbol_list_ui(
ui: &mut Ui, ui: &mut Ui,
ctx: SymbolDiffContext<'_>, ctx: SymbolDiffContext<'_>,
@@ -613,6 +614,7 @@ pub fn symbol_list_ui(
filter: SymbolFilter<'_>, filter: SymbolFilter<'_>,
appearance: &Appearance, appearance: &Appearance,
column: usize, column: usize,
open_sections: Option<bool>,
) -> Option<DiffViewAction> { ) -> Option<DiffViewAction> {
let mut ret = None; let mut ret = None;
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
@@ -766,6 +768,7 @@ pub fn symbol_list_ui(
CollapsingHeader::new(header) CollapsingHeader::new(header)
.id_salt(Id::new(section.name.clone()).with(section.orig_index)) .id_salt(Id::new(section.name.clone()).with(section.orig_index))
.default_open(true) .default_open(true)
.open(open_sections)
.show(ui, |ui| { .show(ui, |ui| {
if section.kind == ObjSectionKind::Code && state.reverse_fn_order { if section.kind == ObjSectionKind::Code && state.reverse_fn_order {
for (symbol, symbol_diff) in mapping for (symbol, symbol_diff) in mapping
@@ -873,6 +876,7 @@ pub fn symbol_diff_ui(
// Header // Header
let available_width = ui.available_width(); let available_width = ui.available_width();
let mut open_sections = (None, None);
render_header(ui, available_width, 2, |ui, column| { render_header(ui, available_width, 2, |ui, column| {
if column == 0 { if column == 0 {
// Left column // Left column
@@ -891,6 +895,7 @@ pub fn symbol_diff_ui(
} }
}); });
ui.horizontal(|ui| {
let mut search = state.search.clone(); let mut search = state.search.clone();
let response = TextEdit::singleline(&mut search).hint_text("Filter symbols").ui(ui); let response = TextEdit::singleline(&mut search).hint_text("Filter symbols").ui(ui);
if hotkeys::consume_symbol_filter_shortcut(ui.ctx()) { if hotkeys::consume_symbol_filter_shortcut(ui.ctx()) {
@@ -899,6 +904,16 @@ pub fn symbol_diff_ui(
if response.changed() { if response.changed() {
ret = Some(DiffViewAction::SetSearch(search)); ret = Some(DiffViewAction::SetSearch(search));
} }
ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| {
if ui.small_button("").on_hover_text_at_pointer("Expand all").clicked() {
open_sections.0 = Some(true);
}
if ui.small_button("").on_hover_text_at_pointer("Collapse all").clicked() {
open_sections.0 = Some(false);
}
})
});
} else if column == 1 { } else if column == 1 {
// Right column // Right column
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -930,9 +945,20 @@ pub fn symbol_diff_ui(
} }
}); });
ui.horizontal(|ui| {
if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
ret = Some(DiffViewAction::Build); ret = Some(DiffViewAction::Build);
} }
ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| {
if ui.small_button("").on_hover_text_at_pointer("Expand all").clicked() {
open_sections.1 = Some(true);
}
if ui.small_button("").on_hover_text_at_pointer("Collapse all").clicked() {
open_sections.1 = Some(false);
}
})
});
} }
}); });
@@ -957,6 +983,7 @@ pub fn symbol_diff_ui(
filter, filter,
appearance, appearance,
column, column,
open_sections.0,
) { ) {
ret = Some(result); ret = Some(result);
} }
@@ -981,6 +1008,7 @@ pub fn symbol_diff_ui(
filter, filter,
appearance, appearance,
column, column,
open_sections.1,
) { ) {
ret = Some(result); ret = Some(result);
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "objdiff-wasm", "name": "objdiff-wasm",
"version": "2.0.0", "version": "2.6.0",
"description": "A local diffing tool for decompilation projects.", "description": "A local diffing tool for decompilation projects.",
"author": { "author": {
"name": "Luke Street", "name": "Luke Street",
@@ -21,7 +21,7 @@
"build": "tsup", "build": "tsup",
"build:all": "npm run build:wasm && npm run build:proto && npm run build", "build:all": "npm run build:wasm && npm run build:proto && npm run build",
"build:proto": "protoc --ts_out=gen --ts_opt add_pb_suffix,eslint_disable,ts_nocheck,use_proto_field_name --proto_path=../objdiff-core/protos ../objdiff-core/protos/*.proto", "build:proto": "protoc --ts_out=gen --ts_opt add_pb_suffix,eslint_disable,ts_nocheck,use_proto_field_name --proto_path=../objdiff-core/protos ../objdiff-core/protos/*.proto",
"build:wasm": "cd ../objdiff-core && wasm-pack build --out-dir ../objdiff-wasm/pkg --target web -- --features arm,dwarf,ppc,x86,wasm" "build:wasm": "cd ../objdiff-core && wasm-pack build --out-dir ../objdiff-wasm/pkg --target web -- --features arm,arm64,dwarf,config,ppc,x86,wasm"
}, },
"dependencies": { "dependencies": {
"@protobuf-ts/runtime": "^2.9.4" "@protobuf-ts/runtime": "^2.9.4"

View File

@@ -111,12 +111,12 @@ async function defer<T>(message: AnyHandlerData, worker?: Worker): Promise<T> {
return promise; return promise;
} }
export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, config?: DiffObjConfig): Promise<DiffResult> { export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, diff_config?: DiffObjConfig): Promise<DiffResult> {
const data = await defer<Uint8Array>({ const data = await defer<Uint8Array>({
type: 'run_diff_proto', type: 'run_diff_proto',
left, left,
right, right,
config diff_config
}); });
const parseStart = performance.now(); const parseStart = performance.now();
const result = DiffResult.fromBinary(data, {readUnknownField: false}); const result = DiffResult.fromBinary(data, {readUnknownField: false});
@@ -194,12 +194,17 @@ export function displayDiff(diff: InstructionDiff, baseAddr: bigint, cb: (text:
cb({type: 'spacing', count: 4}); cb({type: 'spacing', count: 4});
} }
cb({type: 'opcode', mnemonic: ins.mnemonic, opcode: ins.opcode}); cb({type: 'opcode', mnemonic: ins.mnemonic, opcode: ins.opcode});
let arg_diff_idx = 0; // non-PlainText argument index
for (let i = 0; i < ins.arguments.length; i++) { for (let i = 0; i < ins.arguments.length; i++) {
if (i === 0) { if (i === 0) {
cb({type: 'spacing', count: 1}); cb({type: 'spacing', count: 1});
} }
const arg = ins.arguments[i].value; const arg = ins.arguments[i].value;
const diff_index = diff.arg_diff[i]?.diff_index; let diff_index: number | undefined;
if (arg.oneofKind !== 'plain_text') {
diff_index = diff.arg_diff[arg_diff_idx]?.diff_index;
arg_diff_idx++;
}
switch (arg.oneofKind) { switch (arg.oneofKind) {
case "plain_text": case "plain_text":
cb({type: 'basic', text: arg.plain_text, diff_index}); cb({type: 'basic', text: arg.plain_text, diff_index});

View File

@@ -38,13 +38,15 @@ async function initIfNeeded() {
// return exports.run_diff_json(left, right, cfg); // return exports.run_diff_json(left, right, cfg);
// } // }
async function run_diff_proto({left, right, config}: { async function run_diff_proto({left, right, diff_config, mapping_config}: {
left: Uint8Array | undefined, left: Uint8Array | undefined,
right: Uint8Array | undefined, right: Uint8Array | undefined,
config?: exports.DiffObjConfig, diff_config?: exports.DiffObjConfig,
mapping_config?: exports.MappingConfig,
}): Promise<Uint8Array> { }): Promise<Uint8Array> {
config = config || {}; diff_config = diff_config || {};
return exports.run_diff_proto(left, right, config); mapping_config = mapping_config || {};
return exports.run_diff_proto(left, right, diff_config, mapping_config);
} }
export type AnyHandlerData = HandlerData[keyof HandlerData]; export type AnyHandlerData = HandlerData[keyof HandlerData];
@@ -73,12 +75,19 @@ self.onmessage = (event: MessageEvent<InMessage>) => {
const result = await handler(data as never); const result = await handler(data as never);
const end = performance.now(); const end = performance.now();
console.debug(`Worker message ${data.messageId} took ${end - start}ms`); console.debug(`Worker message ${data.messageId} took ${end - start}ms`);
let transfer: Transferable[] = [];
if (result instanceof Uint8Array) {
console.log("Transferring!", result.byteLength);
transfer = [result.buffer];
} else {
console.log("Didn't transfer", typeof result);
}
self.postMessage({ self.postMessage({
type: 'result', type: 'result',
result: result, result: result,
error: null, error: null,
messageId, messageId,
} as OutMessage); } as OutMessage, {transfer});
} else { } else {
throw new Error(`No handler for ${data.type}`); throw new Error(`No handler for ${data.type}`);
} }