Add experimental wasm bindings

Published to npm as objdiff-wasm
This commit is contained in:
Luke Street 2024-08-20 21:40:32 -06:00
parent 8250d26b77
commit 0fccae1049
40 changed files with 4732 additions and 311 deletions

35
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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");
} }

View File

@ -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

View File

@ -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![];

View File

@ -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,

View File

@ -1,2 +1,2 @@
pub mod report; pub mod output;
pub mod term; pub mod term;

View File

@ -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(())
}

View File

@ -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"

54
objdiff-core/build.rs Normal file
View File

@ -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");
}

View File

@ -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.

View File

@ -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;

View File

@ -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}"))?;

View File

@ -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>;

View File

@ -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;

View File

@ -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() };

View File

@ -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 }
}
}

View File

@ -0,0 +1,4 @@
pub mod diff;
pub mod report;
#[cfg(feature = "wasm")]
pub mod wasm;

View File

@ -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

View File

@ -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())
}

View File

@ -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

View File

@ -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:

View File

@ -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 {

View File

@ -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>,

View File

@ -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;

View File

@ -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>,

View File

@ -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> {

View File

@ -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>"]

View File

@ -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.

4
objdiff-wasm/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
dist/
gen/
node_modules/
pkg/

View File

@ -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"]}},
];

3519
objdiff-wasm/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

39
objdiff-wasm/package.json Normal file
View File

@ -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"
}
}

202
objdiff-wasm/src/main.ts Normal file
View File

@ -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});
}
}

View File

@ -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
});
}
};

View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"module": "ES2022",
"moduleResolution": "Node",
"target": "ES2022",
"esModuleInterop": true
}
}

View File

@ -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');
}
});