diff --git a/Cargo.lock b/Cargo.lock index 73cc799..98a228d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3009,7 +3009,7 @@ dependencies = [ [[package]] name = "objdiff-cli" -version = "2.5.0" +version = "2.6.0" dependencies = [ "anyhow", "argp", @@ -3031,7 +3031,7 @@ dependencies = [ [[package]] name = "objdiff-core" -version = "2.5.0" +version = "2.6.0" dependencies = [ "anyhow", "arm-attr", @@ -3046,6 +3046,7 @@ dependencies = [ "flagset", "gimli", "globset", + "heck", "iced-x86", "log", "memmap2", @@ -3058,8 +3059,11 @@ dependencies = [ "pbjson", "pbjson-build", "ppc750cl", + "prettyplease", + "proc-macro2", "prost", "prost-build", + "quote", "rabbitizer", "reqwest", "self_update", @@ -3070,6 +3074,7 @@ dependencies = [ "shell-escape", "similar", "strum", + "syn 2.0.90", "tempfile", "time", "tsify-next", @@ -3082,7 +3087,7 @@ dependencies = [ [[package]] name = "objdiff-gui" -version = "2.5.0" +version = "2.6.0" dependencies = [ "anyhow", "bytes", diff --git a/Cargo.toml b/Cargo.toml index fe89f31..1fc5d97 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ strip = "debuginfo" codegen-units = 1 [workspace.package] -version = "2.5.0" +version = "2.6.0" authors = ["Luke Street "] edition = "2021" license = "MIT OR Apache-2.0" diff --git a/objdiff-cli/src/cmd/diff.rs b/objdiff-cli/src/cmd/diff.rs index 6ef8ea0..129d4d2 100644 --- a/objdiff-cli/src/cmd/diff.rs +++ b/objdiff-cli/src/cmd/diff.rs @@ -12,7 +12,7 @@ use std::{ time::Duration, }; -use anyhow::{bail, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use argp::FromArgs; use crossterm::{ event, @@ -27,9 +27,11 @@ use objdiff_core::{ watcher::{create_watcher, Watcher}, BuildConfig, }, - config::{build_globset, default_watch_patterns, ProjectConfig, ProjectObject}, + config::{build_globset, ProjectConfig, ProjectObject}, diff, - diff::ObjDiff, + diff::{ + ConfigEnum, ConfigPropertyId, ConfigPropertyKind, DiffObjConfig, MappingConfig, ObjDiff, + }, jobs::{ objdiff::{start_build, ObjDiffConfig}, Job, JobQueue, JobResult, @@ -63,9 +65,6 @@ pub struct Args { #[argp(option, short = 'u')] /// Unit name within project unit: Option, - #[argp(switch, short = 'x')] - /// Relax relocation diffs - relax_reloc_diffs: bool, #[argp(option, short = 'o')] /// Output file (one-shot mode) ("-" for stdout) output: Option, @@ -75,6 +74,18 @@ pub struct Args { #[argp(positional)] /// Function symbol to diff symbol: Option, + #[argp(option, short = 'c')] + /// Configuration property (key=value) + config: Vec, + #[argp(option, short = 'm')] + /// Symbol mapping (target=base) + mapping: Vec, + #[argp(option)] + /// Left symbol name for selection + selecting_left: Option, + #[argp(option)] + /// Right symbol name for selection + selecting_right: Option, } pub fn run(args: Args) -> Result<()> { @@ -84,7 +95,9 @@ pub fn run(args: Args) -> Result<()> { &args.project, &args.unit, ) { - (Some(t), Some(b), None, None) => (Some(t.clone()), Some(b.clone()), None), + (Some(_), Some(_), None, None) + | (Some(_), None, None, None) + | (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None), (None, None, p, u) => { let project = match p { Some(project) => project.clone(), @@ -193,6 +206,43 @@ pub fn run(args: Args) -> Result<()> { } } +fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)> { + let mut diff_config = DiffObjConfig::default(); + for config in &args.config { + let (key, value) = config.split_once('=').context("--config expects \"key=value\"")?; + let property_id = ConfigPropertyId::from_str(key) + .map_err(|()| anyhow!("Invalid configuration property: {}", key))?; + diff_config.set_property_value_str(property_id, value).map_err(|()| { + let mut options = String::new(); + match property_id.kind() { + ConfigPropertyKind::Boolean => { + options = "true, false".to_string(); + } + ConfigPropertyKind::Choice(variants) => { + for (i, variant) in variants.iter().enumerate() { + if i > 0 { + options.push_str(", "); + } + options.push_str(variant.value); + } + } + } + anyhow!("Invalid value for {}. Expected one of: {}", property_id.name(), options) + })?; + } + let mut mapping_config = MappingConfig { + mappings: Default::default(), + selecting_left: args.selecting_left.clone(), + selecting_right: args.selecting_right.clone(), + }; + for mapping in &args.mapping { + let (target, base) = + mapping.split_once('=').context("--mapping expects \"target=base\"")?; + mapping_config.mappings.insert(target.to_string(), base.to_string()); + } + Ok((diff_config, mapping_config)) +} + fn run_oneshot( args: &Args, output: &Path, @@ -200,17 +250,19 @@ fn run_oneshot( 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 (diff_config, mapping_config) = build_config_from_args(args)?; let target = target_path - .map(|p| obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display()))) + .map(|p| { + obj::read::read(p, &diff_config).with_context(|| format!("Loading {}", p.display())) + }) .transpose()?; let base = base_path - .map(|p| obj::read::read(p, &config).with_context(|| format!("Loading {}", p.display()))) + .map(|p| { + obj::read::read(p, &diff_config).with_context(|| format!("Loading {}", p.display())) + }) .transpose()?; - let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?; + let result = + diff::diff_objs(&diff_config, &mapping_config, target.as_ref(), base.as_ref(), None)?; let left = target.as_ref().and_then(|o| result.left.as_ref().map(|d| (o, d))); let 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)?; @@ -229,9 +281,10 @@ pub struct AppState { pub prev_obj: Option<(ObjInfo, ObjDiff)>, pub reload_time: Option, pub time_format: Vec>, - pub relax_reloc_diffs: bool, pub watcher: Option, pub modified: Arc, + pub diff_obj_config: DiffObjConfig, + pub mapping_config: MappingConfig, } fn create_objdiff_config(state: &AppState) -> ObjDiffConfig { @@ -257,13 +310,8 @@ fn create_objdiff_config(state: &AppState) -> ObjDiffConfig { .is_some_and(|p| p.build_target.unwrap_or(false)), target_path: state.target_path.clone(), base_path: state.base_path.clone(), - diff_obj_config: diff::DiffObjConfig { - relax_reloc_diffs: state.relax_reloc_diffs, - ..Default::default() // TODO - }, - symbol_mappings: Default::default(), - selecting_left: None, - selecting_right: None, + diff_obj_config: state.diff_obj_config.clone(), + mapping_config: state.mapping_config.clone(), } } @@ -314,6 +362,7 @@ fn run_interactive( let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") }; let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]") .context("Failed to parse time format")?; + let (diff_obj_config, mapping_config) = build_config_from_args(&args)?; let mut state = AppState { jobs: Default::default(), waker: Default::default(), @@ -326,17 +375,13 @@ fn run_interactive( prev_obj: None, reload_time: None, time_format, - relax_reloc_diffs: args.relax_reloc_diffs, watcher: None, modified: Default::default(), + diff_obj_config, + mapping_config, }; - if let Some(project_dir) = &state.project_dir { - let watch_patterns = state - .project_config - .as_ref() - .and_then(|c| c.watch_patterns.as_ref()) - .cloned() - .unwrap_or_else(default_watch_patterns); + if let (Some(project_dir), Some(project_config)) = (&state.project_dir, &state.project_config) { + let watch_patterns = project_config.build_watch_patterns()?; state.watcher = Some(create_watcher( state.modified.clone(), project_dir, diff --git a/objdiff-cli/src/cmd/report.rs b/objdiff-cli/src/cmd/report.rs index 68e6c28..701aa0f 100644 --- a/objdiff-cli/src/cmd/report.rs +++ b/objdiff-cli/src/cmd/report.rs @@ -169,22 +169,26 @@ fn report_object( } _ => {} } - let config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() }; + let diff_config = diff::DiffObjConfig { relax_reloc_diffs: true, ..Default::default() }; + let mapping_config = diff::MappingConfig::default(); let target = object .target_path .as_ref() .map(|p| { - obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display())) + obj::read::read(p, &diff_config) + .with_context(|| format!("Failed to open {}", p.display())) }) .transpose()?; let base = object .base_path .as_ref() .map(|p| { - obj::read::read(p, &config).with_context(|| format!("Failed to open {}", p.display())) + obj::read::read(p, &diff_config) + .with_context(|| format!("Failed to open {}", p.display())) }) .transpose()?; - let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?; + let result = + diff::diff_objs(&diff_config, &mapping_config, target.as_ref(), base.as_ref(), None)?; let metadata = ReportUnitMetadata { complete: object.complete(), diff --git a/objdiff-cli/src/views/function_diff.rs b/objdiff-cli/src/views/function_diff.rs index 112fb50..0595084 100644 --- a/objdiff-cli/src/views/function_diff.rs +++ b/objdiff-cli/src/views/function_diff.rs @@ -370,7 +370,8 @@ impl UiView for FunctionDiffUi { } // Toggle relax relocation diffs KeyCode::Char('x') => { - state.relax_reloc_diffs = !state.relax_reloc_diffs; + state.diff_obj_config.relax_reloc_diffs = + !state.diff_obj_config.relax_reloc_diffs; result.redraw = true; return EventControlFlow::Reload; } diff --git a/objdiff-core/Cargo.toml b/objdiff-core/Cargo.toml index 76ffefd..6a85ffd 100644 --- a/objdiff-core/Cargo.toml +++ b/objdiff-core/Cargo.toml @@ -16,18 +16,104 @@ documentation = "https://docs.rs/objdiff-core" crate-type = ["cdylib", "rlib"] [features] -all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "arm64", "bindings", "build"] -any-arch = ["config", "dep:bimap", "dep:strum", "dep:similar", "dep:flagset", "dep:log", "dep:memmap2", "dep:byteorder", "dep:num-traits"] # Implicit, used to check if any arch is enabled -bindings = ["dep:serde_json", "dep:prost", "dep:pbjson", "dep:serde", "dep:prost-build", "dep:pbjson-build"] -build = ["dep:shell-escape", "dep:path-slash", "dep:winapi", "dep:notify", "dep:notify-debouncer-full", "dep:reqwest", "dep:self_update", "dep:tempfile", "dep:time"] -config = ["dep:bimap", "dep:globset", "dep:semver", "dep:serde_json", "dep:serde_yaml", "dep:serde", "dep:filetime"] +all = [ + # Features + "bindings", + "build", + "config", + "dwarf", + # Architectures + "mips", + "ppc", + "x86", + "arm", + "arm64", +] +# Implicit, used to check if any arch is enabled +any-arch = [ + "config", + "dep:bimap", + "dep:byteorder", + "dep:flagset", + "dep:heck", + "dep:log", + "dep:memmap2", + "dep:num-traits", + "dep:prettyplease", + "dep:proc-macro2", + "dep:quote", + "dep:serde", + "dep:serde_json", + "dep:similar", + "dep:strum", + "dep:syn", +] +bindings = [ + "dep:pbjson", + "dep:pbjson-build", + "dep:prost", + "dep:prost-build", + "dep:serde", + "dep:serde_json", +] +build = [ + "dep:notify", + "dep:notify-debouncer-full", + "dep:path-slash", + "dep:reqwest", + "dep:self_update", + "dep:shell-escape", + "dep:tempfile", + "dep:time", + "dep:winapi", +] +config = [ + "dep:bimap", + "dep:filetime", + "dep:globset", + "dep:semver", + "dep:serde", + "dep:serde_json", + "dep:serde_yaml", +] dwarf = ["dep:gimli"] -mips = ["any-arch", "dep:rabbitizer"] -ppc = ["any-arch", "dep:cwdemangle", "dep:cwextab", "dep:ppc750cl"] -x86 = ["any-arch", "dep:cpp_demangle", "dep:iced-x86", "dep:msvc-demangler"] -arm = ["any-arch", "dep:cpp_demangle", "dep:unarm", "dep:arm-attr"] -arm64 = ["any-arch", "dep:cpp_demangle", "dep:yaxpeax-arch", "dep:yaxpeax-arm"] -wasm = ["bindings", "any-arch", "dep:console_error_panic_hook", "dep:console_log", "dep:wasm-bindgen", "dep:tsify-next", "dep:log"] +mips = [ + "any-arch", + "dep:rabbitizer", +] +ppc = [ + "any-arch", + "dep:cwdemangle", + "dep:cwextab", + "dep:ppc750cl", +] +x86 = [ + "any-arch", + "dep:cpp_demangle", + "dep:iced-x86", + "dep:msvc-demangler", +] +arm = [ + "any-arch", + "dep:arm-attr", + "dep:cpp_demangle", + "dep:unarm", +] +arm64 = [ + "any-arch", + "dep:cpp_demangle", + "dep:yaxpeax-arch", + "dep:yaxpeax-arm", +] +wasm = [ + "any-arch", + "bindings", + "dep:console_error_panic_hook", + "dep:console_log", + "dep:log", + "dep:tsify-next", + "dep:wasm-bindgen", +] [package.metadata.docs.rs] features = ["all"] @@ -104,5 +190,12 @@ reqwest = { version = "0.12", default-features = false, features = ["blocking", self_update = { version = "0.42", optional = true } [build-dependencies] -prost-build = { version = "0.13", optional = true } +heck = { version = "0.5", optional = true } pbjson-build = { version = "0.7", optional = true } +prettyplease = { version = "0.2", optional = true } +proc-macro2 = { version = "1.0", optional = true } +prost-build = { version = "0.13", optional = true } +quote = { version = "1.0", optional = true } +serde = { version = "1.0", features = ["derive"], optional = true } +serde_json = { version = "1.0", optional = true } +syn = { version = "2.0", optional = true } diff --git a/objdiff-core/build.rs b/objdiff-core/build.rs index b5ab6f3..521f203 100644 --- a/objdiff-core/build.rs +++ b/objdiff-core/build.rs @@ -1,6 +1,11 @@ +#[cfg(feature = "any-arch")] +mod config_gen; + fn main() { #[cfg(feature = "bindings")] compile_protos(); + #[cfg(feature = "any-arch")] + config_gen::generate_diff_config(); } #[cfg(feature = "bindings")] diff --git a/objdiff-core/config-schema.json b/objdiff-core/config-schema.json new file mode 100644 index 0000000..c4fa69d --- /dev/null +++ b/objdiff-core/config-schema.json @@ -0,0 +1,230 @@ +{ + "properties": [ + { + "id": "relaxRelocDiffs", + "type": "boolean", + "default": false, + "name": "Relax relocation diffs", + "description": "Ignores differences in relocation targets. (Address, name, etc)" + }, + { + "id": "spaceBetweenArgs", + "type": "boolean", + "default": true, + "name": "Space between args", + "description": "Adds a space between arguments in the diff output." + }, + { + "id": "combineDataSections", + "type": "boolean", + "default": false, + "name": "Combine data sections", + "description": "Combines data sections with equal names." + }, + { + "id": "arm.archVersion", + "type": "choice", + "default": "auto", + "name": "Architecture version", + "description": "ARM architecture version to use for disassembly.", + "items": [ + { + "value": "auto", + "name": "Auto" + }, + { + "value": "v4t", + "name": "ARMv4T (GBA)" + }, + { + "value": "v5te", + "name": "ARMv5TE (DS)" + }, + { + "value": "v6k", + "name": "ARMv6K (3DS)" + } + ] + }, + { + "id": "arm.unifiedSyntax", + "type": "boolean", + "default": false, + "name": "Unified syntax", + "description": "Disassemble as unified assembly language (UAL)." + }, + { + "id": "arm.avRegisters", + "type": "boolean", + "default": false, + "name": "Use A/V registers", + "description": "Display R0-R3 as A1-A4 and R4-R11 as V1-V8." + }, + { + "id": "arm.r9Usage", + "type": "choice", + "default": "generalPurpose", + "name": "Display R9 as", + "items": [ + { + "value": "generalPurpose", + "name": "R9 or V6", + "description": "Use R9 as a general-purpose register." + }, + { + "value": "sb", + "name": "SB (static base)", + "description": "Used for position-independent data (PID)." + }, + { + "value": "tr", + "name": "TR (TLS register)", + "description": "Used for thread-local storage." + } + ] + }, + { + "id": "arm.slUsage", + "type": "boolean", + "default": false, + "name": "Display R10 as SL", + "description": "Used for explicit stack limits." + }, + { + "id": "arm.fpUsage", + "type": "boolean", + "default": false, + "name": "Display R11 as FP", + "description": "Used for frame pointers." + }, + { + "id": "arm.ipUsage", + "type": "boolean", + "default": false, + "name": "Display R12 as IP", + "description": "Used for interworking and long branches." + }, + { + "id": "mips.abi", + "type": "choice", + "default": "auto", + "name": "ABI", + "description": "MIPS ABI to use for disassembly.", + "items": [ + { + "value": "auto", + "name": "Auto" + }, + { + "value": "o32", + "name": "O32" + }, + { + "value": "n32", + "name": "N32" + }, + { + "value": "n64", + "name": "N64" + } + ] + }, + { + "id": "mips.instrCategory", + "type": "choice", + "default": "auto", + "name": "Instruction category", + "description": "MIPS instruction category to use for disassembly.", + "items": [ + { + "value": "auto", + "name": "Auto" + }, + { + "value": "cpu", + "name": "CPU" + }, + { + "value": "rsp", + "name": "RSP (N64)" + }, + { + "value": "r3000gte", + "name": "R3000 GTE (PS1)" + }, + { + "value": "r4000allegrex", + "name": "R4000 ALLEGREX (PSP)" + }, + { + "value": "r5900", + "name": "R5900 EE (PS2)" + } + ] + }, + { + "id": "x86.formatter", + "type": "choice", + "default": "intel", + "name": "Format", + "description": "x86 disassembly syntax.", + "items": [ + { + "value": "intel", + "name": "Intel" + }, + { + "value": "gas", + "name": "AT&T" + }, + { + "value": "nasm", + "name": "NASM" + }, + { + "value": "masm", + "name": "MASM" + } + ] + } + ], + "groups": [ + { + "id": "general", + "name": "General", + "properties": [ + "relaxRelocDiffs", + "spaceBetweenArgs", + "combineDataSections" + ] + }, + { + "id": "arm", + "name": "ARM", + "properties": [ + "arm.archVersion", + "arm.unifiedSyntax", + "arm.avRegisters", + "arm.r9Usage", + "arm.slUsage", + "arm.fpUsage", + "arm.ipUsage" + ] + }, + { + "id": "mips", + "name": "MIPS", + "properties": [ + "mips.abi", + "mips.instrCategory" + ] + }, + { + "id": "x86", + "name": "x86", + "properties": [ + "x86.formatter" + ] + } + ] +} \ No newline at end of file diff --git a/objdiff-core/config_gen.rs b/objdiff-core/config_gen.rs new file mode 100644 index 0000000..9467594 --- /dev/null +++ b/objdiff-core/config_gen.rs @@ -0,0 +1,491 @@ +use std::{ + fs::File, + path::{Path, PathBuf}, +}; + +use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +#[derive(Debug, serde::Deserialize)] +pub struct ConfigSchema { + pub properties: Vec, + pub groups: Vec, +} + +#[derive(Debug, serde::Deserialize)] +#[serde(tag = "type")] +pub enum ConfigProperty { + #[serde(rename = "boolean")] + Boolean(ConfigPropertyBoolean), + #[serde(rename = "choice")] + Choice(ConfigPropertyChoice), +} + +#[derive(Debug, serde::Deserialize)] +pub struct ConfigPropertyBase { + pub id: String, + pub name: String, + pub description: Option, +} + +#[derive(Debug, serde::Deserialize)] +pub struct ConfigPropertyBoolean { + #[serde(flatten)] + pub base: ConfigPropertyBase, + pub default: bool, +} + +#[derive(Debug, serde::Deserialize)] +pub struct ConfigPropertyChoice { + #[serde(flatten)] + pub base: ConfigPropertyBase, + pub default: String, + pub items: Vec, +} + +#[derive(Debug, serde::Deserialize)] +pub struct ConfigPropertyChoiceItem { + pub value: String, + pub name: String, + pub description: Option, +} + +#[derive(Debug, serde::Deserialize)] +pub struct ConfigGroup { + pub id: String, + pub name: String, + pub description: Option, + pub properties: Vec, +} + +fn build_doc(name: &str, description: Option<&str>) -> TokenStream { + let mut doc = format!(" {}", name); + let mut out = quote! { #[doc = #doc] }; + if let Some(description) = description { + doc = format!(" {}", description); + out.extend(quote! { #[doc = ""] }); + out.extend(quote! { #[doc = #doc] }); + } + out +} + +pub fn generate_diff_config() { + let schema_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("config-schema.json"); + println!("cargo:rerun-if-changed={}", schema_path.display()); + let schema_file = File::open(schema_path).expect("Failed to open config schema file"); + let schema: ConfigSchema = + serde_json::from_reader(schema_file).expect("Failed to parse config schema"); + + let mut enums = TokenStream::new(); + for property in &schema.properties { + let ConfigProperty::Choice(choice) = property else { + continue; + }; + let enum_ident = format_ident!("{}", choice.base.id.to_upper_camel_case()); + let mut variants = TokenStream::new(); + let mut full_variants = TokenStream::new(); + let mut variant_info = TokenStream::new(); + let mut variant_to_str = TokenStream::new(); + let mut variant_to_name = TokenStream::new(); + let mut variant_to_description = TokenStream::new(); + let mut variant_from_str = TokenStream::new(); + for item in &choice.items { + let variant_name = item.value.to_upper_camel_case(); + let variant_ident = format_ident!("{}", variant_name); + let is_default = item.value == choice.default; + variants.extend(build_doc(&item.name, item.description.as_deref())); + if is_default { + variants.extend(quote! { #[default] }); + } + let value = &item.value; + variants.extend(quote! { + #[serde(rename = #value, alias = #variant_name)] + #variant_ident, + }); + full_variants.extend(quote! { #enum_ident::#variant_ident, }); + variant_to_str.extend(quote! { #enum_ident::#variant_ident => #value, }); + let name = &item.name; + variant_to_name.extend(quote! { #enum_ident::#variant_ident => #name, }); + if let Some(description) = &item.description { + variant_to_description.extend(quote! { + #enum_ident::#variant_ident => Some(#description), + }); + } else { + variant_to_description.extend(quote! { + #enum_ident::#variant_ident => None, + }); + } + let description = if let Some(description) = &item.description { + quote! { Some(#description) } + } else { + quote! { None } + }; + variant_info.extend(quote! { + ConfigEnumVariantInfo { + value: #value, + name: #name, + description: #description, + is_default: #is_default, + }, + }); + variant_from_str.extend(quote! { + if s.eq_ignore_ascii_case(#value) { return Ok(#enum_ident::#variant_ident); } + }); + } + enums.extend(quote! { + #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, serde::Deserialize, serde::Serialize)] + #[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] + pub enum #enum_ident { + #variants + } + impl ConfigEnum for #enum_ident { + #[inline] + fn variants() -> &'static [Self] { + static VARIANTS: &[#enum_ident] = &[#full_variants]; + VARIANTS + } + #[inline] + fn variant_info() -> &'static [ConfigEnumVariantInfo] { + static VARIANT_INFO: &[ConfigEnumVariantInfo] = &[ + #variant_info + ]; + VARIANT_INFO + } + fn as_str(&self) -> &'static str { + match self { + #variant_to_str + } + } + fn name(&self) -> &'static str { + match self { + #variant_to_name + } + } + fn description(&self) -> Option<&'static str> { + match self { + #variant_to_description + } + } + } + impl std::str::FromStr for #enum_ident { + type Err = (); + fn from_str(s: &str) -> Result { + #variant_from_str + Err(()) + } + } + }); + } + + let mut groups = TokenStream::new(); + let mut group_idents = Vec::new(); + for group in &schema.groups { + let ident = format_ident!("CONFIG_GROUP_{}", group.id.to_shouty_snake_case()); + let id = &group.id; + let name = &group.name; + let description = if let Some(description) = &group.description { + quote! { Some(#description) } + } else { + quote! { None } + }; + let properties = + group.properties.iter().map(|p| format_ident!("{}", p.to_upper_camel_case())); + groups.extend(quote! { + ConfigPropertyGroup { + id: #id, + name: #name, + description: #description, + properties: &[#(ConfigPropertyId::#properties,)*], + }, + }); + group_idents.push(ident); + } + + let mut property_idents = Vec::new(); + let mut property_variants = TokenStream::new(); + let mut variant_info = TokenStream::new(); + let mut config_property_id_to_str = TokenStream::new(); + let mut config_property_id_to_name = TokenStream::new(); + let mut config_property_id_to_description = TokenStream::new(); + let mut config_property_id_to_kind = TokenStream::new(); + let mut property_fields = TokenStream::new(); + let mut default_fields = TokenStream::new(); + let mut get_property_value_variants = TokenStream::new(); + let mut set_property_value_variants = TokenStream::new(); + let mut set_property_value_str_variants = TokenStream::new(); + let mut config_property_id_from_str = TokenStream::new(); + for property in &schema.properties { + let base = match property { + ConfigProperty::Boolean(b) => &b.base, + ConfigProperty::Choice(c) => &c.base, + }; + let id = &base.id; + let enum_ident = format_ident!("{}", id.to_upper_camel_case()); + property_idents.push(enum_ident.clone()); + config_property_id_to_str.extend(quote! { Self::#enum_ident => #id, }); + let name = &base.name; + config_property_id_to_name.extend(quote! { Self::#enum_ident => #name, }); + if let Some(description) = &base.description { + config_property_id_to_description.extend(quote! { + Self::#enum_ident => Some(#description), + }); + } else { + config_property_id_to_description.extend(quote! { + Self::#enum_ident => None, + }); + } + let doc = build_doc(name, base.description.as_deref()); + property_variants.extend(quote! { #doc #enum_ident, }); + property_fields.extend(doc); + let field_ident = format_ident!("{}", id.to_snake_case()); + match property { + ConfigProperty::Boolean(b) => { + let default = b.default; + if default { + property_fields.extend(quote! { + #[serde(default = "default_true")] + }); + } + property_fields.extend(quote! { + pub #field_ident: bool, + }); + default_fields.extend(quote! { + #field_ident: #default, + }); + } + ConfigProperty::Choice(_) => { + property_fields.extend(quote! { + pub #field_ident: #enum_ident, + }); + default_fields.extend(quote! { + #field_ident: #enum_ident::default(), + }); + } + } + let property_value = match property { + ConfigProperty::Boolean(_) => { + quote! { ConfigPropertyValue::Boolean(self.#field_ident) } + } + ConfigProperty::Choice(_) => { + quote! { ConfigPropertyValue::Choice(self.#field_ident.as_str()) } + } + }; + get_property_value_variants.extend(quote! { + ConfigPropertyId::#enum_ident => #property_value, + }); + match property { + ConfigProperty::Boolean(_) => { + set_property_value_variants.extend(quote! { + ConfigPropertyId::#enum_ident => { + if let ConfigPropertyValue::Boolean(value) = value { + self.#field_ident = value; + Ok(()) + } else { + Err(()) + } + }, + }); + set_property_value_str_variants.extend(quote! { + ConfigPropertyId::#enum_ident => { + if let Ok(value) = value.parse() { + self.#field_ident = value; + Ok(()) + } else { + Err(()) + } + }, + }); + } + ConfigProperty::Choice(_) => { + set_property_value_variants.extend(quote! { + ConfigPropertyId::#enum_ident => { + if let ConfigPropertyValue::Choice(value) = value { + if let Ok(value) = value.parse() { + self.#field_ident = value; + Ok(()) + } else { + Err(()) + } + } else { + Err(()) + } + }, + }); + set_property_value_str_variants.extend(quote! { + ConfigPropertyId::#enum_ident => { + if let Ok(value) = value.parse() { + self.#field_ident = value; + Ok(()) + } else { + Err(()) + } + }, + }); + } + } + let description = if let Some(description) = &base.description { + quote! { Some(#description) } + } else { + quote! { None } + }; + variant_info.extend(quote! { + ConfigEnumVariantInfo { + value: #id, + name: #name, + description: #description, + is_default: false, + }, + }); + match property { + ConfigProperty::Boolean(_) => { + config_property_id_to_kind.extend(quote! { + Self::#enum_ident => ConfigPropertyKind::Boolean, + }); + } + ConfigProperty::Choice(_) => { + config_property_id_to_kind.extend(quote! { + Self::#enum_ident => ConfigPropertyKind::Choice(#enum_ident::variant_info()), + }); + } + } + let snake_id = id.to_snake_case(); + config_property_id_from_str.extend(quote! { + if s.eq_ignore_ascii_case(#id) || s.eq_ignore_ascii_case(#snake_id) { + return Ok(Self::#enum_ident); + } + }); + } + + let tokens = quote! { + pub trait ConfigEnum: Sized { + fn variants() -> &'static [Self]; + fn variant_info() -> &'static [ConfigEnumVariantInfo]; + fn as_str(&self) -> &'static str; + fn name(&self) -> &'static str; + fn description(&self) -> Option<&'static str>; + } + #[derive(Clone, Debug)] + pub struct ConfigEnumVariantInfo { + pub value: &'static str, + pub name: &'static str, + pub description: Option<&'static str>, + pub is_default: bool, + } + #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] + pub enum ConfigPropertyId { + #property_variants + } + impl ConfigEnum for ConfigPropertyId { + #[inline] + fn variants() -> &'static [Self] { + static VARIANTS: &[ConfigPropertyId] = &[#(ConfigPropertyId::#property_idents,)*]; + VARIANTS + } + #[inline] + fn variant_info() -> &'static [ConfigEnumVariantInfo] { + static VARIANT_INFO: &[ConfigEnumVariantInfo] = &[ + #variant_info + ]; + VARIANT_INFO + } + fn as_str(&self) -> &'static str { + match self { + #config_property_id_to_str + } + } + fn name(&self) -> &'static str { + match self { + #config_property_id_to_name + } + } + fn description(&self) -> Option<&'static str> { + match self { + #config_property_id_to_description + } + } + } + impl ConfigPropertyId { + pub fn kind(&self) -> ConfigPropertyKind { + match self { + #config_property_id_to_kind + } + } + } + impl std::str::FromStr for ConfigPropertyId { + type Err = (); + fn from_str(s: &str) -> Result { + #config_property_id_from_str + Err(()) + } + } + #[derive(Clone, Debug)] + pub struct ConfigPropertyGroup { + pub id: &'static str, + pub name: &'static str, + pub description: Option<&'static str>, + pub properties: &'static [ConfigPropertyId], + } + pub static CONFIG_GROUPS: &[ConfigPropertyGroup] = &[#groups]; + #[derive(Clone, Debug, Eq, PartialEq, Hash)] + pub enum ConfigPropertyValue { + Boolean(bool), + Choice(&'static str), + } + impl ConfigPropertyValue { + pub fn to_json(&self) -> serde_json::Value { + match self { + ConfigPropertyValue::Boolean(value) => serde_json::Value::Bool(*value), + ConfigPropertyValue::Choice(value) => serde_json::Value::String(value.to_string()), + } + } + } + #[derive(Clone, Debug)] + pub enum ConfigPropertyKind { + Boolean, + Choice(&'static [ConfigEnumVariantInfo]), + } + #enums + #[inline(always)] + fn default_true() -> bool { true } + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] + #[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] + #[serde(default)] + pub struct DiffObjConfig { + #property_fields + } + impl Default for DiffObjConfig { + fn default() -> Self { + Self { + #default_fields + } + } + } + impl DiffObjConfig { + pub fn get_property_value(&self, id: ConfigPropertyId) -> ConfigPropertyValue { + match id { + #get_property_value_variants + } + } + #[allow(clippy::result_unit_err)] + pub fn set_property_value(&mut self, id: ConfigPropertyId, value: ConfigPropertyValue) -> Result<(), ()> { + match id { + #set_property_value_variants + } + } + #[allow(clippy::result_unit_err)] + pub fn set_property_value_str(&mut self, id: ConfigPropertyId, value: &str) -> Result<(), ()> { + match id { + #set_property_value_str_variants + } + } + } + }; + let file = syn::parse2(tokens).unwrap(); + let formatted = prettyplease::unparse(&file); + std::fs::write( + PathBuf::from(std::env::var_os("OUT_DIR").unwrap()).join("config.gen.rs"), + formatted, + ) + .unwrap(); +} diff --git a/objdiff-core/src/arch/arm.rs b/objdiff-core/src/arch/arm.rs index b36b5b7..28826c8 100644 --- a/objdiff-core/src/arch/arm.rs +++ b/objdiff-core/src/arch/arm.rs @@ -139,9 +139,9 @@ impl ObjArch for ObjArchArm { let version = match config.arm_arch_version { ArmArchVersion::Auto => self.detected_version.unwrap_or(ArmVersion::V5Te), - ArmArchVersion::V4T => ArmVersion::V4T, - ArmArchVersion::V5TE => ArmVersion::V5Te, - ArmArchVersion::V6K => ArmVersion::V6K, + ArmArchVersion::V4t => ArmVersion::V4T, + ArmArchVersion::V5te => ArmVersion::V5Te, + ArmArchVersion::V6k => ArmVersion::V6K, }; let endian = match self.endianness { object::Endianness::Little => unarm::Endian::Little, diff --git a/objdiff-core/src/arch/mips.rs b/objdiff-core/src/arch/mips.rs index 4bb3be0..87b6bdd 100644 --- a/objdiff-core/src/arch/mips.rs +++ b/objdiff-core/src/arch/mips.rs @@ -99,8 +99,8 @@ impl ObjArch for ObjArchMips { MipsInstrCategory::Auto => self.instr_category, MipsInstrCategory::Cpu => InstrCategory::CPU, MipsInstrCategory::Rsp => InstrCategory::RSP, - MipsInstrCategory::R3000Gte => InstrCategory::R3000GTE, - MipsInstrCategory::R4000Allegrex => InstrCategory::R4000ALLEGREX, + MipsInstrCategory::R3000gte => InstrCategory::R3000GTE, + MipsInstrCategory::R4000allegrex => InstrCategory::R4000ALLEGREX, MipsInstrCategory::R5900 => InstrCategory::R5900, }; diff --git a/objdiff-core/src/bindings/wasm.rs b/objdiff-core/src/bindings/wasm.rs index 1a6be61..37cb884 100644 --- a/objdiff-core/src/bindings/wasm.rs +++ b/objdiff-core/src/bindings/wasm.rs @@ -13,20 +13,22 @@ fn parse_object( fn parse_and_run_diff( left: Option>, right: Option>, - config: diff::DiffObjConfig, + diff_config: diff::DiffObjConfig, + mapping_config: diff::MappingConfig, ) -> Result { - let target = parse_object(left, &config)?; - let base = parse_object(right, &config)?; - run_diff(target.as_ref(), base.as_ref(), config) + let target = parse_object(left, &diff_config)?; + let base = parse_object(right, &diff_config)?; + run_diff(target.as_ref(), base.as_ref(), diff_config, mapping_config) } fn run_diff( left: Option<&obj::ObjInfo>, right: Option<&obj::ObjInfo>, - config: diff::DiffObjConfig, + diff_config: diff::DiffObjConfig, + mapping_config: diff::MappingConfig, ) -> Result { - log::debug!("Running diff with config: {:?}", config); - let result = diff::diff_objs(&config, left, right, None).to_js()?; + log::debug!("Running diff with config: {:?}", diff_config); + let result = diff::diff_objs(&diff_config, &mapping_config, left, right, None).to_js()?; let left = left.and_then(|o| result.left.as_ref().map(|d| (o, d))); let right = right.and_then(|o| result.right.as_ref().map(|d| (o, d))); Ok(DiffResult::new(left, right)) @@ -46,9 +48,10 @@ fn run_diff( pub fn run_diff_proto( left: Option>, right: Option>, - config: diff::DiffObjConfig, + diff_config: diff::DiffObjConfig, + mapping_config: diff::MappingConfig, ) -> Result, JsError> { - let out = parse_and_run_diff(left, right, config)?; + let out = parse_and_run_diff(left, right, diff_config, mapping_config)?; Ok(out.encode_to_vec().into_boxed_slice()) } diff --git a/objdiff-core/src/config/mod.rs b/objdiff-core/src/config/mod.rs index 26f11fc..3b39ab0 100644 --- a/objdiff-core/src/config/mod.rs +++ b/objdiff-core/src/config/mod.rs @@ -1,4 +1,5 @@ use std::{ + collections::BTreeMap, fs, fs::File, io::{BufReader, BufWriter, Read}, @@ -27,7 +28,7 @@ pub struct ProjectConfig { #[serde(default, skip_serializing_if = "Option::is_none")] pub build_target: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub watch_patterns: Option>, + pub watch_patterns: Option>, #[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")] pub units: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -52,6 +53,17 @@ impl ProjectConfig { pub fn progress_categories_mut(&mut self) -> &mut Vec { self.progress_categories.get_or_insert_with(Vec::new) } + + pub fn build_watch_patterns(&self) -> Result, globset::Error> { + Ok(if let Some(watch_patterns) = &self.watch_patterns { + watch_patterns + .iter() + .map(|s| Glob::new(s)) + .collect::, globset::Error>>()? + } else { + DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() + }) + } } #[derive(Default, Clone, serde::Serialize, serde::Deserialize)] @@ -79,12 +91,8 @@ pub struct ProjectObject { pub symbol_mappings: Option, } -#[cfg(feature = "wasm")] -#[tsify_next::declare] -pub type SymbolMappings = std::collections::BTreeMap; - -#[cfg(not(feature = "wasm"))] -pub type SymbolMappings = bimap::BiBTreeMap; +#[cfg_attr(feature = "wasm", tsify_next::declare)] +pub type SymbolMappings = BTreeMap; #[derive(Default, Clone, serde::Serialize, serde::Deserialize)] #[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] diff --git a/objdiff-core/src/diff/mod.rs b/objdiff-core/src/diff/mod.rs index 2401e4d..405645d 100644 --- a/objdiff-core/src/diff/mod.rs +++ b/objdiff-core/src/diff/mod.rs @@ -18,187 +18,7 @@ pub mod code; pub mod data; pub mod display; -#[derive( - Debug, - Copy, - Clone, - Default, - Eq, - PartialEq, - serde::Deserialize, - serde::Serialize, - strum::VariantArray, - strum::EnumMessage, -)] -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -pub enum X86Formatter { - #[default] - #[strum(message = "Intel (default)")] - Intel, - #[strum(message = "AT&T")] - Gas, - #[strum(message = "NASM")] - Nasm, - #[strum(message = "MASM")] - Masm, -} - -#[derive( - Debug, - Copy, - Clone, - Default, - Eq, - PartialEq, - serde::Deserialize, - serde::Serialize, - strum::VariantArray, - strum::EnumMessage, -)] -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -pub enum MipsAbi { - #[default] - #[strum(message = "Auto (default)")] - Auto, - #[strum(message = "O32")] - O32, - #[strum(message = "N32")] - N32, - #[strum(message = "N64")] - N64, -} - -#[derive( - Debug, - Copy, - Clone, - Default, - Eq, - PartialEq, - serde::Deserialize, - serde::Serialize, - strum::VariantArray, - strum::EnumMessage, -)] -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -pub enum MipsInstrCategory { - #[default] - #[strum(message = "Auto (default)")] - Auto, - #[strum(message = "CPU")] - Cpu, - #[strum(message = "RSP (N64)")] - Rsp, - #[strum(message = "R3000 GTE (PS1)")] - R3000Gte, - #[strum(message = "R4000 ALLEGREX (PSP)")] - R4000Allegrex, - #[strum(message = "R5900 EE (PS2)")] - R5900, -} - -#[derive( - Debug, - Copy, - Clone, - Default, - Eq, - PartialEq, - serde::Deserialize, - serde::Serialize, - strum::VariantArray, - strum::EnumMessage, -)] -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -pub enum ArmArchVersion { - #[default] - #[strum(message = "Auto (default)")] - Auto, - #[strum(message = "ARMv4T (GBA)")] - V4T, - #[strum(message = "ARMv5TE (DS)")] - V5TE, - #[strum(message = "ARMv6K (3DS)")] - V6K, -} - -#[derive( - Debug, - Copy, - Clone, - Default, - Eq, - PartialEq, - serde::Deserialize, - serde::Serialize, - strum::VariantArray, - strum::EnumMessage, -)] -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify))] -pub enum ArmR9Usage { - #[default] - #[strum( - message = "R9 or V6 (default)", - detailed_message = "Use R9 as a general-purpose register." - )] - GeneralPurpose, - #[strum( - message = "SB (static base)", - detailed_message = "Used for position-independent data (PID)." - )] - Sb, - #[strum(message = "TR (TLS register)", detailed_message = "Used for thread-local storage.")] - Tr, -} - -#[inline] -const fn default_true() -> bool { true } - -#[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] -#[serde(default)] -pub struct DiffObjConfig { - pub relax_reloc_diffs: bool, - #[serde(default = "default_true")] - pub space_between_args: bool, - pub combine_data_sections: bool, - #[serde(default)] - pub symbol_mappings: MappingConfig, - // x86 - pub x86_formatter: X86Formatter, - // MIPS - pub mips_abi: MipsAbi, - pub mips_instr_category: MipsInstrCategory, - // ARM - pub arm_arch_version: ArmArchVersion, - pub arm_unified_syntax: bool, - pub arm_av_registers: bool, - pub arm_r9_usage: ArmR9Usage, - pub arm_sl_usage: bool, - pub arm_fp_usage: bool, - pub arm_ip_usage: bool, -} - -impl Default for DiffObjConfig { - fn default() -> Self { - Self { - relax_reloc_diffs: false, - space_between_args: true, - combine_data_sections: false, - symbol_mappings: Default::default(), - x86_formatter: Default::default(), - mips_abi: Default::default(), - mips_instr_category: Default::default(), - arm_arch_version: Default::default(), - arm_unified_syntax: true, - arm_av_registers: false, - arm_r9_usage: Default::default(), - arm_sl_usage: false, - arm_fp_usage: false, - arm_ip_usage: false, - } - } -} +include!(concat!(env!("OUT_DIR"), "/config.gen.rs")); impl DiffObjConfig { pub fn separator(&self) -> &'static str { @@ -385,12 +205,13 @@ pub struct DiffObjsResult { } pub fn diff_objs( - config: &DiffObjConfig, + diff_config: &DiffObjConfig, + mapping_config: &MappingConfig, left: Option<&ObjInfo>, right: Option<&ObjInfo>, prev: Option<&ObjInfo>, ) -> Result { - let symbol_matches = matching_symbols(left, right, prev, &config.symbol_mappings)?; + let symbol_matches = matching_symbols(left, right, prev, mapping_config)?; let section_matches = matching_sections(left, right)?; let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p))); let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p))); @@ -408,8 +229,10 @@ pub fn diff_objs( let (right_obj, right_out) = right.as_mut().unwrap(); match section_kind { ObjSectionKind::Code => { - let left_code = process_code_symbol(left_obj, left_symbol_ref, config)?; - let right_code = process_code_symbol(right_obj, right_symbol_ref, config)?; + let left_code = + process_code_symbol(left_obj, left_symbol_ref, diff_config)?; + let right_code = + process_code_symbol(right_obj, right_symbol_ref, diff_config)?; let (left_diff, right_diff) = diff_code( left_obj, right_obj, @@ -417,14 +240,15 @@ pub fn diff_objs( &right_code, left_symbol_ref, right_symbol_ref, - config, + diff_config, )?; *left_out.symbol_diff_mut(left_symbol_ref) = left_diff; *right_out.symbol_diff_mut(right_symbol_ref) = right_diff; if let Some(prev_symbol_ref) = prev_symbol_ref { let (prev_obj, prev_out) = prev.as_mut().unwrap(); - let prev_code = process_code_symbol(prev_obj, prev_symbol_ref, config)?; + let prev_code = + process_code_symbol(prev_obj, prev_symbol_ref, diff_config)?; let (_, prev_diff) = diff_code( left_obj, right_obj, @@ -432,7 +256,7 @@ pub fn diff_objs( &prev_code, right_symbol_ref, prev_symbol_ref, - config, + diff_config, )?; *prev_out.symbol_diff_mut(prev_symbol_ref) = prev_diff; } @@ -463,7 +287,7 @@ pub fn diff_objs( let (left_obj, left_out) = left.as_mut().unwrap(); match section_kind { ObjSectionKind::Code => { - let code = process_code_symbol(left_obj, left_symbol_ref, config)?; + let code = process_code_symbol(left_obj, left_symbol_ref, diff_config)?; *left_out.symbol_diff_mut(left_symbol_ref) = no_diff_code(&code, left_symbol_ref)?; } @@ -477,7 +301,7 @@ pub fn diff_objs( let (right_obj, right_out) = right.as_mut().unwrap(); match section_kind { ObjSectionKind::Code => { - let code = process_code_symbol(right_obj, right_symbol_ref, config)?; + let code = process_code_symbol(right_obj, right_symbol_ref, diff_config)?; *right_out.symbol_diff_mut(right_symbol_ref) = no_diff_code(&code, right_symbol_ref)?; } @@ -548,11 +372,11 @@ pub fn diff_objs( if let (Some((right_obj, right_out)), Some((left_obj, left_out))) = (right.as_mut(), left.as_mut()) { - if let Some(right_name) = &config.symbol_mappings.selecting_left { - generate_mapping_symbols(right_obj, right_name, left_obj, left_out, config)?; + if let Some(right_name) = &mapping_config.selecting_left { + generate_mapping_symbols(right_obj, right_name, left_obj, left_out, diff_config)?; } - if let Some(left_name) = &config.symbol_mappings.selecting_right { - generate_mapping_symbols(left_obj, left_name, right_obj, right_out, config)?; + if let Some(left_name) = &mapping_config.selecting_right { + generate_mapping_symbols(left_obj, left_name, right_obj, right_out, diff_config)?; } } @@ -635,8 +459,9 @@ struct SectionMatch { section_kind: ObjSectionKind, } -#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)] #[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] +#[serde(default)] pub struct MappingConfig { /// Manual symbol mappings pub mappings: SymbolMappings, diff --git a/objdiff-core/src/jobs/objdiff.rs b/objdiff-core/src/jobs/objdiff.rs index d9c33af..36ca981 100644 --- a/objdiff-core/src/jobs/objdiff.rs +++ b/objdiff-core/src/jobs/objdiff.rs @@ -5,7 +5,6 @@ use time::OffsetDateTime; use crate::{ build::{run_make, BuildConfig, BuildStatus}, - config::SymbolMappings, diff::{diff_objs, DiffObjConfig, MappingConfig, ObjDiff}, jobs::{start_job, update_status, Job, JobContext, JobResult, JobState}, obj::{read, ObjInfo}, @@ -18,9 +17,7 @@ pub struct ObjDiffConfig { pub target_path: Option, pub base_path: Option, pub diff_obj_config: DiffObjConfig, - pub symbol_mappings: SymbolMappings, - pub selecting_left: Option, - pub selecting_right: Option, + pub mapping_config: MappingConfig, } pub struct ObjDiffResult { @@ -34,15 +31,8 @@ pub struct ObjDiffResult { fn run_build( context: &JobContext, cancel: Receiver<()>, - mut config: ObjDiffConfig, + config: ObjDiffConfig, ) -> Result> { - // Use the per-object symbol mappings, we don't set mappings globally - config.diff_obj_config.symbol_mappings = MappingConfig { - mappings: config.symbol_mappings, - selecting_left: config.selecting_left, - selecting_right: config.selecting_right, - }; - let mut target_path_rel = None; let mut base_path_rel = None; if config.build_target || config.build_base { @@ -180,7 +170,13 @@ fn run_build( update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?; step_idx += 1; - let result = diff_objs(&config.diff_obj_config, first_obj.as_ref(), second_obj.as_ref(), None)?; + let result = diff_objs( + &config.diff_obj_config, + &config.mapping_config, + first_obj.as_ref(), + second_obj.as_ref(), + None, + )?; update_status(context, "Complete".to_string(), step_idx, total, &cancel)?; Ok(Box::new(ObjDiffResult { diff --git a/objdiff-gui/src/app.rs b/objdiff-gui/src/app.rs index d6f6b66..9bfdb22 100644 --- a/objdiff-gui/src/app.rs +++ b/objdiff-gui/src/app.rs @@ -30,7 +30,8 @@ use crate::{ views::{ appearance::{appearance_window, Appearance}, config::{ - arch_config_window, config_ui, project_window, ConfigViewState, CONFIG_DISABLED_TEXT, + arch_config_window, config_ui, general_config_ui, project_window, ConfigViewState, + CONFIG_DISABLED_TEXT, }, data_diff::data_diff_ui, debug::debug_window, @@ -293,7 +294,7 @@ impl AppState { let Some(object) = self.config.selected_obj.as_mut() else { return; }; - object.symbol_mappings.remove_by_right(right); + object.symbol_mappings.retain(|_, r| r != right); self.selecting_left = Some(right.to_string()); self.queue_reload = true; self.save_config(); @@ -303,7 +304,7 @@ impl AppState { let Some(object) = self.config.selected_obj.as_mut() else { return; }; - object.symbol_mappings.remove_by_left(left); + object.symbol_mappings.retain(|l, _| l != left); self.selecting_right = Some(left.to_string()); self.queue_reload = true; self.save_config(); @@ -316,10 +317,8 @@ impl AppState { }; self.selecting_left = None; self.selecting_right = None; - if left == right { - object.symbol_mappings.remove_by_left(&left); - object.symbol_mappings.remove_by_right(&right); - } else { + object.symbol_mappings.retain(|l, r| l != &left && r != &right); + if left != right { object.symbol_mappings.insert(left.clone(), right.clone()); } self.queue_reload = true; @@ -728,37 +727,9 @@ impl eframe::App for App { &mut diff_state.symbol_state.show_hidden_symbols, "Show hidden symbols", ); - if ui - .checkbox( - &mut state.config.diff_obj_config.relax_reloc_diffs, - "Relax relocation diffs", - ) - .on_hover_text( - "Ignores differences in relocation targets. (Address, name, etc)", - ) - .changed() - { - state.queue_reload = true; - } - if ui - .checkbox( - &mut state.config.diff_obj_config.space_between_args, - "Space between args", - ) - .changed() - { - state.queue_reload = true; - } - if ui - .checkbox( - &mut state.config.diff_obj_config.combine_data_sections, - "Combine data sections", - ) - .on_hover_text("Combines data sections with equal names.") - .changed() - { - state.queue_reload = true; - } + ui.separator(); + general_config_ui(ui, &mut state); + ui.separator(); if ui.button("Clear custom symbol mappings").clicked() { state.clear_mappings(); diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff()); diff --git a/objdiff-gui/src/app_config.rs b/objdiff-gui/src/app_config.rs index 965324c..2647949 100644 --- a/objdiff-gui/src/app_config.rs +++ b/objdiff-gui/src/app_config.rs @@ -160,7 +160,6 @@ impl DiffObjConfigV1 { arm_sl_usage: self.arm_sl_usage, arm_fp_usage: self.arm_fp_usage, arm_ip_usage: self.arm_ip_usage, - ..Default::default() } } } diff --git a/objdiff-gui/src/config.rs b/objdiff-gui/src/config.rs index 95d34d9..86d14e0 100644 --- a/objdiff-gui/src/config.rs +++ b/objdiff-gui/src/config.rs @@ -99,9 +99,15 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> { state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p)); state.config.build_base = project_config.build_base.unwrap_or(true); state.config.build_target = project_config.build_target.unwrap_or(false); - state.config.watch_patterns = project_config.watch_patterns.clone().unwrap_or_else(|| { - DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() - }); + if let Some(watch_patterns) = &project_config.watch_patterns { + state.config.watch_patterns = watch_patterns + .iter() + .map(|s| Glob::new(s)) + .collect::, globset::Error>>()?; + } else { + state.config.watch_patterns = + DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(); + } state.watcher_change = true; state.objects = project_config.units.clone().unwrap_or_default(); state.object_nodes = build_nodes( diff --git a/objdiff-gui/src/jobs.rs b/objdiff-gui/src/jobs.rs index fadd635..0f9168b 100644 --- a/objdiff-gui/src/jobs.rs +++ b/objdiff-gui/src/jobs.rs @@ -7,6 +7,7 @@ use anyhow::{bail, Result}; use jobs::create_scratch; use objdiff_core::{ build::BuildConfig, + diff::MappingConfig, jobs, jobs::{check_update::CheckUpdateConfig, objdiff, update::UpdateConfig, Job, JobQueue}, }; @@ -106,15 +107,17 @@ pub fn create_objdiff_config(state: &AppState) -> objdiff::ObjDiffConfig { .and_then(|obj| obj.base_path.as_ref()) .cloned(), diff_obj_config: state.config.diff_obj_config.clone(), - symbol_mappings: state - .config - .selected_obj - .as_ref() - .map(|obj| &obj.symbol_mappings) - .cloned() - .unwrap_or_default(), - selecting_left: state.selecting_left.clone(), - selecting_right: state.selecting_right.clone(), + mapping_config: MappingConfig { + mappings: state + .config + .selected_obj + .as_ref() + .map(|obj| &obj.symbol_mappings) + .cloned() + .unwrap_or_default(), + selecting_left: state.selecting_left.clone(), + selecting_right: state.selecting_right.clone(), + }, } } diff --git a/objdiff-gui/src/views/config.rs b/objdiff-gui/src/views/config.rs index 3eda501..abcf473 100644 --- a/objdiff-gui/src/views/config.rs +++ b/objdiff-gui/src/views/config.rs @@ -14,10 +14,12 @@ use egui::{ use globset::Glob; use objdiff_core::{ config::{ProjectObject, DEFAULT_WATCH_PATTERNS}, - diff::{ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter}, + diff::{ + ConfigEnum, ConfigEnumVariantInfo, ConfigPropertyId, ConfigPropertyKind, + ConfigPropertyValue, CONFIG_GROUPS, + }, jobs::{check_update::CheckUpdateResult, Job, JobQueue, JobResult}, }; -use strum::{EnumMessage, VariantArray}; use crate::{ app::{AppConfig, AppState, AppStateRef, ObjectConfig}, @@ -874,121 +876,102 @@ pub fn arch_config_window( }); } +fn config_property_ui( + ui: &mut egui::Ui, + state: &mut AppState, + property_id: ConfigPropertyId, +) -> bool { + let mut changed = false; + let current_value = state.config.diff_obj_config.get_property_value(property_id); + match (property_id.kind(), current_value) { + (ConfigPropertyKind::Boolean, ConfigPropertyValue::Boolean(mut checked)) => { + let mut response = ui.checkbox(&mut checked, property_id.name()); + if let Some(description) = property_id.description() { + response = response.on_hover_text(description); + } + if response.changed() { + state + .config + .diff_obj_config + .set_property_value(property_id, ConfigPropertyValue::Boolean(checked)) + .expect("Failed to set property value"); + changed = true; + } + } + (ConfigPropertyKind::Choice(variants), ConfigPropertyValue::Choice(selected)) => { + fn variant_name(variant: &ConfigEnumVariantInfo) -> String { + if variant.is_default { + format!("{} (default)", variant.name) + } else { + variant.name.to_string() + } + } + let selected_variant = variants + .iter() + .find(|v| v.value == selected) + .or_else(|| variants.iter().find(|v| v.is_default)) + .expect("Invalid choice variant"); + let response = egui::ComboBox::new(property_id.name(), property_id.name()) + .selected_text(variant_name(selected_variant)) + .show_ui(ui, |ui| { + for variant in variants { + let mut response = + ui.selectable_label(selected == variant.value, variant_name(variant)); + if let Some(description) = variant.description { + response = response.on_hover_text(description); + } + if response.clicked() { + state + .config + .diff_obj_config + .set_property_value( + property_id, + ConfigPropertyValue::Choice(variant.value), + ) + .expect("Failed to set property value"); + changed = true; + } + } + }) + .response; + if let Some(description) = property_id.description() { + response.on_hover_text(description); + } + } + _ => panic!("Incompatible property kind and value"), + } + changed +} + fn arch_config_ui(ui: &mut egui::Ui, state: &mut AppState, _appearance: &Appearance) { - ui.heading("x86"); - egui::ComboBox::new("x86_formatter", "Format") - .selected_text(state.config.diff_obj_config.x86_formatter.get_message().unwrap()) - .show_ui(ui, |ui| { - for &formatter in X86Formatter::VARIANTS { - if ui - .selectable_label( - state.config.diff_obj_config.x86_formatter == formatter, - formatter.get_message().unwrap(), - ) - .clicked() - { - state.config.diff_obj_config.x86_formatter = formatter; - state.queue_reload = true; - } - } - }); - ui.separator(); - ui.heading("MIPS"); - egui::ComboBox::new("mips_abi", "ABI") - .selected_text(state.config.diff_obj_config.mips_abi.get_message().unwrap()) - .show_ui(ui, |ui| { - for &abi in MipsAbi::VARIANTS { - if ui - .selectable_label( - state.config.diff_obj_config.mips_abi == abi, - abi.get_message().unwrap(), - ) - .clicked() - { - state.config.diff_obj_config.mips_abi = abi; - state.queue_reload = true; - } - } - }); - egui::ComboBox::new("mips_instr_category", "Instruction Category") - .selected_text(state.config.diff_obj_config.mips_instr_category.get_message().unwrap()) - .show_ui(ui, |ui| { - for &category in MipsInstrCategory::VARIANTS { - if ui - .selectable_label( - state.config.diff_obj_config.mips_instr_category == category, - category.get_message().unwrap(), - ) - .clicked() - { - state.config.diff_obj_config.mips_instr_category = category; - state.queue_reload = true; - } - } - }); - ui.separator(); - ui.heading("ARM"); - egui::ComboBox::new("arm_arch_version", "Architecture Version") - .selected_text(state.config.diff_obj_config.arm_arch_version.get_message().unwrap()) - .show_ui(ui, |ui| { - for &version in ArmArchVersion::VARIANTS { - if ui - .selectable_label( - state.config.diff_obj_config.arm_arch_version == version, - version.get_message().unwrap(), - ) - .clicked() - { - state.config.diff_obj_config.arm_arch_version = version; - state.queue_reload = true; - } - } - }); - let response = ui - .checkbox(&mut state.config.diff_obj_config.arm_unified_syntax, "Unified syntax") - .on_hover_text("Disassemble as unified assembly language (UAL)."); - if response.changed() { - state.queue_reload = true; + let mut first = true; + let mut changed = false; + for group in CONFIG_GROUPS { + if group.id == "general" { + continue; + } + if first { + first = false; + } else { + ui.separator(); + } + ui.heading(group.name); + for property_id in group.properties.iter().cloned() { + changed |= config_property_ui(ui, state, property_id); + } } - let response = ui - .checkbox(&mut state.config.diff_obj_config.arm_av_registers, "Use A/V registers") - .on_hover_text("Display R0-R3 as A1-A4 and R4-R11 as V1-V8"); - if response.changed() { + if changed { state.queue_reload = true; } - egui::ComboBox::new("arm_r9_usage", "Display R9 as") - .selected_text(state.config.diff_obj_config.arm_r9_usage.get_message().unwrap()) - .show_ui(ui, |ui| { - for &usage in ArmR9Usage::VARIANTS { - if ui - .selectable_label( - state.config.diff_obj_config.arm_r9_usage == usage, - usage.get_message().unwrap(), - ) - .on_hover_text(usage.get_detailed_message().unwrap()) - .clicked() - { - state.config.diff_obj_config.arm_r9_usage = usage; - state.queue_reload = true; - } - } - }); - let response = ui - .checkbox(&mut state.config.diff_obj_config.arm_sl_usage, "Display R10 as SL") - .on_hover_text("Used for explicit stack limits."); - if response.changed() { - state.queue_reload = true; - } - let response = ui - .checkbox(&mut state.config.diff_obj_config.arm_fp_usage, "Display R11 as FP") - .on_hover_text("Used for frame pointers."); - if response.changed() { - state.queue_reload = true; +} + +pub fn general_config_ui(ui: &mut egui::Ui, state: &mut AppState) { + let mut changed = false; + let group = CONFIG_GROUPS.iter().find(|group| group.id == "general").unwrap(); + for property_id in group.properties.iter().cloned() { + changed |= config_property_ui(ui, state, property_id); } - let response = ui - .checkbox(&mut state.config.diff_obj_config.arm_ip_usage, "Display R12 as IP") - .on_hover_text("Used for interworking and long branches."); - if response.changed() { + if changed { state.queue_reload = true; } } diff --git a/objdiff-wasm/package.json b/objdiff-wasm/package.json index bfaba00..d6a6f27 100644 --- a/objdiff-wasm/package.json +++ b/objdiff-wasm/package.json @@ -1,6 +1,6 @@ { "name": "objdiff-wasm", - "version": "2.5.0", + "version": "2.6.0", "description": "A local diffing tool for decompilation projects.", "author": { "name": "Luke Street", diff --git a/objdiff-wasm/src/main.ts b/objdiff-wasm/src/main.ts index 0847479..1704778 100644 --- a/objdiff-wasm/src/main.ts +++ b/objdiff-wasm/src/main.ts @@ -111,12 +111,12 @@ async function defer(message: AnyHandlerData, worker?: Worker): Promise { return promise; } -export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, config?: DiffObjConfig): Promise { +export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, diff_config?: DiffObjConfig): Promise { const data = await defer({ type: 'run_diff_proto', left, right, - config + diff_config }); const parseStart = performance.now(); const result = DiffResult.fromBinary(data, {readUnknownField: false}); diff --git a/objdiff-wasm/src/worker.ts b/objdiff-wasm/src/worker.ts index defad77..0ac9374 100644 --- a/objdiff-wasm/src/worker.ts +++ b/objdiff-wasm/src/worker.ts @@ -38,13 +38,15 @@ async function initIfNeeded() { // return exports.run_diff_json(left, right, cfg); // } -async function run_diff_proto({left, right, config}: { +async function run_diff_proto({left, right, diff_config, mapping_config}: { left: Uint8Array | undefined, right: Uint8Array | undefined, - config?: exports.DiffObjConfig, + diff_config?: exports.DiffObjConfig, + mapping_config?: exports.MappingConfig, }): Promise { - config = config || {}; - return exports.run_diff_proto(left, right, config); + diff_config = diff_config || {}; + mapping_config = mapping_config || {}; + return exports.run_diff_proto(left, right, diff_config, mapping_config); } export type AnyHandlerData = HandlerData[keyof HandlerData];