mirror of https://github.com/encounter/objdiff.git
Add experimental wasm bindings
Published to npm as objdiff-wasm
This commit is contained in:
parent
8250d26b77
commit
0fccae1049
|
@ -2825,7 +2825,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objdiff-cli"
|
name = "objdiff-cli"
|
||||||
version = "2.0.0-beta.4"
|
version = "2.0.0-beta.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argp",
|
"argp",
|
||||||
|
@ -2833,10 +2833,7 @@ dependencies = [
|
||||||
"enable-ansi-support",
|
"enable-ansi-support",
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"objdiff-core",
|
"objdiff-core",
|
||||||
"pbjson",
|
|
||||||
"pbjson-build",
|
|
||||||
"prost",
|
"prost",
|
||||||
"prost-build",
|
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"rayon",
|
"rayon",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -2849,7 +2846,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objdiff-core"
|
name = "objdiff-core"
|
||||||
version = "2.0.0-beta.4"
|
version = "2.0.0-beta.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arm-attr",
|
"arm-attr",
|
||||||
|
@ -2867,7 +2864,11 @@ dependencies = [
|
||||||
"msvc-demangler",
|
"msvc-demangler",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"object 0.35.0",
|
"object 0.35.0",
|
||||||
|
"pbjson",
|
||||||
|
"pbjson-build",
|
||||||
"ppc750cl",
|
"ppc750cl",
|
||||||
|
"prost",
|
||||||
|
"prost-build",
|
||||||
"rabbitizer",
|
"rabbitizer",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -2876,11 +2877,12 @@ dependencies = [
|
||||||
"similar",
|
"similar",
|
||||||
"strum",
|
"strum",
|
||||||
"unarm",
|
"unarm",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objdiff-gui"
|
name = "objdiff-gui"
|
||||||
version = "2.0.0-beta.4"
|
version = "2.0.0-beta.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -4620,19 +4622,20 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.92"
|
version = "0.2.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
"wasm-bindgen-macro",
|
"wasm-bindgen-macro",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-backend"
|
name = "wasm-bindgen-backend"
|
||||||
version = "0.2.92"
|
version = "0.2.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"log",
|
"log",
|
||||||
|
@ -4657,9 +4660,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.92"
|
version = "0.2.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
|
@ -4667,9 +4670,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.92"
|
version = "0.2.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -4680,9 +4683,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.92"
|
version = "0.2.93"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wayland-backend"
|
name = "wayland-backend"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "objdiff-cli"
|
name = "objdiff-cli"
|
||||||
version = "2.0.0-beta.4"
|
version = "2.0.0-beta.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.70"
|
rust-version = "1.70"
|
||||||
authors = ["Luke Street <luke@street.dev>"]
|
authors = ["Luke Street <luke@street.dev>"]
|
||||||
|
@ -20,7 +20,6 @@ crossterm = "0.27.0"
|
||||||
enable-ansi-support = "0.2.1"
|
enable-ansi-support = "0.2.1"
|
||||||
memmap2 = "0.9.4"
|
memmap2 = "0.9.4"
|
||||||
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
objdiff-core = { path = "../objdiff-core", features = ["all"] }
|
||||||
pbjson = "0.7.0"
|
|
||||||
prost = "0.13.1"
|
prost = "0.13.1"
|
||||||
ratatui = "0.26.2"
|
ratatui = "0.26.2"
|
||||||
rayon = "1.10.0"
|
rayon = "1.10.0"
|
||||||
|
@ -30,8 +29,3 @@ supports-color = "3.0.0"
|
||||||
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
|
time = { version = "0.3.36", features = ["formatting", "local-offset"] }
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
prost-build = "0.13.1"
|
|
||||||
pbjson-build = "0.7.0"
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let output = std::process::Command::new("git")
|
let output = std::process::Command::new("git")
|
||||||
.args(["rev-parse", "HEAD"])
|
.args(["rev-parse", "HEAD"])
|
||||||
|
@ -8,55 +6,4 @@ fn main() {
|
||||||
let rev = String::from_utf8(output.stdout).expect("Failed to parse git output");
|
let rev = String::from_utf8(output.stdout).expect("Failed to parse git output");
|
||||||
println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}");
|
println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}");
|
||||||
println!("cargo:rustc-rerun-if-changed=.git/HEAD");
|
println!("cargo:rustc-rerun-if-changed=.git/HEAD");
|
||||||
|
|
||||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos");
|
|
||||||
let descriptor_path = root.join("proto_descriptor.bin");
|
|
||||||
println!("cargo:rerun-if-changed={}", descriptor_path.display());
|
|
||||||
let descriptor_mtime = std::fs::metadata(&descriptor_path)
|
|
||||||
.map(|m| m.modified().unwrap())
|
|
||||||
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
|
|
||||||
let mut run_protoc = false;
|
|
||||||
let proto_files = vec![root.join("report.proto")];
|
|
||||||
for proto_file in &proto_files {
|
|
||||||
println!("cargo:rerun-if-changed={}", proto_file.display());
|
|
||||||
let mtime = match std::fs::metadata(proto_file) {
|
|
||||||
Ok(m) => m.modified().unwrap(),
|
|
||||||
Err(e) => panic!("Failed to stat proto file {}: {:?}", proto_file.display(), e),
|
|
||||||
};
|
|
||||||
if mtime > descriptor_mtime {
|
|
||||||
run_protoc = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prost_config(descriptor_path: &Path, run_protoc: bool) -> prost_build::Config {
|
|
||||||
let mut config = prost_build::Config::new();
|
|
||||||
config.file_descriptor_set_path(descriptor_path);
|
|
||||||
// If our cached descriptor is up-to-date, we don't need to run protoc.
|
|
||||||
// This is helpful so that users don't need to have protoc installed
|
|
||||||
// unless they're updating the protos.
|
|
||||||
if !run_protoc {
|
|
||||||
config.skip_protoc_run();
|
|
||||||
}
|
|
||||||
config
|
|
||||||
}
|
|
||||||
if let Err(e) =
|
|
||||||
prost_config(&descriptor_path, run_protoc).compile_protos(&proto_files, &[root.as_path()])
|
|
||||||
{
|
|
||||||
if e.kind() == std::io::ErrorKind::NotFound && e.to_string().contains("protoc") {
|
|
||||||
eprintln!("protoc not found, skipping protobuf compilation");
|
|
||||||
prost_config(&descriptor_path, false)
|
|
||||||
.compile_protos(&proto_files, &[root.as_path()])
|
|
||||||
.expect("Failed to compile protos");
|
|
||||||
} else {
|
|
||||||
panic!("Failed to compile protos: {e:?}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set");
|
|
||||||
pbjson_build::Builder::new()
|
|
||||||
.register_descriptors(&descriptor_set)
|
|
||||||
.expect("Failed to register descriptors")
|
|
||||||
.preserve_proto_field_names()
|
|
||||||
.build(&[".objdiff"])
|
|
||||||
.expect("Failed to build pbjson");
|
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
|
@ -1,4 +1,9 @@
|
||||||
use std::{fs, io::stdout, path::PathBuf, str::FromStr};
|
use std::{
|
||||||
|
fs,
|
||||||
|
io::stdout,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use argp::FromArgs;
|
use argp::FromArgs;
|
||||||
|
@ -14,6 +19,7 @@ use crossterm::{
|
||||||
};
|
};
|
||||||
use event::KeyModifiers;
|
use event::KeyModifiers;
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
|
bindings::diff::DiffResult,
|
||||||
config::{ProjectConfig, ProjectObject},
|
config::{ProjectConfig, ProjectObject},
|
||||||
diff,
|
diff,
|
||||||
diff::{
|
diff::{
|
||||||
|
@ -28,10 +34,13 @@ use ratatui::{
|
||||||
widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
widgets::{Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::util::term::crossterm_panic_handler;
|
use crate::util::{
|
||||||
|
output::{write_output, OutputFormat},
|
||||||
|
term::crossterm_panic_handler,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(FromArgs, PartialEq, Debug)]
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
/// Diff two object files.
|
/// Diff two object files. (Interactive or one-shot mode)
|
||||||
#[argp(subcommand, name = "diff")]
|
#[argp(subcommand, name = "diff")]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
#[argp(option, short = '1')]
|
#[argp(option, short = '1')]
|
||||||
|
@ -49,14 +58,24 @@ pub struct Args {
|
||||||
#[argp(switch, short = 'x')]
|
#[argp(switch, short = 'x')]
|
||||||
/// Relax relocation diffs
|
/// Relax relocation diffs
|
||||||
relax_reloc_diffs: bool,
|
relax_reloc_diffs: bool,
|
||||||
|
#[argp(option, short = 'o')]
|
||||||
|
/// Output file (one-shot mode) ("-" for stdout)
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
#[argp(option)]
|
||||||
|
/// Output format (json, json-pretty, proto) (default: json)
|
||||||
|
format: Option<String>,
|
||||||
#[argp(positional)]
|
#[argp(positional)]
|
||||||
/// Function symbol to diff
|
/// Function symbol to diff
|
||||||
symbol: String,
|
symbol: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(args: Args) -> Result<()> {
|
pub fn run(args: Args) -> Result<()> {
|
||||||
let (target_path, base_path, project_config) =
|
let (target_path, base_path, project_config) = match (
|
||||||
match (&args.target, &args.base, &args.project, &args.unit) {
|
&args.target,
|
||||||
|
&args.base,
|
||||||
|
&args.project,
|
||||||
|
&args.unit,
|
||||||
|
) {
|
||||||
(Some(t), Some(b), None, None) => (Some(t.clone()), Some(b.clone()), None),
|
(Some(t), Some(b), None, None) => (Some(t.clone()), Some(b.clone()), None),
|
||||||
(None, None, p, u) => {
|
(None, None, p, u) => {
|
||||||
let project = match p {
|
let project = match p {
|
||||||
|
@ -107,7 +126,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||||
};
|
};
|
||||||
|
|
||||||
object
|
object
|
||||||
} else {
|
} else if let Some(symbol_name) = &args.symbol {
|
||||||
let mut idx = None;
|
let mut idx = None;
|
||||||
let mut count = 0usize;
|
let mut count = 0usize;
|
||||||
for (i, obj) in project_config.objects.iter_mut().enumerate() {
|
for (i, obj) in project_config.objects.iter_mut().enumerate() {
|
||||||
|
@ -116,7 +135,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||||
if obj
|
if obj
|
||||||
.target_path
|
.target_path
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.map(|o| obj::read::has_function(o, &args.symbol))
|
.map(|o| obj::read::has_function(o, symbol_name))
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
|
@ -128,14 +147,16 @@ pub fn run(args: Args) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match (count, idx) {
|
match (count, idx) {
|
||||||
(0, None) => bail!("Symbol not found: {}", &args.symbol),
|
(0, None) => bail!("Symbol not found: {}", symbol_name),
|
||||||
(1, Some(i)) => &mut project_config.objects[i],
|
(1, Some(i)) => &mut project_config.objects[i],
|
||||||
(2.., Some(_)) => bail!(
|
(2.., Some(_)) => bail!(
|
||||||
"Multiple instances of {} were found, try specifying a unit",
|
"Multiple instances of {} were found, try specifying a unit",
|
||||||
&args.symbol
|
symbol_name
|
||||||
),
|
),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
bail!("Must specify one of: symbol, project and unit, target and base objects")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let target_path = object.target_path.clone();
|
let target_path = object.target_path.clone();
|
||||||
|
@ -144,6 +165,45 @@ pub fn run(args: Args) -> Result<()> {
|
||||||
}
|
}
|
||||||
_ => bail!("Either target and base or project and unit must be specified"),
|
_ => bail!("Either target and base or project and unit must be specified"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(output) = &args.output {
|
||||||
|
run_oneshot(&args, output, target_path.as_deref(), base_path.as_deref())
|
||||||
|
} else {
|
||||||
|
run_interactive(args, target_path, base_path, project_config)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_oneshot(
|
||||||
|
args: &Args,
|
||||||
|
output: &Path,
|
||||||
|
target_path: Option<&Path>,
|
||||||
|
base_path: Option<&Path>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||||
|
let config = diff::DiffObjConfig {
|
||||||
|
relax_reloc_diffs: args.relax_reloc_diffs,
|
||||||
|
..Default::default() // TODO
|
||||||
|
};
|
||||||
|
let target = target_path
|
||||||
|
.map(|p| obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display())))
|
||||||
|
.transpose()?;
|
||||||
|
let base = base_path
|
||||||
|
.map(|p| obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display())))
|
||||||
|
.transpose()?;
|
||||||
|
let result = diff::diff_objs(&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 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)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_interactive(
|
||||||
|
args: Args,
|
||||||
|
target_path: Option<PathBuf>,
|
||||||
|
base_path: Option<PathBuf>,
|
||||||
|
project_config: Option<ProjectConfig>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") };
|
||||||
let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]")
|
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 mut state = Box::new(FunctionDiffUi {
|
let mut state = Box::new(FunctionDiffUi {
|
||||||
|
@ -156,7 +216,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||||
scroll_state_y: ScrollbarState::default(),
|
scroll_state_y: ScrollbarState::default(),
|
||||||
per_page: 0,
|
per_page: 0,
|
||||||
num_rows: 0,
|
num_rows: 0,
|
||||||
symbol_name: args.symbol.clone(),
|
symbol_name: symbol_name.clone(),
|
||||||
target_path,
|
target_path,
|
||||||
base_path,
|
base_path,
|
||||||
project_config,
|
project_config,
|
||||||
|
@ -180,7 +240,7 @@ pub fn run(args: Args) -> Result<()> {
|
||||||
stdout(),
|
stdout(),
|
||||||
EnterAlternateScreen,
|
EnterAlternateScreen,
|
||||||
EnableMouseCapture,
|
EnableMouseCapture,
|
||||||
SetTitle(format!("{} - objdiff", args.symbol)),
|
SetTitle(format!("{} - objdiff", symbol_name)),
|
||||||
)?;
|
)?;
|
||||||
let backend = CrosstermBackend::new(stdout());
|
let backend = CrosstermBackend::new(stdout());
|
||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
@ -814,18 +874,7 @@ impl FunctionDiffUi {
|
||||||
let prev = self.right_obj.take();
|
let prev = self.right_obj.take();
|
||||||
let config = diff::DiffObjConfig {
|
let config = diff::DiffObjConfig {
|
||||||
relax_reloc_diffs: self.relax_reloc_diffs,
|
relax_reloc_diffs: self.relax_reloc_diffs,
|
||||||
space_between_args: true, // TODO
|
..Default::default() // TODO
|
||||||
combine_data_sections: false, // TODO
|
|
||||||
x86_formatter: Default::default(), // TODO
|
|
||||||
mips_abi: Default::default(), // TODO
|
|
||||||
mips_instr_category: Default::default(), // TODO
|
|
||||||
arm_arch_version: Default::default(), // TODO
|
|
||||||
arm_unified_syntax: true, // TODO
|
|
||||||
arm_av_registers: false, // TODO
|
|
||||||
arm_r9_usage: Default::default(), // TODO
|
|
||||||
arm_sl_usage: false, // TODO
|
|
||||||
arm_fp_usage: false, // TODO
|
|
||||||
arm_ip_usage: false, // TODO
|
|
||||||
};
|
};
|
||||||
let target = self
|
let target = self
|
||||||
.target_path
|
.target_path
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
fs::File,
|
fs::File,
|
||||||
io::{BufWriter, Read, Write},
|
io::Read,
|
||||||
ops::DerefMut,
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
@ -10,6 +9,10 @@ use std::{
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use argp::FromArgs;
|
use argp::FromArgs;
|
||||||
use objdiff_core::{
|
use objdiff_core::{
|
||||||
|
bindings::report::{
|
||||||
|
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, Report,
|
||||||
|
ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
|
||||||
|
},
|
||||||
config::ProjectObject,
|
config::ProjectObject,
|
||||||
diff, obj,
|
diff, obj,
|
||||||
obj::{ObjSectionKind, ObjSymbolFlags},
|
obj::{ObjSectionKind, ObjSymbolFlags},
|
||||||
|
@ -18,13 +21,10 @@ use prost::Message;
|
||||||
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
|
||||||
use tracing::{info, warn};
|
use tracing::{info, warn};
|
||||||
|
|
||||||
use crate::util::report::{
|
use crate::util::output::{write_output, OutputFormat};
|
||||||
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, Report, ReportItem,
|
|
||||||
ReportItemMetadata, ReportUnit, ReportUnitMetadata,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(FromArgs, PartialEq, Debug)]
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
/// Commands for processing NVIDIA Shield TV alf files.
|
/// Generate a progress report for a project.
|
||||||
#[argp(subcommand, name = "report")]
|
#[argp(subcommand, name = "report")]
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
#[argp(subcommand)]
|
#[argp(subcommand)]
|
||||||
|
@ -39,7 +39,7 @@ pub enum SubCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromArgs, PartialEq, Debug)]
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
/// Generate a report from a project.
|
/// Generate a progress report for a project.
|
||||||
#[argp(subcommand, name = "generate")]
|
#[argp(subcommand, name = "generate")]
|
||||||
pub struct GenerateArgs {
|
pub struct GenerateArgs {
|
||||||
#[argp(option, short = 'p')]
|
#[argp(option, short = 'p')]
|
||||||
|
@ -52,7 +52,7 @@ pub struct GenerateArgs {
|
||||||
/// Deduplicate global and weak symbols (runs single-threaded)
|
/// Deduplicate global and weak symbols (runs single-threaded)
|
||||||
deduplicate: bool,
|
deduplicate: bool,
|
||||||
#[argp(option, short = 'f')]
|
#[argp(option, short = 'f')]
|
||||||
/// Output format (json or proto, default json)
|
/// Output format (json, json-pretty, proto) (default: json)
|
||||||
format: Option<String>,
|
format: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ pub struct ChangesArgs {
|
||||||
/// Output file
|
/// Output file
|
||||||
output: Option<PathBuf>,
|
output: Option<PathBuf>,
|
||||||
#[argp(option, short = 'f')]
|
#[argp(option, short = 'f')]
|
||||||
/// Output format (json or proto, default json)
|
/// Output format (json, json-pretty, proto) (default: json)
|
||||||
format: Option<String>,
|
format: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,28 +81,8 @@ pub fn run(args: Args) -> Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum OutputFormat {
|
|
||||||
Json,
|
|
||||||
Proto,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutputFormat {
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
match s {
|
|
||||||
"json" => Ok(Self::Json),
|
|
||||||
"binpb" | "pb" | "proto" | "protobuf" => Ok(Self::Proto),
|
|
||||||
_ => bail!("Invalid output format: {}", s),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate(args: GenerateArgs) -> Result<()> {
|
fn generate(args: GenerateArgs) -> Result<()> {
|
||||||
let output_format = if let Some(format) = &args.format {
|
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||||
OutputFormat::from_str(format)?
|
|
||||||
} else {
|
|
||||||
OutputFormat::Json
|
|
||||||
};
|
|
||||||
|
|
||||||
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
|
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
|
||||||
info!("Loading project {}", project_dir.display());
|
info!("Loading project {}", project_dir.display());
|
||||||
|
|
||||||
|
@ -156,45 +136,6 @@ fn generate(args: GenerateArgs) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_output<T>(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()>
|
|
||||||
where T: serde::Serialize + prost::Message {
|
|
||||||
if let Some(output) = output {
|
|
||||||
info!("Writing to {}", output.display());
|
|
||||||
let file = File::options()
|
|
||||||
.read(true)
|
|
||||||
.write(true)
|
|
||||||
.create(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open(output)
|
|
||||||
.with_context(|| format!("Failed to create file {}", output.display()))?;
|
|
||||||
match format {
|
|
||||||
OutputFormat::Json => {
|
|
||||||
let mut output = BufWriter::new(file);
|
|
||||||
serde_json::to_writer_pretty(&mut output, input)
|
|
||||||
.context("Failed to write output file")?;
|
|
||||||
output.flush().context("Failed to flush output file")?;
|
|
||||||
}
|
|
||||||
OutputFormat::Proto => {
|
|
||||||
file.set_len(input.encoded_len() as u64)?;
|
|
||||||
let map =
|
|
||||||
unsafe { memmap2::Mmap::map(&file) }.context("Failed to map output file")?;
|
|
||||||
let mut output = map.make_mut().context("Failed to remap output file")?;
|
|
||||||
input.encode(&mut output.deref_mut()).context("Failed to encode output")?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
match format {
|
|
||||||
OutputFormat::Json => {
|
|
||||||
serde_json::to_writer_pretty(std::io::stdout(), input)?;
|
|
||||||
}
|
|
||||||
OutputFormat::Proto => {
|
|
||||||
std::io::stdout().write_all(&input.encode_to_vec())?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn report_object(
|
fn report_object(
|
||||||
object: &mut ProjectObject,
|
object: &mut ProjectObject,
|
||||||
project_dir: &Path,
|
project_dir: &Path,
|
||||||
|
@ -329,19 +270,8 @@ fn report_object(
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&ReportItem> for ChangeItemInfo {
|
|
||||||
fn from(value: &ReportItem) -> Self {
|
|
||||||
Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn changes(args: ChangesArgs) -> Result<()> {
|
fn changes(args: ChangesArgs) -> Result<()> {
|
||||||
let output_format = if let Some(format) = &args.format {
|
let output_format = OutputFormat::from_option(args.format.as_deref())?;
|
||||||
OutputFormat::from_str(format)?
|
|
||||||
} else {
|
|
||||||
OutputFormat::Json
|
|
||||||
};
|
|
||||||
|
|
||||||
let (previous, current) = if args.previous == Path::new("-") && args.current == Path::new("-") {
|
let (previous, current) = if args.previous == Path::new("-") && args.current == Path::new("-") {
|
||||||
// Special case for comparing two reports from stdin
|
// Special case for comparing two reports from stdin
|
||||||
let mut data = vec![];
|
let mut data = vec![];
|
||||||
|
|
|
@ -54,7 +54,7 @@ impl FromArgValue for LogLevel {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromArgs, PartialEq, Debug)]
|
#[derive(FromArgs, PartialEq, Debug)]
|
||||||
/// Yet another GameCube/Wii decompilation toolkit.
|
/// A local diffing tool for decompilation projects.
|
||||||
struct TopLevel {
|
struct TopLevel {
|
||||||
#[argp(subcommand)]
|
#[argp(subcommand)]
|
||||||
command: SubCommand,
|
command: SubCommand,
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
pub mod report;
|
pub mod output;
|
||||||
pub mod term;
|
pub mod term;
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{BufWriter, Write},
|
||||||
|
ops::DerefMut,
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub enum OutputFormat {
|
||||||
|
#[default]
|
||||||
|
Json,
|
||||||
|
JsonPretty,
|
||||||
|
Proto,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputFormat {
|
||||||
|
pub fn from_str(s: &str) -> Result<Self> {
|
||||||
|
match s.to_ascii_lowercase().as_str() {
|
||||||
|
"json" => Ok(Self::Json),
|
||||||
|
"json-pretty" | "json_pretty" => Ok(Self::JsonPretty),
|
||||||
|
"binpb" | "pb" | "proto" | "protobuf" => Ok(Self::Proto),
|
||||||
|
_ => bail!("Invalid output format: {}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_option(s: Option<&str>) -> Result<Self> {
|
||||||
|
match s {
|
||||||
|
Some(s) => Self::from_str(s),
|
||||||
|
None => Ok(Self::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_output<T>(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()>
|
||||||
|
where T: serde::Serialize + prost::Message {
|
||||||
|
match output {
|
||||||
|
Some(output) if output != Path::new("-") => {
|
||||||
|
info!("Writing to {}", output.display());
|
||||||
|
let file = File::options()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(true)
|
||||||
|
.open(output)
|
||||||
|
.with_context(|| format!("Failed to create file {}", output.display()))?;
|
||||||
|
match format {
|
||||||
|
OutputFormat::Json => {
|
||||||
|
let mut output = BufWriter::new(file);
|
||||||
|
serde_json::to_writer(&mut output, input)
|
||||||
|
.context("Failed to write output file")?;
|
||||||
|
output.flush().context("Failed to flush output file")?;
|
||||||
|
}
|
||||||
|
OutputFormat::JsonPretty => {
|
||||||
|
let mut output = BufWriter::new(file);
|
||||||
|
serde_json::to_writer_pretty(&mut output, input)
|
||||||
|
.context("Failed to write output file")?;
|
||||||
|
output.flush().context("Failed to flush output file")?;
|
||||||
|
}
|
||||||
|
OutputFormat::Proto => {
|
||||||
|
file.set_len(input.encoded_len() as u64)?;
|
||||||
|
let map = unsafe { memmap2::Mmap::map(&file) }
|
||||||
|
.context("Failed to map output file")?;
|
||||||
|
let mut output = map.make_mut().context("Failed to remap output file")?;
|
||||||
|
input.encode(&mut output.deref_mut()).context("Failed to encode output")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => match format {
|
||||||
|
OutputFormat::Json => {
|
||||||
|
serde_json::to_writer(std::io::stdout(), input)?;
|
||||||
|
}
|
||||||
|
OutputFormat::JsonPretty => {
|
||||||
|
serde_json::to_writer_pretty(std::io::stdout(), input)?;
|
||||||
|
}
|
||||||
|
OutputFormat::Proto => {
|
||||||
|
std::io::stdout().write_all(&input.encode_to_vec())?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "objdiff-core"
|
name = "objdiff-core"
|
||||||
version = "2.0.0-beta.4"
|
version = "2.0.0-beta.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.70"
|
rust-version = "1.70"
|
||||||
authors = ["Luke Street <luke@street.dev>"]
|
authors = ["Luke Street <luke@street.dev>"]
|
||||||
|
@ -11,6 +11,9 @@ description = """
|
||||||
A local diffing tool for decompilation projects.
|
A local diffing tool for decompilation projects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
all = ["config", "dwarf", "mips", "ppc", "x86", "arm"]
|
all = ["config", "dwarf", "mips", "ppc", "x86", "arm"]
|
||||||
any-arch = [] # Implicit, used to check if any arch is enabled
|
any-arch = [] # Implicit, used to check if any arch is enabled
|
||||||
|
@ -20,6 +23,7 @@ mips = ["any-arch", "rabbitizer"]
|
||||||
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
|
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
|
||||||
x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"]
|
x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"]
|
||||||
arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"]
|
arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"]
|
||||||
|
wasm = ["serde_json"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0.82"
|
||||||
|
@ -30,9 +34,12 @@ log = "0.4.21"
|
||||||
memmap2 = "0.9.4"
|
memmap2 = "0.9.4"
|
||||||
num-traits = "0.2.18"
|
num-traits = "0.2.18"
|
||||||
object = { version = "0.35.0", features = ["read_core", "std", "elf", "pe"], default-features = false }
|
object = { version = "0.35.0", features = ["read_core", "std", "elf", "pe"], default-features = false }
|
||||||
|
pbjson = "0.7.0"
|
||||||
|
prost = "0.13.1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
similar = { version = "2.5.0", default-features = false }
|
similar = { version = "2.5.0", default-features = false }
|
||||||
strum = { version = "0.26.2", features = ["derive"] }
|
strum = { version = "0.26.2", features = ["derive"] }
|
||||||
|
wasm-bindgen = "0.2.93"
|
||||||
|
|
||||||
# config
|
# config
|
||||||
globset = { version = "0.4.14", features = ["serde1"], optional = true }
|
globset = { version = "0.4.14", features = ["serde1"], optional = true }
|
||||||
|
@ -59,3 +66,7 @@ msvc-demangler = { version = "0.10.0", optional = true }
|
||||||
# arm
|
# arm
|
||||||
unarm = { version = "1.4.0", optional = true }
|
unarm = { version = "1.4.0", optional = true }
|
||||||
arm-attr = { version = "0.1.1", optional = true }
|
arm-attr = { version = "0.1.1", optional = true }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
prost-build = "0.13.1"
|
||||||
|
pbjson-build = "0.7.0"
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos");
|
||||||
|
let descriptor_path = root.join("proto_descriptor.bin");
|
||||||
|
println!("cargo:rerun-if-changed={}", descriptor_path.display());
|
||||||
|
let descriptor_mtime = std::fs::metadata(&descriptor_path)
|
||||||
|
.map(|m| m.modified().unwrap())
|
||||||
|
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
|
||||||
|
let mut run_protoc = false;
|
||||||
|
let proto_files = vec![root.join("diff.proto"), root.join("report.proto")];
|
||||||
|
for proto_file in &proto_files {
|
||||||
|
println!("cargo:rerun-if-changed={}", proto_file.display());
|
||||||
|
let mtime = match std::fs::metadata(proto_file) {
|
||||||
|
Ok(m) => m.modified().unwrap(),
|
||||||
|
Err(e) => panic!("Failed to stat proto file {}: {:?}", proto_file.display(), e),
|
||||||
|
};
|
||||||
|
if mtime > descriptor_mtime {
|
||||||
|
run_protoc = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prost_config(descriptor_path: &Path, run_protoc: bool) -> prost_build::Config {
|
||||||
|
let mut config = prost_build::Config::new();
|
||||||
|
config.file_descriptor_set_path(descriptor_path);
|
||||||
|
// If our cached descriptor is up-to-date, we don't need to run protoc.
|
||||||
|
// This is helpful so that users don't need to have protoc installed
|
||||||
|
// unless they're updating the protos.
|
||||||
|
if !run_protoc {
|
||||||
|
config.skip_protoc_run();
|
||||||
|
}
|
||||||
|
config
|
||||||
|
}
|
||||||
|
if let Err(e) =
|
||||||
|
prost_config(&descriptor_path, run_protoc).compile_protos(&proto_files, &[root.as_path()])
|
||||||
|
{
|
||||||
|
if e.kind() == std::io::ErrorKind::NotFound && e.to_string().contains("protoc") {
|
||||||
|
eprintln!("protoc not found, skipping protobuf compilation");
|
||||||
|
prost_config(&descriptor_path, false)
|
||||||
|
.compile_protos(&proto_files, &[root.as_path()])
|
||||||
|
.expect("Failed to compile protos");
|
||||||
|
} else {
|
||||||
|
panic!("Failed to compile protos: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set");
|
||||||
|
pbjson_build::Builder::new()
|
||||||
|
.register_descriptors(&descriptor_set)
|
||||||
|
.expect("Failed to register descriptors")
|
||||||
|
.preserve_proto_field_names()
|
||||||
|
.build(&[".objdiff"])
|
||||||
|
.expect("Failed to build pbjson");
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package objdiff.diff;
|
||||||
|
|
||||||
|
// A symbol
|
||||||
|
message Symbol {
|
||||||
|
// Name of the symbol
|
||||||
|
string name = 1;
|
||||||
|
// Demangled name of the symbol
|
||||||
|
optional string demangled_name = 2;
|
||||||
|
// Symbol address
|
||||||
|
uint64 address = 3;
|
||||||
|
// Symbol size
|
||||||
|
uint64 size = 4;
|
||||||
|
// Bitmask of SymbolFlag
|
||||||
|
uint32 flags = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Symbol visibility flags
|
||||||
|
enum SymbolFlag {
|
||||||
|
SYMBOL_NONE = 0;
|
||||||
|
SYMBOL_GLOBAL = 1;
|
||||||
|
SYMBOL_LOCAL = 2;
|
||||||
|
SYMBOL_WEAK = 3;
|
||||||
|
SYMBOL_COMMON = 4;
|
||||||
|
SYMBOL_HIDDEN = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single parsed instruction
|
||||||
|
message Instruction {
|
||||||
|
// Instruction address
|
||||||
|
uint64 address = 1;
|
||||||
|
// Instruction size
|
||||||
|
uint32 size = 2;
|
||||||
|
// Instruction opcode
|
||||||
|
uint32 opcode = 3;
|
||||||
|
// Instruction mnemonic
|
||||||
|
string mnemonic = 4;
|
||||||
|
// Instruction formatted string
|
||||||
|
string formatted = 5;
|
||||||
|
// Original (unsimplified) instruction string
|
||||||
|
optional string original = 6;
|
||||||
|
// Instruction arguments
|
||||||
|
repeated Argument arguments = 7;
|
||||||
|
// Instruction relocation
|
||||||
|
optional Relocation relocation = 8;
|
||||||
|
// Instruction branch destination
|
||||||
|
optional uint64 branch_dest = 9;
|
||||||
|
// Instruction line number
|
||||||
|
optional uint32 line_number = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An instruction argument
|
||||||
|
message Argument {
|
||||||
|
oneof value {
|
||||||
|
// Plain text
|
||||||
|
string plain_text = 1;
|
||||||
|
// Value
|
||||||
|
ArgumentValue argument = 2;
|
||||||
|
// Relocation
|
||||||
|
ArgumentRelocation relocation = 3;
|
||||||
|
// Branch destination
|
||||||
|
uint64 branch_dest = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An instruction argument value
|
||||||
|
message ArgumentValue {
|
||||||
|
oneof value {
|
||||||
|
// Signed integer
|
||||||
|
int64 signed = 1;
|
||||||
|
// Unsigned integer
|
||||||
|
uint64 unsigned = 2;
|
||||||
|
// Opaque value
|
||||||
|
string opaque = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marker type for relocation arguments
|
||||||
|
message ArgumentRelocation {
|
||||||
|
}
|
||||||
|
|
||||||
|
message Relocation {
|
||||||
|
uint32 type = 1;
|
||||||
|
string type_name = 2;
|
||||||
|
RelocationTarget target = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RelocationTarget {
|
||||||
|
Symbol symbol = 1;
|
||||||
|
int64 addend = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InstructionDiff {
|
||||||
|
DiffKind diff_kind = 1;
|
||||||
|
optional Instruction instruction = 2;
|
||||||
|
optional InstructionBranchFrom branch_from = 3;
|
||||||
|
optional InstructionBranchTo branch_to = 4;
|
||||||
|
repeated ArgumentDiff arg_diff = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ArgumentDiff {
|
||||||
|
optional uint32 diff_index = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DiffKind {
|
||||||
|
DIFF_NONE = 0;
|
||||||
|
DIFF_REPLACE = 1;
|
||||||
|
DIFF_DELETE = 2;
|
||||||
|
DIFF_INSERT = 3;
|
||||||
|
DIFF_OP_MISMATCH = 4;
|
||||||
|
DIFF_ARG_MISMATCH = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InstructionBranchFrom {
|
||||||
|
repeated uint32 instruction_index = 1;
|
||||||
|
uint32 branch_index = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InstructionBranchTo {
|
||||||
|
uint32 instruction_index = 1;
|
||||||
|
uint32 branch_index = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FunctionDiff {
|
||||||
|
Symbol symbol = 1;
|
||||||
|
repeated InstructionDiff instructions = 2;
|
||||||
|
optional float match_percent = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DataDiff {
|
||||||
|
DiffKind kind = 1;
|
||||||
|
bytes data = 2;
|
||||||
|
// May be larger than data
|
||||||
|
uint64 size = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SectionDiff {
|
||||||
|
string name = 1;
|
||||||
|
SectionKind kind = 2;
|
||||||
|
uint64 size = 3;
|
||||||
|
uint64 address = 4;
|
||||||
|
repeated FunctionDiff functions = 5;
|
||||||
|
repeated DataDiff data = 6;
|
||||||
|
optional float match_percent = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SectionKind {
|
||||||
|
SECTION_UNKNOWN = 0;
|
||||||
|
SECTION_TEXT = 1;
|
||||||
|
SECTION_DATA = 2;
|
||||||
|
SECTION_BSS = 3;
|
||||||
|
SECTION_COMMON = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ObjectDiff {
|
||||||
|
repeated SectionDiff sections = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DiffResult {
|
||||||
|
optional ObjectDiff left = 1;
|
||||||
|
optional ObjectDiff right = 2;
|
||||||
|
}
|
Binary file not shown.
|
@ -111,7 +111,7 @@ impl ObjArch for ObjArchArm {
|
||||||
code: &[u8],
|
code: &[u8],
|
||||||
section_index: usize,
|
section_index: usize,
|
||||||
relocations: &[ObjReloc],
|
relocations: &[ObjReloc],
|
||||||
line_info: &BTreeMap<u64, u64>,
|
line_info: &BTreeMap<u64, u32>,
|
||||||
config: &DiffObjConfig,
|
config: &DiffObjConfig,
|
||||||
) -> Result<ProcessCodeResult> {
|
) -> Result<ProcessCodeResult> {
|
||||||
let start_addr = address as u32;
|
let start_addr = address as u32;
|
||||||
|
|
|
@ -85,7 +85,7 @@ impl ObjArch for ObjArchMips {
|
||||||
code: &[u8],
|
code: &[u8],
|
||||||
_section_index: usize,
|
_section_index: usize,
|
||||||
relocations: &[ObjReloc],
|
relocations: &[ObjReloc],
|
||||||
line_info: &BTreeMap<u64, u64>,
|
line_info: &BTreeMap<u64, u32>,
|
||||||
config: &DiffObjConfig,
|
config: &DiffObjConfig,
|
||||||
) -> Result<ProcessCodeResult> {
|
) -> Result<ProcessCodeResult> {
|
||||||
let _guard = RABBITIZER_MUTEX.lock().map_err(|e| anyhow!("Failed to lock mutex: {e}"))?;
|
let _guard = RABBITIZER_MUTEX.lock().map_err(|e| anyhow!("Failed to lock mutex: {e}"))?;
|
||||||
|
|
|
@ -24,7 +24,7 @@ pub trait ObjArch: Send + Sync {
|
||||||
code: &[u8],
|
code: &[u8],
|
||||||
section_index: usize,
|
section_index: usize,
|
||||||
relocations: &[ObjReloc],
|
relocations: &[ObjReloc],
|
||||||
line_info: &BTreeMap<u64, u64>,
|
line_info: &BTreeMap<u64, u32>,
|
||||||
config: &DiffObjConfig,
|
config: &DiffObjConfig,
|
||||||
) -> Result<ProcessCodeResult>;
|
) -> Result<ProcessCodeResult>;
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ impl ObjArch for ObjArchPpc {
|
||||||
code: &[u8],
|
code: &[u8],
|
||||||
_section_index: usize,
|
_section_index: usize,
|
||||||
relocations: &[ObjReloc],
|
relocations: &[ObjReloc],
|
||||||
line_info: &BTreeMap<u64, u64>,
|
line_info: &BTreeMap<u64, u32>,
|
||||||
config: &DiffObjConfig,
|
config: &DiffObjConfig,
|
||||||
) -> Result<ProcessCodeResult> {
|
) -> Result<ProcessCodeResult> {
|
||||||
let ins_count = code.len() / 4;
|
let ins_count = code.len() / 4;
|
||||||
|
|
|
@ -32,7 +32,7 @@ impl ObjArch for ObjArchX86 {
|
||||||
code: &[u8],
|
code: &[u8],
|
||||||
_section_index: usize,
|
_section_index: usize,
|
||||||
relocations: &[ObjReloc],
|
relocations: &[ObjReloc],
|
||||||
line_info: &BTreeMap<u64, u64>,
|
line_info: &BTreeMap<u64, u32>,
|
||||||
config: &DiffObjConfig,
|
config: &DiffObjConfig,
|
||||||
) -> Result<ProcessCodeResult> {
|
) -> Result<ProcessCodeResult> {
|
||||||
let mut result = ProcessCodeResult { ops: Vec::new(), insts: Vec::new() };
|
let mut result = ProcessCodeResult { ops: Vec::new(), insts: Vec::new() };
|
||||||
|
|
|
@ -0,0 +1,244 @@
|
||||||
|
use crate::{
|
||||||
|
diff::{
|
||||||
|
ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo,
|
||||||
|
ObjInsDiff, ObjInsDiffKind, ObjSectionDiff, ObjSymbolDiff,
|
||||||
|
},
|
||||||
|
obj::{
|
||||||
|
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjReloc, ObjSectionKind, ObjSymbol,
|
||||||
|
ObjSymbolFlagSet, ObjSymbolFlags,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Protobuf diff types
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.rs"));
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.serde.rs"));
|
||||||
|
|
||||||
|
impl DiffResult {
|
||||||
|
pub fn new(left: Option<(&ObjInfo, &ObjDiff)>, right: Option<(&ObjInfo, &ObjDiff)>) -> Self {
|
||||||
|
Self {
|
||||||
|
left: left.map(|(obj, diff)| ObjectDiff::new(obj, diff)),
|
||||||
|
right: right.map(|(obj, diff)| ObjectDiff::new(obj, diff)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectDiff {
|
||||||
|
pub fn new(obj: &ObjInfo, diff: &ObjDiff) -> Self {
|
||||||
|
Self {
|
||||||
|
sections: diff
|
||||||
|
.sections
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, d)| SectionDiff::new(obj, i, d))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SectionDiff {
|
||||||
|
pub fn new(obj: &ObjInfo, section_index: usize, section_diff: &ObjSectionDiff) -> Self {
|
||||||
|
let section = &obj.sections[section_index];
|
||||||
|
let functions = section_diff.symbols.iter().map(|d| FunctionDiff::new(obj, d)).collect();
|
||||||
|
let data = section_diff.data_diff.iter().map(|d| DataDiff::new(obj, d)).collect();
|
||||||
|
Self {
|
||||||
|
name: section.name.to_string(),
|
||||||
|
kind: SectionKind::from(section.kind) as i32,
|
||||||
|
size: section.size,
|
||||||
|
address: section.address,
|
||||||
|
functions,
|
||||||
|
data,
|
||||||
|
match_percent: section_diff.match_percent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ObjSectionKind> for SectionKind {
|
||||||
|
fn from(value: ObjSectionKind) -> Self {
|
||||||
|
match value {
|
||||||
|
ObjSectionKind::Code => SectionKind::SectionText,
|
||||||
|
ObjSectionKind::Data => SectionKind::SectionData,
|
||||||
|
ObjSectionKind::Bss => SectionKind::SectionBss,
|
||||||
|
// TODO common
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FunctionDiff {
|
||||||
|
pub fn new(object: &ObjInfo, symbol_diff: &ObjSymbolDiff) -> Self {
|
||||||
|
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.instructions.iter().map(InstructionDiff::from).collect();
|
||||||
|
Self {
|
||||||
|
symbol: Some(Symbol::from(symbol)),
|
||||||
|
// diff_symbol,
|
||||||
|
instructions,
|
||||||
|
match_percent: symbol_diff.match_percent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DataDiff {
|
||||||
|
pub fn new(_object: &ObjInfo, data_diff: &ObjDataDiff) -> Self {
|
||||||
|
Self {
|
||||||
|
kind: DiffKind::from(data_diff.kind) as i32,
|
||||||
|
data: data_diff.data.clone(),
|
||||||
|
size: data_diff.len as u64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a ObjSymbol> for Symbol {
|
||||||
|
fn from(value: &'a ObjSymbol) -> Self {
|
||||||
|
Self {
|
||||||
|
name: value.name.to_string(),
|
||||||
|
demangled_name: value.demangled_name.clone(),
|
||||||
|
address: value.address,
|
||||||
|
size: value.size,
|
||||||
|
flags: symbol_flags(value.flags),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn symbol_flags(value: ObjSymbolFlagSet) -> u32 {
|
||||||
|
let mut flags = 0u32;
|
||||||
|
if value.0.contains(ObjSymbolFlags::Global) {
|
||||||
|
flags |= SymbolFlag::SymbolNone as u32;
|
||||||
|
}
|
||||||
|
if value.0.contains(ObjSymbolFlags::Local) {
|
||||||
|
flags |= SymbolFlag::SymbolLocal as u32;
|
||||||
|
}
|
||||||
|
if value.0.contains(ObjSymbolFlags::Weak) {
|
||||||
|
flags |= SymbolFlag::SymbolWeak as u32;
|
||||||
|
}
|
||||||
|
if value.0.contains(ObjSymbolFlags::Common) {
|
||||||
|
flags |= SymbolFlag::SymbolCommon as u32;
|
||||||
|
}
|
||||||
|
if value.0.contains(ObjSymbolFlags::Hidden) {
|
||||||
|
flags |= SymbolFlag::SymbolHidden as u32;
|
||||||
|
}
|
||||||
|
flags
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a ObjIns> for Instruction {
|
||||||
|
fn from(value: &'a ObjIns) -> Self {
|
||||||
|
Self {
|
||||||
|
address: value.address,
|
||||||
|
size: value.size as u32,
|
||||||
|
opcode: value.op as u32,
|
||||||
|
mnemonic: value.mnemonic.clone(),
|
||||||
|
formatted: value.formatted.clone(),
|
||||||
|
arguments: value.args.iter().map(Argument::from).collect(),
|
||||||
|
relocation: value.reloc.as_ref().map(Relocation::from),
|
||||||
|
branch_dest: value.branch_dest,
|
||||||
|
line_number: value.line,
|
||||||
|
original: value.orig.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a ObjInsArg> for Argument {
|
||||||
|
fn from(value: &'a ObjInsArg) -> Self {
|
||||||
|
Self {
|
||||||
|
value: Some(match value {
|
||||||
|
ObjInsArg::PlainText(s) => argument::Value::PlainText(s.to_string()),
|
||||||
|
ObjInsArg::Arg(v) => argument::Value::Argument(ArgumentValue::from(v)),
|
||||||
|
ObjInsArg::Reloc => argument::Value::Relocation(ArgumentRelocation {}),
|
||||||
|
ObjInsArg::BranchDest(dest) => argument::Value::BranchDest(*dest),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ObjInsArgValue> for ArgumentValue {
|
||||||
|
fn from(value: &ObjInsArgValue) -> Self {
|
||||||
|
Self {
|
||||||
|
value: Some(match value {
|
||||||
|
ObjInsArgValue::Signed(v) => argument_value::Value::Signed(*v),
|
||||||
|
ObjInsArgValue::Unsigned(v) => argument_value::Value::Unsigned(*v),
|
||||||
|
ObjInsArgValue::Opaque(v) => argument_value::Value::Opaque(v.to_string()),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a ObjReloc> for Relocation {
|
||||||
|
fn from(value: &ObjReloc) -> Self {
|
||||||
|
Self {
|
||||||
|
r#type: match value.flags {
|
||||||
|
object::RelocationFlags::Elf { r_type } => r_type,
|
||||||
|
object::RelocationFlags::MachO { r_type, .. } => r_type as u32,
|
||||||
|
object::RelocationFlags::Coff { typ } => typ as u32,
|
||||||
|
object::RelocationFlags::Xcoff { r_rtype, .. } => r_rtype as u32,
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
type_name: String::new(), // TODO
|
||||||
|
target: Some(RelocationTarget::from(&value.target)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a ObjSymbol> for RelocationTarget {
|
||||||
|
fn from(value: &'a ObjSymbol) -> Self {
|
||||||
|
Self { symbol: Some(Symbol::from(value)), addend: value.addend }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a ObjInsDiff> for InstructionDiff {
|
||||||
|
fn from(value: &'a ObjInsDiff) -> Self {
|
||||||
|
Self {
|
||||||
|
instruction: value.ins.as_ref().map(Instruction::from),
|
||||||
|
diff_kind: DiffKind::from(value.kind) as i32,
|
||||||
|
branch_from: value.branch_from.as_ref().map(InstructionBranchFrom::from),
|
||||||
|
branch_to: value.branch_to.as_ref().map(InstructionBranchTo::from),
|
||||||
|
arg_diff: value.arg_diff.iter().map(ArgumentDiff::from).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Option<ObjInsArgDiff>> for ArgumentDiff {
|
||||||
|
fn from(value: &Option<ObjInsArgDiff>) -> Self {
|
||||||
|
Self { diff_index: value.as_ref().map(|v| v.idx as u32) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ObjInsDiffKind> for DiffKind {
|
||||||
|
fn from(value: ObjInsDiffKind) -> Self {
|
||||||
|
match value {
|
||||||
|
ObjInsDiffKind::None => DiffKind::DiffNone,
|
||||||
|
ObjInsDiffKind::OpMismatch => DiffKind::DiffOpMismatch,
|
||||||
|
ObjInsDiffKind::ArgMismatch => DiffKind::DiffArgMismatch,
|
||||||
|
ObjInsDiffKind::Replace => DiffKind::DiffReplace,
|
||||||
|
ObjInsDiffKind::Delete => DiffKind::DiffDelete,
|
||||||
|
ObjInsDiffKind::Insert => DiffKind::DiffInsert,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ObjDataDiffKind> for DiffKind {
|
||||||
|
fn from(value: ObjDataDiffKind) -> Self {
|
||||||
|
match value {
|
||||||
|
ObjDataDiffKind::None => DiffKind::DiffNone,
|
||||||
|
ObjDataDiffKind::Replace => DiffKind::DiffReplace,
|
||||||
|
ObjDataDiffKind::Delete => DiffKind::DiffDelete,
|
||||||
|
ObjDataDiffKind::Insert => DiffKind::DiffInsert,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a ObjInsBranchFrom> for InstructionBranchFrom {
|
||||||
|
fn from(value: &'a ObjInsBranchFrom) -> Self {
|
||||||
|
Self {
|
||||||
|
instruction_index: value.ins_idx.iter().map(|&x| x as u32).collect(),
|
||||||
|
branch_index: value.branch_idx as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a ObjInsBranchTo> for InstructionBranchTo {
|
||||||
|
fn from(value: &'a ObjInsBranchTo) -> Self {
|
||||||
|
Self { instruction_index: value.ins_idx as u32, branch_index: value.branch_idx as u32 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod diff;
|
||||||
|
pub mod report;
|
||||||
|
#[cfg(feature = "wasm")]
|
||||||
|
pub mod wasm;
|
|
@ -69,6 +69,12 @@ impl Measures {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&ReportItem> for ChangeItemInfo {
|
||||||
|
fn from(value: &ReportItem) -> Self {
|
||||||
|
Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Allows [collect](Iterator::collect) to be used on an iterator of [Measures].
|
/// Allows [collect](Iterator::collect) to be used on an iterator of [Measures].
|
||||||
impl FromIterator<Measures> for Measures {
|
impl FromIterator<Measures> for Measures {
|
||||||
fn from_iter<T>(iter: T) -> Self
|
fn from_iter<T>(iter: T) -> Self
|
|
@ -0,0 +1,53 @@
|
||||||
|
use anyhow::Context;
|
||||||
|
use prost::Message;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
use crate::{bindings::diff::DiffResult, diff, obj};
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn run_diff(
|
||||||
|
left: Option<Box<[u8]>>,
|
||||||
|
right: Option<Box<[u8]>>,
|
||||||
|
config: diff::DiffObjConfig,
|
||||||
|
) -> Result<String, JsError> {
|
||||||
|
let target = left
|
||||||
|
.as_ref()
|
||||||
|
.map(|data| obj::read::parse(data, &config).context("Loading target"))
|
||||||
|
.transpose()
|
||||||
|
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||||
|
let base = right
|
||||||
|
.as_ref()
|
||||||
|
.map(|data| obj::read::parse(data, &config).context("Loading base"))
|
||||||
|
.transpose()
|
||||||
|
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||||
|
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)
|
||||||
|
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||||
|
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 out = DiffResult::new(left, right);
|
||||||
|
serde_json::to_string(&out).map_err(|e| JsError::new(&e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn run_diff_proto(
|
||||||
|
left: Option<Box<[u8]>>,
|
||||||
|
right: Option<Box<[u8]>>,
|
||||||
|
config: diff::DiffObjConfig,
|
||||||
|
) -> Result<Box<[u8]>, JsError> {
|
||||||
|
let target = left
|
||||||
|
.as_ref()
|
||||||
|
.map(|data| obj::read::parse(data, &config).context("Loading target"))
|
||||||
|
.transpose()
|
||||||
|
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||||
|
let base = right
|
||||||
|
.as_ref()
|
||||||
|
.map(|data| obj::read::parse(data, &config).context("Loading base"))
|
||||||
|
.transpose()
|
||||||
|
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||||
|
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)
|
||||||
|
.map_err(|e| JsError::new(&e.to_string()))?;
|
||||||
|
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 out = DiffResult::new(left, right);
|
||||||
|
Ok(out.encode_to_vec().into_boxed_slice())
|
||||||
|
}
|
|
@ -1,8 +1,4 @@
|
||||||
use std::{
|
use std::{cmp::max, collections::BTreeMap};
|
||||||
cmp::max,
|
|
||||||
collections::BTreeMap,
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use similar::{capture_diff_slices_deadline, Algorithm};
|
use similar::{capture_diff_slices_deadline, Algorithm};
|
||||||
|
@ -100,13 +96,8 @@ fn diff_instructions(
|
||||||
left_code: &ProcessCodeResult,
|
left_code: &ProcessCodeResult,
|
||||||
right_code: &ProcessCodeResult,
|
right_code: &ProcessCodeResult,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let deadline = Instant::now() + Duration::from_secs(5);
|
let ops =
|
||||||
let ops = capture_diff_slices_deadline(
|
capture_diff_slices_deadline(Algorithm::Patience, &left_code.ops, &right_code.ops, None);
|
||||||
Algorithm::Patience,
|
|
||||||
&left_code.ops,
|
|
||||||
&right_code.ops,
|
|
||||||
Some(deadline),
|
|
||||||
);
|
|
||||||
if ops.is_empty() {
|
if ops.is_empty() {
|
||||||
left_diff.extend(
|
left_diff.extend(
|
||||||
left_code
|
left_code
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
use std::{
|
use std::cmp::{max, min, Ordering};
|
||||||
cmp::{max, min, Ordering},
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
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};
|
||||||
|
@ -47,15 +44,13 @@ pub fn diff_data_section(
|
||||||
left_section_diff: &ObjSectionDiff,
|
left_section_diff: &ObjSectionDiff,
|
||||||
right_section_diff: &ObjSectionDiff,
|
right_section_diff: &ObjSectionDiff,
|
||||||
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
|
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
|
||||||
let deadline = Instant::now() + Duration::from_secs(5);
|
|
||||||
let left_max =
|
let left_max =
|
||||||
left.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0).min(left.size);
|
left.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0).min(left.size);
|
||||||
let right_max =
|
let right_max =
|
||||||
right.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0).min(right.size);
|
right.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0).min(right.size);
|
||||||
let left_data = &left.data[..left_max as usize];
|
let left_data = &left.data[..left_max as usize];
|
||||||
let right_data = &right.data[..right_max as usize];
|
let right_data = &right.data[..right_max as usize];
|
||||||
let ops =
|
let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None);
|
||||||
capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, Some(deadline));
|
|
||||||
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
|
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
|
||||||
|
|
||||||
let mut left_diff = Vec::<ObjDataDiff>::new();
|
let mut left_diff = Vec::<ObjDataDiff>::new();
|
||||||
|
@ -157,9 +152,7 @@ pub fn diff_data_symbol(
|
||||||
let right_data = &right_section.data[right_symbol.section_address as usize
|
let right_data = &right_section.data[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 deadline = Instant::now() + Duration::from_secs(5);
|
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, Some(deadline));
|
|
||||||
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
|
let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
|
@ -209,15 +202,9 @@ pub fn diff_bss_section(
|
||||||
left_diff: &ObjSectionDiff,
|
left_diff: &ObjSectionDiff,
|
||||||
right_diff: &ObjSectionDiff,
|
right_diff: &ObjSectionDiff,
|
||||||
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
|
) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
|
||||||
let deadline = Instant::now() + Duration::from_secs(5);
|
|
||||||
let left_sizes = left.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
|
let left_sizes = left.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
|
||||||
let right_sizes = right.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
|
let right_sizes = right.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
|
||||||
let ops = capture_diff_slices_deadline(
|
let ops = capture_diff_slices_deadline(Algorithm::Patience, &left_sizes, &right_sizes, None);
|
||||||
Algorithm::Patience,
|
|
||||||
&left_sizes,
|
|
||||||
&right_sizes,
|
|
||||||
Some(deadline),
|
|
||||||
);
|
|
||||||
let mut match_percent = get_diff_ratio(&ops, left_sizes.len(), right_sizes.len()) * 100.0;
|
let mut match_percent = get_diff_ratio(&ops, left_sizes.len(), right_sizes.len()) * 100.0;
|
||||||
|
|
||||||
// Use the highest match percent between two options:
|
// Use the highest match percent between two options:
|
||||||
|
|
|
@ -12,7 +12,7 @@ pub enum DiffText<'a> {
|
||||||
/// Colored text
|
/// Colored text
|
||||||
BasicColor(&'a str, usize),
|
BasicColor(&'a str, usize),
|
||||||
/// Line number
|
/// Line number
|
||||||
Line(usize),
|
Line(u32),
|
||||||
/// Instruction address
|
/// Instruction address
|
||||||
Address(u64),
|
Address(u64),
|
||||||
/// Instruction mnemonic
|
/// Instruction mnemonic
|
||||||
|
@ -49,7 +49,7 @@ pub fn display_diff<E>(
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
if let Some(line) = ins.line {
|
if let Some(line) = ins.line {
|
||||||
cb(DiffText::Line(line as usize))?;
|
cb(DiffText::Line(line))?;
|
||||||
}
|
}
|
||||||
cb(DiffText::Address(ins.address - base_addr))?;
|
cb(DiffText::Address(ins.address - base_addr))?;
|
||||||
if let Some(branch) = &ins_diff.branch_from {
|
if let Some(branch) = &ins_diff.branch_from {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
diff::{
|
diff::{
|
||||||
|
@ -17,6 +18,7 @@ pub mod code;
|
||||||
pub mod data;
|
pub mod data;
|
||||||
pub mod display;
|
pub mod display;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
Copy,
|
Copy,
|
||||||
|
@ -41,6 +43,7 @@ pub enum X86Formatter {
|
||||||
Masm,
|
Masm,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
Copy,
|
Copy,
|
||||||
|
@ -65,6 +68,7 @@ pub enum MipsAbi {
|
||||||
N64,
|
N64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
Copy,
|
Copy,
|
||||||
|
@ -93,6 +97,7 @@ pub enum MipsInstrCategory {
|
||||||
R5900,
|
R5900,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
Copy,
|
Copy,
|
||||||
|
@ -117,6 +122,7 @@ pub enum ArmArchVersion {
|
||||||
V6K,
|
V6K,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug,
|
Debug,
|
||||||
Copy,
|
Copy,
|
||||||
|
@ -148,6 +154,7 @@ pub enum ArmR9Usage {
|
||||||
#[inline]
|
#[inline]
|
||||||
const fn default_true() -> bool { true }
|
const fn default_true() -> bool { true }
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct DiffObjConfig {
|
pub struct DiffObjConfig {
|
||||||
|
@ -200,6 +207,9 @@ impl DiffObjConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn default_diff_obj_config() -> DiffObjConfig { Default::default() }
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ObjSectionDiff {
|
pub struct ObjSectionDiff {
|
||||||
pub symbols: Vec<ObjSymbolDiff>,
|
pub symbols: Vec<ObjSymbolDiff>,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod arch;
|
pub mod arch;
|
||||||
|
pub mod bindings;
|
||||||
#[cfg(feature = "config")]
|
#[cfg(feature = "config")]
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod diff;
|
pub mod diff;
|
||||||
|
|
|
@ -41,7 +41,7 @@ pub struct ObjSection {
|
||||||
pub relocations: Vec<ObjReloc>,
|
pub relocations: Vec<ObjReloc>,
|
||||||
pub virtual_address: Option<u64>,
|
pub virtual_address: Option<u64>,
|
||||||
/// Line number info (.line or .debug_line section)
|
/// Line number info (.line or .debug_line section)
|
||||||
pub line_info: BTreeMap<u64, u64>,
|
pub line_info: BTreeMap<u64, u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
|
@ -103,7 +103,7 @@ pub struct ObjIns {
|
||||||
pub reloc: Option<ObjReloc>,
|
pub reloc: Option<ObjReloc>,
|
||||||
pub branch_dest: Option<u64>,
|
pub branch_dest: Option<u64>,
|
||||||
/// Line number
|
/// Line number
|
||||||
pub line: Option<u64>,
|
pub line: Option<u32>,
|
||||||
/// Formatted instruction
|
/// Formatted instruction
|
||||||
pub formatted: String,
|
pub formatted: String,
|
||||||
/// Original (unsimplified) instruction
|
/// Original (unsimplified) instruction
|
||||||
|
@ -136,8 +136,8 @@ pub struct ObjExtab {
|
||||||
|
|
||||||
pub struct ObjInfo {
|
pub struct ObjInfo {
|
||||||
pub arch: Box<dyn ObjArch>,
|
pub arch: Box<dyn ObjArch>,
|
||||||
pub path: PathBuf,
|
pub path: Option<PathBuf>,
|
||||||
pub timestamp: FileTime,
|
pub timestamp: Option<FileTime>,
|
||||||
pub sections: Vec<ObjSection>,
|
pub sections: Vec<ObjSection>,
|
||||||
/// Common BSS symbols
|
/// Common BSS symbols
|
||||||
pub common: Vec<ObjSymbol>,
|
pub common: Vec<ObjSymbol>,
|
||||||
|
|
|
@ -426,7 +426,7 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection]) -> Result<()> {
|
||||||
};
|
};
|
||||||
let end = start + size as u64;
|
let end = start + size as u64;
|
||||||
while reader.position() < end {
|
while reader.position() < end {
|
||||||
let line_number = read_u32(obj_file, &mut reader)? as u64;
|
let line_number = read_u32(obj_file, &mut reader)?;
|
||||||
let statement_pos = read_u16(obj_file, &mut reader)?;
|
let statement_pos = read_u16(obj_file, &mut reader)?;
|
||||||
if statement_pos != 0xFFFF {
|
if statement_pos != 0xFFFF {
|
||||||
log::warn!("Unhandled statement pos {}", statement_pos);
|
log::warn!("Unhandled statement pos {}", statement_pos);
|
||||||
|
@ -468,7 +468,7 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection]) -> Result<()> {
|
||||||
let mut rows = program.rows();
|
let mut rows = program.rows();
|
||||||
while let Some((_header, row)) = rows.next_row()? {
|
while let Some((_header, row)) = rows.next_row()? {
|
||||||
if let (Some(line), Some(lines)) = (row.line(), &mut lines) {
|
if let (Some(line), Some(lines)) = (row.line(), &mut lines) {
|
||||||
lines.insert(row.address(), line.get());
|
lines.insert(row.address(), line.get() as u32);
|
||||||
}
|
}
|
||||||
if row.end_sequence() {
|
if row.end_sequence() {
|
||||||
// The next row is the start of a new sequence, which means we must
|
// The next row is the start of a new sequence, which means we must
|
||||||
|
@ -600,7 +600,14 @@ pub fn read(obj_path: &Path, config: &DiffObjConfig) -> Result<ObjInfo> {
|
||||||
let timestamp = FileTime::from_last_modification_time(&file.metadata()?);
|
let timestamp = FileTime::from_last_modification_time(&file.metadata()?);
|
||||||
(unsafe { memmap2::Mmap::map(&file) }?, timestamp)
|
(unsafe { memmap2::Mmap::map(&file) }?, timestamp)
|
||||||
};
|
};
|
||||||
let obj_file = File::parse(&*data)?;
|
let mut obj = parse(&data, config)?;
|
||||||
|
obj.path = Some(obj_path.to_owned());
|
||||||
|
obj.timestamp = Some(timestamp);
|
||||||
|
Ok(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
|
||||||
|
let obj_file = File::parse(data)?;
|
||||||
let arch = new_arch(&obj_file)?;
|
let arch = new_arch(&obj_file)?;
|
||||||
let split_meta = split_meta(&obj_file)?;
|
let split_meta = split_meta(&obj_file)?;
|
||||||
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
|
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
|
||||||
|
@ -616,7 +623,7 @@ pub fn read(obj_path: &Path, config: &DiffObjConfig) -> Result<ObjInfo> {
|
||||||
line_info(&obj_file, &mut sections)?;
|
line_info(&obj_file, &mut sections)?;
|
||||||
let common = common_symbols(arch.as_ref(), &obj_file, split_meta.as_ref())?;
|
let common = common_symbols(arch.as_ref(), &obj_file, split_meta.as_ref())?;
|
||||||
let extab = exception_tables(&mut sections, &obj_file)?;
|
let extab = exception_tables(&mut sections, &obj_file)?;
|
||||||
Ok(ObjInfo { arch, path: obj_path.to_owned(), timestamp, sections, common, extab, split_meta })
|
Ok(ObjInfo { arch, path: None, timestamp: None, sections, common, extab, split_meta })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_function(obj_path: &Path, symbol_name: &str) -> Result<bool> {
|
pub fn has_function(obj_path: &Path, symbol_name: &str) -> Result<bool> {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "objdiff-gui"
|
name = "objdiff-gui"
|
||||||
version = "2.0.0-beta.4"
|
version = "2.0.0-beta.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.70"
|
rust-version = "1.70"
|
||||||
authors = ["Luke Street <luke@street.dev>"]
|
authors = ["Luke Street <luke@street.dev>"]
|
||||||
|
|
|
@ -401,16 +401,20 @@ impl App {
|
||||||
|
|
||||||
if let Some(result) = &diff_state.build {
|
if let Some(result) = &diff_state.build {
|
||||||
if let Some((obj, _)) = &result.first_obj {
|
if let Some((obj, _)) = &result.first_obj {
|
||||||
if file_modified(&obj.path, obj.timestamp) {
|
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||||
|
if file_modified(path, timestamp) {
|
||||||
config.queue_reload = true;
|
config.queue_reload = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if let Some((obj, _)) = &result.second_obj {
|
if let Some((obj, _)) = &result.second_obj {
|
||||||
if file_modified(&obj.path, obj.timestamp) {
|
if let (Some(path), Some(timestamp)) = (&obj.path, obj.timestamp) {
|
||||||
|
if file_modified(path, timestamp) {
|
||||||
config.queue_reload = true;
|
config.queue_reload = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Don't clear `queue_build` if a build is running. A file may have been modified during
|
// Don't clear `queue_build` if a build is running. A file may have been modified during
|
||||||
// the build, so we'll start another build after the current one finishes.
|
// the build, so we'll start another build after the current one finishes.
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
dist/
|
||||||
|
gen/
|
||||||
|
node_modules/
|
||||||
|
pkg/
|
|
@ -0,0 +1,11 @@
|
||||||
|
import globals from "globals";
|
||||||
|
import pluginJs from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{files: ["**/*.{js,mjs,cjs,ts}"]},
|
||||||
|
{languageOptions: {globals: globals.browser}},
|
||||||
|
pluginJs.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{rules: {"semi": [2, "always"]}},
|
||||||
|
];
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,39 @@
|
||||||
|
{
|
||||||
|
"name": "objdiff-wasm",
|
||||||
|
"version": "2.0.0-beta.6",
|
||||||
|
"description": "A local diffing tool for decompilation projects.",
|
||||||
|
"author": {
|
||||||
|
"name": "Luke Street",
|
||||||
|
"email": "luke@street.dev"
|
||||||
|
},
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"type": "module",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/encounter/objdiff.git"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/*"
|
||||||
|
],
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"types": "dist/main.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsup",
|
||||||
|
"build:all": "npm run build && npm run build:proto && npm run build:wasm",
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@protobuf-ts/runtime": "2.9.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.9.0",
|
||||||
|
"@protobuf-ts/plugin": "2.9.4",
|
||||||
|
"@types/node": "22.4.1",
|
||||||
|
"esbuild": "0.23.1",
|
||||||
|
"eslint": "^9.9.0",
|
||||||
|
"globals": "^15.9.0",
|
||||||
|
"tsup": "8.2.4",
|
||||||
|
"typescript-eslint": "^8.2.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,202 @@
|
||||||
|
import {ArgumentValue, DiffResult, InstructionDiff, RelocationTarget} from "../gen/diff_pb";
|
||||||
|
import type {
|
||||||
|
ArmArchVersion,
|
||||||
|
ArmR9Usage,
|
||||||
|
DiffObjConfig as WasmDiffObjConfig,
|
||||||
|
MipsAbi,
|
||||||
|
MipsInstrCategory,
|
||||||
|
X86Formatter
|
||||||
|
} from '../pkg';
|
||||||
|
import {InMessage, OutMessage} from './worker';
|
||||||
|
|
||||||
|
// Export wasm types
|
||||||
|
export type DiffObjConfig = Omit<Partial<WasmDiffObjConfig>, 'free'>;
|
||||||
|
export {ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter};
|
||||||
|
|
||||||
|
// Export protobuf types
|
||||||
|
export * from '../gen/diff_pb';
|
||||||
|
|
||||||
|
interface PromiseCallbacks {
|
||||||
|
start: number;
|
||||||
|
resolve: (value: unknown) => void;
|
||||||
|
reject: (reason?: unknown) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let workerInit = false;
|
||||||
|
let workerCallbacks: PromiseCallbacks | null = null;
|
||||||
|
const workerReady = new Promise<Worker>((resolve, reject) => {
|
||||||
|
workerCallbacks = {start: performance.now(), resolve, reject};
|
||||||
|
});
|
||||||
|
|
||||||
|
export function initialize(workerUrl?: string | URL) {
|
||||||
|
if (workerInit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
workerInit = true;
|
||||||
|
const worker = new Worker(workerUrl || 'worker.js', {type: 'module'});
|
||||||
|
worker.onmessage = onMessage.bind(null, worker);
|
||||||
|
worker.onerror = (error) => {
|
||||||
|
console.error("Worker error", error);
|
||||||
|
workerCallbacks.reject(error);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let globalMessageId = 0;
|
||||||
|
const messageCallbacks = new Map<number, PromiseCallbacks>();
|
||||||
|
|
||||||
|
function onMessage(worker: Worker, event: MessageEvent<OutMessage>) {
|
||||||
|
switch (event.data.type) {
|
||||||
|
case 'ready':
|
||||||
|
workerCallbacks.resolve(worker);
|
||||||
|
break;
|
||||||
|
case 'result': {
|
||||||
|
const {messageId, result} = event.data;
|
||||||
|
const callbacks = messageCallbacks.get(messageId);
|
||||||
|
if (callbacks) {
|
||||||
|
const end = performance.now();
|
||||||
|
console.debug(`Message ${messageId} took ${end - callbacks.start}ms`);
|
||||||
|
messageCallbacks.delete(messageId);
|
||||||
|
callbacks.resolve(result);
|
||||||
|
} else {
|
||||||
|
console.warn(`Unknown message ID ${messageId}`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function defer<T>(message: Omit<InMessage, 'messageId'>): Promise<T> {
|
||||||
|
if (!workerInit) {
|
||||||
|
throw new Error('Worker not initialized');
|
||||||
|
}
|
||||||
|
const worker = await workerReady;
|
||||||
|
const messageId = globalMessageId++;
|
||||||
|
const promise = new Promise<T>((resolve, reject) => {
|
||||||
|
messageCallbacks.set(messageId, {start: performance.now(), resolve, reject});
|
||||||
|
});
|
||||||
|
worker.postMessage({
|
||||||
|
...message,
|
||||||
|
messageId
|
||||||
|
});
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, config?: DiffObjConfig): Promise<DiffResult> {
|
||||||
|
const data = await defer<Uint8Array>({
|
||||||
|
type: 'run_diff',
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
config
|
||||||
|
} as InMessage);
|
||||||
|
const parseStart = performance.now();
|
||||||
|
const result = DiffResult.fromBinary(data, {readUnknownField: false});
|
||||||
|
const end = performance.now();
|
||||||
|
console.debug(`Parsing message took ${end - parseStart}ms`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DiffText =
|
||||||
|
DiffTextBasic
|
||||||
|
| DiffTextBasicColor
|
||||||
|
| DiffTextAddress
|
||||||
|
| DiffTextLine
|
||||||
|
| DiffTextOpcode
|
||||||
|
| DiffTextArgument
|
||||||
|
| DiffTextSymbol
|
||||||
|
| DiffTextBranchDest
|
||||||
|
| DiffTextSpacing;
|
||||||
|
|
||||||
|
type DiffTextBase = {
|
||||||
|
diff_index?: number,
|
||||||
|
};
|
||||||
|
export type DiffTextBasic = DiffTextBase & {
|
||||||
|
type: 'basic',
|
||||||
|
text: string,
|
||||||
|
};
|
||||||
|
export type DiffTextBasicColor = DiffTextBase & {
|
||||||
|
type: 'basic_color',
|
||||||
|
text: string,
|
||||||
|
index: number,
|
||||||
|
};
|
||||||
|
export type DiffTextAddress = DiffTextBase & {
|
||||||
|
type: 'address',
|
||||||
|
address: bigint,
|
||||||
|
};
|
||||||
|
export type DiffTextLine = DiffTextBase & {
|
||||||
|
type: 'line',
|
||||||
|
line_number: number,
|
||||||
|
};
|
||||||
|
export type DiffTextOpcode = DiffTextBase & {
|
||||||
|
type: 'opcode',
|
||||||
|
mnemonic: string,
|
||||||
|
opcode: number,
|
||||||
|
};
|
||||||
|
export type DiffTextArgument = DiffTextBase & {
|
||||||
|
type: 'argument',
|
||||||
|
value: ArgumentValue,
|
||||||
|
};
|
||||||
|
export type DiffTextSymbol = DiffTextBase & {
|
||||||
|
type: 'symbol',
|
||||||
|
target: RelocationTarget,
|
||||||
|
};
|
||||||
|
export type DiffTextBranchDest = DiffTextBase & {
|
||||||
|
type: 'branch_dest',
|
||||||
|
address: bigint,
|
||||||
|
};
|
||||||
|
export type DiffTextSpacing = DiffTextBase & {
|
||||||
|
type: 'spacing',
|
||||||
|
count: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TypeScript workaround for oneof types
|
||||||
|
export function oneof<T extends { oneofKind: string }>(type: T): T & { oneofKind: string } {
|
||||||
|
return type as T & { oneofKind: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native JavaScript implementation of objdiff_core::diff::display::display_diff
|
||||||
|
export function displayDiff(diff: InstructionDiff, baseAddr: bigint, cb: (text: DiffText) => void) {
|
||||||
|
const ins = diff.instruction;
|
||||||
|
if (!ins) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ins.line_number != null) {
|
||||||
|
cb({type: 'line', line_number: ins.line_number});
|
||||||
|
}
|
||||||
|
cb({type: 'address', address: ins.address - baseAddr});
|
||||||
|
if (diff.branch_from) {
|
||||||
|
cb({type: 'basic_color', text: ' ~> ', index: diff.branch_from.branch_index});
|
||||||
|
} else {
|
||||||
|
cb({type: 'spacing', count: 4});
|
||||||
|
}
|
||||||
|
cb({type: 'opcode', mnemonic: ins.mnemonic, opcode: ins.opcode});
|
||||||
|
for (let i = 0; i < ins.arguments.length; i++) {
|
||||||
|
if (i === 0) {
|
||||||
|
cb({type: 'spacing', count: 1});
|
||||||
|
}
|
||||||
|
const arg = oneof(ins.arguments[i].value);
|
||||||
|
const diff_index = diff.arg_diff[i]?.diff_index;
|
||||||
|
switch (arg.oneofKind) {
|
||||||
|
case "plain_text":
|
||||||
|
cb({type: 'basic', text: arg.plain_text, diff_index});
|
||||||
|
break;
|
||||||
|
case "argument":
|
||||||
|
cb({type: 'argument', value: arg.argument, diff_index});
|
||||||
|
break;
|
||||||
|
case "relocation": {
|
||||||
|
const reloc = ins.relocation!;
|
||||||
|
cb({type: 'symbol', target: reloc.target, diff_index});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "branch_dest":
|
||||||
|
if (arg.branch_dest < baseAddr) {
|
||||||
|
cb({type: 'basic', text: '<unknown>', diff_index});
|
||||||
|
} else {
|
||||||
|
cb({type: 'branch_dest', address: arg.branch_dest - baseAddr, diff_index});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (diff.branch_to) {
|
||||||
|
cb({type: 'basic_color', text: ' ~> ', index: diff.branch_to.branch_index});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
import wasmInit, {default_diff_obj_config, run_diff_proto} from '../pkg';
|
||||||
|
import {DiffObjConfig} from "./main";
|
||||||
|
|
||||||
|
self.postMessage({type: 'init'} as OutMessage);
|
||||||
|
await wasmInit({});
|
||||||
|
self.postMessage({type: 'ready'} as OutMessage);
|
||||||
|
|
||||||
|
type ExtractParam<T> = {
|
||||||
|
[K in keyof T]: T[K] extends (arg1: infer U, ...args: any[]) => any ? U & { type: K } : never;
|
||||||
|
}[keyof T];
|
||||||
|
type HandlerData = ExtractParam<{
|
||||||
|
run_diff: typeof run_diff,
|
||||||
|
}>;
|
||||||
|
const handlers: {
|
||||||
|
[K in HandlerData['type']]: (data: Omit<HandlerData, 'type'>) => unknown
|
||||||
|
} = {
|
||||||
|
'run_diff': run_diff,
|
||||||
|
};
|
||||||
|
|
||||||
|
function run_diff({left, right, config}: {
|
||||||
|
left: Uint8Array | undefined,
|
||||||
|
right: Uint8Array | undefined,
|
||||||
|
config?: DiffObjConfig
|
||||||
|
}): Uint8Array {
|
||||||
|
const cfg = default_diff_obj_config();
|
||||||
|
if (config) {
|
||||||
|
for (const key in config) {
|
||||||
|
if (key in config) {
|
||||||
|
cfg[key] = config[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return run_diff_proto(left, right, cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InMessage = HandlerData & { messageId: number };
|
||||||
|
|
||||||
|
export type OutMessage = ({
|
||||||
|
type: 'result',
|
||||||
|
result: unknown | null,
|
||||||
|
error: unknown | null,
|
||||||
|
} | {
|
||||||
|
type: 'init',
|
||||||
|
msg: string
|
||||||
|
} | {
|
||||||
|
type: 'ready',
|
||||||
|
msg: string
|
||||||
|
}) & { messageId: number };
|
||||||
|
|
||||||
|
self.onmessage = async (event: MessageEvent<InMessage>) => {
|
||||||
|
const data = event.data;
|
||||||
|
const handler = handlers[data.type];
|
||||||
|
if (handler) {
|
||||||
|
try {
|
||||||
|
const start = performance.now();
|
||||||
|
const result = handler(data);
|
||||||
|
const end = performance.now();
|
||||||
|
console.debug(`Worker message ${data.messageId} took ${end - start}ms`);
|
||||||
|
self.postMessage({
|
||||||
|
type: 'result',
|
||||||
|
result: result,
|
||||||
|
error: null,
|
||||||
|
messageId: data.messageId
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
self.postMessage({
|
||||||
|
type: 'result',
|
||||||
|
result: null,
|
||||||
|
error: error,
|
||||||
|
messageId: data.messageId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.postMessage({
|
||||||
|
type: 'result',
|
||||||
|
result: null,
|
||||||
|
error: `No handler for ${data.type}`,
|
||||||
|
messageId: data.messageId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"target": "ES2022",
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import {defineConfig} from 'tsup';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ['src/main.ts', 'src/worker.ts'],
|
||||||
|
clean: true,
|
||||||
|
dts: true,
|
||||||
|
format: 'esm',
|
||||||
|
sourcemap: true,
|
||||||
|
splitting: false,
|
||||||
|
target: ['es2022', 'chrome89', 'edge89', 'firefox89', 'safari15', 'node14.8'],
|
||||||
|
async onSuccess() {
|
||||||
|
await fs.copyFile('pkg/objdiff_core_bg.wasm', 'dist/objdiff_core_bg.wasm');
|
||||||
|
}
|
||||||
|
});
|
Loading…
Reference in New Issue