├── .cargo └── config.toml ├── .editorconfig ├── .github └── workflows │ └── build.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── assets ├── screen-diff.png └── screen-symbols.png ├── config.schema.json ├── deny.toml ├── objdiff-cli ├── Cargo.toml └── src │ ├── argp_version.rs │ ├── cmd │ ├── diff.rs │ ├── mod.rs │ └── report.rs │ ├── main.rs │ ├── util │ ├── mod.rs │ ├── output.rs │ └── term.rs │ └── views │ ├── function_diff.rs │ └── mod.rs ├── objdiff-core ├── Cargo.toml ├── README.md ├── build.rs ├── config-schema.json ├── config_gen.rs ├── protos │ ├── changes.proto │ ├── diff.proto │ ├── proto_descriptor.bin │ └── report.proto ├── src │ ├── arch │ │ ├── arm.rs │ │ ├── arm64.rs │ │ ├── mips.rs │ │ ├── mod.rs │ │ ├── ppc.rs │ │ ├── superh │ │ │ ├── disasm.rs │ │ │ └── mod.rs │ │ └── x86.rs │ ├── bindings │ │ ├── diff.rs │ │ ├── mod.rs │ │ └── report.rs │ ├── build │ │ ├── mod.rs │ │ └── watcher.rs │ ├── config │ │ ├── mod.rs │ │ └── path.rs │ ├── diff │ │ ├── code.rs │ │ ├── data.rs │ │ ├── display.rs │ │ └── mod.rs │ ├── jobs │ │ ├── check_update.rs │ │ ├── create_scratch.rs │ │ ├── mod.rs │ │ ├── objdiff.rs │ │ └── update.rs │ ├── lib.rs │ ├── obj │ │ ├── mod.rs │ │ ├── read.rs │ │ ├── snapshots │ │ │ └── objdiff_core__obj__read__test__combine_sections.snap │ │ └── split_meta.rs │ └── util.rs └── tests │ ├── arch_arm.rs │ ├── arch_mips.rs │ ├── arch_ppc.rs │ ├── arch_x86.rs │ ├── common.rs │ ├── data │ ├── arm │ │ ├── LinkStateItem.o │ │ ├── enemy300.o │ │ └── thumb.o │ ├── mips │ │ ├── code_be.o │ │ ├── code_le.o │ │ ├── main.c.o │ │ └── vw_main.c.o │ ├── ppc │ │ ├── CDamageVulnerability_base.o │ │ ├── CDamageVulnerability_target.o │ │ ├── IObj.o │ │ ├── NMWException.o │ │ └── m_Do_hostIO.o │ ├── x86 │ │ ├── basenode.obj │ │ ├── jumptable.o │ │ ├── local_labels.obj │ │ ├── rtest.obj │ │ └── staticdebug.obj │ └── x86_64 │ │ └── vs2022.o │ └── snapshots │ ├── arch_arm__combine_text_sections-2.snap │ ├── arch_arm__combine_text_sections.snap │ ├── arch_arm__read_arm-2.snap │ ├── arch_arm__read_arm-3.snap │ ├── arch_arm__read_arm.snap │ ├── arch_arm__read_thumb-2.snap │ ├── arch_arm__read_thumb-3.snap │ ├── arch_arm__read_thumb.snap │ ├── arch_mips__filter_non_matching.snap │ ├── arch_mips__read_mips-2.snap │ ├── arch_mips__read_mips-3.snap │ ├── arch_mips__read_mips.snap │ ├── arch_ppc__diff_ppc-2.snap │ ├── arch_ppc__diff_ppc.snap │ ├── arch_ppc__read_dwarf1_line_info.snap │ ├── arch_ppc__read_extab.snap │ ├── arch_ppc__read_ppc-2.snap │ ├── arch_ppc__read_ppc-3.snap │ ├── arch_ppc__read_ppc.snap │ ├── arch_x86__display_section_ordering.snap │ ├── arch_x86__read_x86-2.snap │ ├── arch_x86__read_x86-3.snap │ ├── arch_x86__read_x86.snap │ ├── arch_x86__read_x86_64-2.snap │ ├── arch_x86__read_x86_64-3.snap │ ├── arch_x86__read_x86_64.snap │ ├── arch_x86__read_x86_combine_sections.snap │ ├── arch_x86__read_x86_jumptable-2.snap │ ├── arch_x86__read_x86_jumptable-3.snap │ ├── arch_x86__read_x86_jumptable.snap │ └── arch_x86__read_x86_local_labels.snap ├── objdiff-gui ├── Cargo.toml ├── assets │ ├── icon.ico │ ├── icon.png │ └── icon_64.png ├── build.rs └── src │ ├── app.rs │ ├── app_config.rs │ ├── config.rs │ ├── fonts │ ├── matching.rs │ └── mod.rs │ ├── hotkeys.rs │ ├── jobs.rs │ ├── main.rs │ ├── update.rs │ └── views │ ├── appearance.rs │ ├── column_layout.rs │ ├── config.rs │ ├── data_diff.rs │ ├── debug.rs │ ├── demangle.rs │ ├── diff.rs │ ├── extab_diff.rs │ ├── file.rs │ ├── frame_history.rs │ ├── function_diff.rs │ ├── graphics.rs │ ├── jobs.rs │ ├── mod.rs │ ├── rlwinm.rs │ └── symbol_diff.rs ├── objdiff-wasm ├── .gitignore ├── Cargo.toml ├── biome.json ├── build.rs ├── lib │ └── wasi-logging.ts ├── package-lock.json ├── package.json ├── rslib.config.ts ├── src │ ├── api.rs │ ├── cabi_realloc.rs │ ├── lib.rs │ └── logging.rs ├── tsconfig.json └── wit │ ├── .gitignore │ ├── deps.lock │ ├── deps.toml │ └── objdiff.wit └── rustfmt.toml /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # statically link the C runtime so the executable does not depend on 2 | # that shared/dynamic library. 3 | [target.'cfg(all(target_env = "msvc", target_os = "windows"))'] 4 | rustflags = ["-C", "target-feature=+crt-static"] 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.md] 2 | trim_trailing_whitespace = false 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | target/ 3 | **/*.rs.bk 4 | generated/ 5 | 6 | # macOS 7 | .DS_Store 8 | 9 | # JetBrains 10 | .idea 11 | 12 | # Generated SPIR-V 13 | *.spv 14 | 15 | # project 16 | textures/ 17 | android.keystore 18 | *.frag 19 | *.vert 20 | *.metal 21 | .vscode/ 22 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: trailing-whitespace 8 | args: [--markdown-linebreak-ext=md] 9 | - id: end-of-file-fixer 10 | - id: fix-byte-order-marker 11 | - id: check-yaml 12 | - id: check-added-large-files 13 | - repo: local 14 | hooks: 15 | - id: cargo-fmt 16 | name: cargo fmt 17 | description: Run cargo fmt on all project files. 18 | language: system 19 | entry: cargo 20 | args: ["+nightly", "fmt", "--all"] 21 | pass_filenames: false 22 | - id: cargo clippy 23 | name: cargo clippy 24 | description: Run cargo clippy on all project files. 25 | language: system 26 | entry: cargo 27 | args: ["+nightly", "clippy", "--all-targets", "--all-features"] 28 | pass_filenames: false 29 | - id: cargo-deny 30 | name: cargo deny 31 | description: Run cargo deny on all project files. 32 | language: system 33 | entry: cargo 34 | args: ["deny", "check"] 35 | pass_filenames: false 36 | always_run: true 37 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "objdiff-cli", 4 | "objdiff-core", 5 | "objdiff-gui", 6 | "objdiff-wasm", 7 | ] 8 | resolver = "3" 9 | 10 | [profile.release-lto] 11 | inherits = "release" 12 | lto = "fat" 13 | strip = "debuginfo" 14 | codegen-units = 1 15 | 16 | [workspace.package] 17 | version = "3.0.0-beta.9" 18 | authors = ["Luke Street "] 19 | edition = "2024" 20 | license = "MIT OR Apache-2.0" 21 | repository = "https://github.com/encounter/objdiff" 22 | rust-version = "1.85" 23 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2022 Luke Street. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/screen-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/assets/screen-diff.png -------------------------------------------------------------------------------- /assets/screen-symbols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/assets/screen-symbols.png -------------------------------------------------------------------------------- /objdiff-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "objdiff-cli" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | readme = "../README.md" 10 | description = """ 11 | A local diffing tool for decompilation projects. 12 | """ 13 | publish = false 14 | 15 | [dependencies] 16 | anyhow = "1.0" 17 | argp = "0.4" 18 | crossterm = "0.28" 19 | enable-ansi-support = "0.2" 20 | memmap2 = "0.9" 21 | objdiff-core = { path = "../objdiff-core", features = ["all"] } 22 | prost = "0.13" 23 | ratatui = "0.29" 24 | rayon = "1.10" 25 | serde = { version = "1.0", features = ["derive"] } 26 | serde_json = "1.0" 27 | supports-color = "3.0" 28 | time = { version = "0.3", features = ["formatting", "local-offset"] } 29 | tracing = "0.1" 30 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 31 | typed-path = "0.11" 32 | 33 | [target.'cfg(target_env = "musl")'.dependencies] 34 | mimalloc = "0.1" 35 | -------------------------------------------------------------------------------- /objdiff-cli/src/argp_version.rs: -------------------------------------------------------------------------------- 1 | // Originally from https://gist.github.com/suluke/e0c672492126be0a4f3b4f0e1115d77c 2 | //! Extend `argp` to be better integrated with the `cargo` ecosystem 3 | //! 4 | //! For now, this only adds a --version/-V option which causes early-exit. 5 | use std::ffi::OsStr; 6 | 7 | use argp::{EarlyExit, FromArgs, TopLevelCommand, parser::ParseGlobalOptions}; 8 | 9 | struct ArgsOrVersion(T) 10 | where T: FromArgs; 11 | 12 | impl TopLevelCommand for ArgsOrVersion where T: FromArgs {} 13 | 14 | impl FromArgs for ArgsOrVersion 15 | where T: FromArgs 16 | { 17 | fn _from_args( 18 | command_name: &[&str], 19 | args: &[&OsStr], 20 | parent: Option<&mut dyn ParseGlobalOptions>, 21 | ) -> Result { 22 | /// Also use argp for catching `--version`-only invocations 23 | #[derive(FromArgs)] 24 | struct Version { 25 | /// Print version information and exit. 26 | #[argp(switch, short = 'V')] 27 | pub version: bool, 28 | } 29 | 30 | match Version::from_args(command_name, args) { 31 | Ok(v) => { 32 | if v.version { 33 | println!( 34 | "{} {}", 35 | command_name.first().unwrap_or(&""), 36 | env!("CARGO_PKG_VERSION"), 37 | ); 38 | std::process::exit(0); 39 | } else { 40 | // Pass through empty arguments 41 | T::_from_args(command_name, args, parent).map(Self) 42 | } 43 | } 44 | Err(exit) => match exit { 45 | EarlyExit::Help(_help) => { 46 | // TODO: Chain help info from Version 47 | // For now, we just put the switch on T as well 48 | T::from_args(command_name, &["--help"]).map(Self) 49 | } 50 | EarlyExit::Err(_) => T::_from_args(command_name, args, parent).map(Self), 51 | }, 52 | } 53 | } 54 | } 55 | 56 | /// Create a `FromArgs` type from the current process’s `env::args`. 57 | /// 58 | /// This function will exit early from the current process if argument parsing was unsuccessful or if information like `--help` was requested. 59 | /// Error messages will be printed to stderr, and `--help` output to stdout. 60 | pub fn from_env() -> T 61 | where T: TopLevelCommand { 62 | argp::parse_args_or_exit::>(argp::DEFAULT).0 63 | } 64 | -------------------------------------------------------------------------------- /objdiff-cli/src/cmd/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod diff; 2 | pub mod report; 3 | 4 | use std::str::FromStr; 5 | 6 | use anyhow::{Context, Result, anyhow}; 7 | use objdiff_core::diff::{ConfigEnum, ConfigPropertyId, ConfigPropertyKind, DiffObjConfig}; 8 | 9 | pub fn apply_config_args(diff_config: &mut DiffObjConfig, args: &[String]) -> Result<()> { 10 | for config in args { 11 | let (key, value) = config.split_once('=').context("--config expects \"key=value\"")?; 12 | let property_id = ConfigPropertyId::from_str(key) 13 | .map_err(|()| anyhow!("Invalid configuration property: {}", key))?; 14 | diff_config.set_property_value_str(property_id, value).map_err(|()| { 15 | let mut options = String::new(); 16 | match property_id.kind() { 17 | ConfigPropertyKind::Boolean => { 18 | options = "true, false".to_string(); 19 | } 20 | ConfigPropertyKind::Choice(variants) => { 21 | for (i, variant) in variants.iter().enumerate() { 22 | if i > 0 { 23 | options.push_str(", "); 24 | } 25 | options.push_str(variant.value); 26 | } 27 | } 28 | } 29 | anyhow!("Invalid value for {}. Expected one of: {}", property_id.name(), options) 30 | })?; 31 | } 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /objdiff-cli/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::too_many_arguments)] 2 | 3 | mod argp_version; 4 | mod cmd; 5 | mod util; 6 | mod views; 7 | 8 | // musl's allocator is very slow, so use mimalloc when targeting musl. 9 | // Otherwise, use the system allocator to avoid extra code size. 10 | #[cfg(target_env = "musl")] 11 | #[global_allocator] 12 | static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; 13 | 14 | use std::{env, ffi::OsStr, fmt::Display, path::PathBuf, str::FromStr}; 15 | 16 | use anyhow::{Error, Result}; 17 | use argp::{FromArgValue, FromArgs}; 18 | use enable_ansi_support::enable_ansi_support; 19 | use supports_color::Stream; 20 | use tracing_subscriber::{EnvFilter, filter::LevelFilter}; 21 | 22 | #[derive(Debug, Eq, PartialEq, Copy, Clone)] 23 | enum LogLevel { 24 | Error, 25 | Warn, 26 | Info, 27 | Debug, 28 | Trace, 29 | } 30 | 31 | impl FromStr for LogLevel { 32 | type Err = (); 33 | 34 | fn from_str(s: &str) -> Result { 35 | Ok(match s { 36 | "error" => Self::Error, 37 | "warn" => Self::Warn, 38 | "info" => Self::Info, 39 | "debug" => Self::Debug, 40 | "trace" => Self::Trace, 41 | _ => return Err(()), 42 | }) 43 | } 44 | } 45 | 46 | impl Display for LogLevel { 47 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 48 | f.write_str(match self { 49 | LogLevel::Error => "error", 50 | LogLevel::Warn => "warn", 51 | LogLevel::Info => "info", 52 | LogLevel::Debug => "debug", 53 | LogLevel::Trace => "trace", 54 | }) 55 | } 56 | } 57 | 58 | impl FromArgValue for LogLevel { 59 | fn from_arg_value(value: &OsStr) -> Result { 60 | String::from_arg_value(value) 61 | .and_then(|s| Self::from_str(&s).map_err(|_| "Invalid log level".to_string())) 62 | } 63 | } 64 | 65 | #[derive(FromArgs, PartialEq, Debug)] 66 | /// A local diffing tool for decompilation projects. 67 | struct TopLevel { 68 | #[argp(subcommand)] 69 | command: SubCommand, 70 | #[argp(option, short = 'C')] 71 | /// Change working directory. 72 | chdir: Option, 73 | #[argp(option, short = 'L')] 74 | /// Minimum logging level. (Default: info) 75 | /// Possible values: error, warn, info, debug, trace 76 | log_level: Option, 77 | /// Print version information and exit. 78 | #[argp(switch, short = 'V')] 79 | version: bool, 80 | /// Disable color output. (env: NO_COLOR) 81 | #[argp(switch)] 82 | no_color: bool, 83 | } 84 | 85 | #[derive(FromArgs, PartialEq, Debug)] 86 | #[argp(subcommand)] 87 | enum SubCommand { 88 | Diff(cmd::diff::Args), 89 | Report(cmd::report::Args), 90 | } 91 | 92 | // Duplicated from supports-color so we can check early. 93 | fn env_no_color() -> bool { 94 | match env::var("NO_COLOR").as_deref() { 95 | Ok("") | Ok("0") | Err(_) => false, 96 | Ok(_) => true, 97 | } 98 | } 99 | 100 | fn main() { 101 | let args: TopLevel = argp_version::from_env(); 102 | let use_colors = if args.no_color || env_no_color() { 103 | false 104 | } else { 105 | // Try to enable ANSI support on Windows. 106 | let _ = enable_ansi_support(); 107 | // Disable isatty check for supports-color. (e.g. when used with ninja) 108 | unsafe { env::set_var("IGNORE_IS_TERMINAL", "1") }; 109 | supports_color::on(Stream::Stdout).is_some_and(|c| c.has_basic) 110 | }; 111 | 112 | let format = 113 | tracing_subscriber::fmt::format().with_ansi(use_colors).with_target(false).without_time(); 114 | let builder = tracing_subscriber::fmt().event_format(format).with_writer(std::io::stderr); 115 | if let Some(level) = args.log_level { 116 | builder 117 | .with_max_level(match level { 118 | LogLevel::Error => LevelFilter::ERROR, 119 | LogLevel::Warn => LevelFilter::WARN, 120 | LogLevel::Info => LevelFilter::INFO, 121 | LogLevel::Debug => LevelFilter::DEBUG, 122 | LogLevel::Trace => LevelFilter::TRACE, 123 | }) 124 | .init(); 125 | } else { 126 | builder 127 | .with_env_filter( 128 | EnvFilter::builder() 129 | .with_default_directive(LevelFilter::INFO.into()) 130 | .from_env_lossy(), 131 | ) 132 | .init(); 133 | } 134 | 135 | let mut result = Ok(()); 136 | if let Some(dir) = &args.chdir { 137 | result = env::set_current_dir(dir).map_err(|e| { 138 | Error::new(e) 139 | .context(format!("Failed to change working directory to '{}'", dir.display())) 140 | }); 141 | } 142 | result = result.and_then(|_| match args.command { 143 | SubCommand::Diff(c_args) => cmd::diff::run(c_args), 144 | SubCommand::Report(c_args) => cmd::report::run(c_args), 145 | }); 146 | if let Err(e) = result { 147 | eprintln!("Failed: {e:?}"); 148 | std::process::exit(1); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /objdiff-cli/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod output; 2 | pub mod term; 3 | -------------------------------------------------------------------------------- /objdiff-cli/src/util/output.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{BufWriter, Write}, 4 | ops::DerefMut, 5 | path::Path, 6 | }; 7 | 8 | use anyhow::{Context, Result, bail}; 9 | use tracing::info; 10 | 11 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] 12 | pub enum OutputFormat { 13 | #[default] 14 | Json, 15 | JsonPretty, 16 | Proto, 17 | } 18 | 19 | impl OutputFormat { 20 | pub fn from_str(s: &str) -> Result { 21 | match s.to_ascii_lowercase().as_str() { 22 | "json" => Ok(Self::Json), 23 | "json-pretty" | "json_pretty" => Ok(Self::JsonPretty), 24 | "binpb" | "pb" | "proto" | "protobuf" => Ok(Self::Proto), 25 | _ => bail!("Invalid output format: {}", s), 26 | } 27 | } 28 | 29 | pub fn from_option(s: Option<&str>) -> Result { 30 | match s { 31 | Some(s) => Self::from_str(s), 32 | None => Ok(Self::default()), 33 | } 34 | } 35 | } 36 | 37 | pub fn write_output(input: &T, output: Option

, format: OutputFormat) -> Result<()> 38 | where 39 | T: serde::Serialize + prost::Message, 40 | P: AsRef, 41 | { 42 | match output.as_ref().map(|p| p.as_ref()) { 43 | Some(output) if output != Path::new("-") => { 44 | info!("Writing to {}", output.display()); 45 | let file = File::options() 46 | .read(true) 47 | .write(true) 48 | .create(true) 49 | .truncate(true) 50 | .open(output) 51 | .with_context(|| format!("Failed to create file {}", output.display()))?; 52 | match format { 53 | OutputFormat::Json => { 54 | let mut output = BufWriter::new(file); 55 | serde_json::to_writer(&mut output, input) 56 | .context("Failed to write output file")?; 57 | output.flush().context("Failed to flush output file")?; 58 | } 59 | OutputFormat::JsonPretty => { 60 | let mut output = BufWriter::new(file); 61 | serde_json::to_writer_pretty(&mut output, input) 62 | .context("Failed to write output file")?; 63 | output.flush().context("Failed to flush output file")?; 64 | } 65 | OutputFormat::Proto => { 66 | file.set_len(input.encoded_len() as u64)?; 67 | let map = unsafe { memmap2::Mmap::map(&file) } 68 | .context("Failed to map output file")?; 69 | let mut output = map.make_mut().context("Failed to remap output file")?; 70 | input.encode(&mut output.deref_mut()).context("Failed to encode output")?; 71 | } 72 | } 73 | } 74 | _ => match format { 75 | OutputFormat::Json => { 76 | serde_json::to_writer(std::io::stdout(), input)?; 77 | } 78 | OutputFormat::JsonPretty => { 79 | serde_json::to_writer_pretty(std::io::stdout(), input)?; 80 | } 81 | OutputFormat::Proto => { 82 | std::io::stdout().write_all(&input.encode_to_vec())?; 83 | } 84 | }, 85 | } 86 | Ok(()) 87 | } 88 | -------------------------------------------------------------------------------- /objdiff-cli/src/util/term.rs: -------------------------------------------------------------------------------- 1 | use std::{io::stdout, panic}; 2 | 3 | use crossterm::{ 4 | cursor::Show, 5 | event::DisableMouseCapture, 6 | terminal::{LeaveAlternateScreen, disable_raw_mode}, 7 | }; 8 | 9 | pub fn crossterm_panic_handler() { 10 | let original_hook = panic::take_hook(); 11 | panic::set_hook(Box::new(move |panic_info| { 12 | let _ = crossterm::execute!(stdout(), LeaveAlternateScreen, DisableMouseCapture, Show); 13 | let _ = disable_raw_mode(); 14 | original_hook(panic_info); 15 | })); 16 | } 17 | -------------------------------------------------------------------------------- /objdiff-cli/src/views/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use crossterm::event::Event; 3 | use ratatui::Frame; 4 | 5 | use crate::cmd::diff::AppState; 6 | 7 | pub mod function_diff; 8 | 9 | #[derive(Default)] 10 | pub struct EventResult { 11 | pub redraw: bool, 12 | pub click_xy: Option<(u16, u16)>, 13 | } 14 | 15 | pub enum EventControlFlow { 16 | Break, 17 | Continue(EventResult), 18 | Reload, 19 | } 20 | 21 | pub trait UiView { 22 | fn draw(&mut self, state: &AppState, f: &mut Frame, result: &mut EventResult); 23 | fn handle_event(&mut self, state: &mut AppState, event: Event) -> EventControlFlow; 24 | fn reload(&mut self, state: &AppState) -> Result<()>; 25 | } 26 | -------------------------------------------------------------------------------- /objdiff-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "objdiff-core" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | readme = "README.md" 10 | description = """ 11 | A local diffing tool for decompilation projects. 12 | """ 13 | documentation = "https://docs.rs/objdiff-core" 14 | 15 | [features] 16 | default = ["std"] 17 | all = [ 18 | # Features 19 | "bindings", 20 | "build", 21 | "config", 22 | "dwarf", 23 | "serde", 24 | # Architectures 25 | "arm", 26 | "arm64", 27 | "mips", 28 | "ppc", 29 | "x86", 30 | "superh" 31 | ] 32 | # Implicit, used to check if any arch is enabled 33 | any-arch = [ 34 | "dep:flagset", 35 | "dep:heck", 36 | "dep:log", 37 | "dep:num-traits", 38 | "dep:prettyplease", 39 | "dep:proc-macro2", 40 | "dep:quote", 41 | "dep:regex", 42 | "dep:similar", 43 | "dep:syn", 44 | "dep:encoding_rs" 45 | ] 46 | bindings = [ 47 | "dep:prost", 48 | "dep:prost-build", 49 | ] 50 | build = [ 51 | "dep:notify", 52 | "dep:notify-debouncer-full", 53 | "dep:reqwest", 54 | "dep:self_update", 55 | "dep:shell-escape", 56 | "dep:tempfile", 57 | "dep:time", 58 | "dep:winapi", 59 | ] 60 | config = [ 61 | "dep:globset", 62 | "dep:semver", 63 | "dep:typed-path", 64 | ] 65 | dwarf = ["dep:gimli"] 66 | serde = [ 67 | "dep:pbjson", 68 | "dep:pbjson-build", 69 | "dep:serde", 70 | "dep:serde_json", 71 | ] 72 | std = [ 73 | "anyhow/std", 74 | "flagset?/std", 75 | "log?/std", 76 | "num-traits?/std", 77 | "object/std", 78 | "prost?/std", 79 | "serde?/std", 80 | "similar?/std", 81 | "typed-path?/std", 82 | "dep:filetime", 83 | "dep:memmap2", 84 | ] 85 | mips = [ 86 | "any-arch", 87 | "dep:cpp_demangle", 88 | "dep:cwdemangle", 89 | "dep:rabbitizer", 90 | ] 91 | ppc = [ 92 | "any-arch", 93 | "dep:cwdemangle", 94 | "dep:cwextab", 95 | "dep:ppc750cl", 96 | "dep:rlwinmdec", 97 | ] 98 | x86 = [ 99 | "any-arch", 100 | "dep:cpp_demangle", 101 | "dep:iced-x86", 102 | "dep:msvc-demangler", 103 | ] 104 | arm = [ 105 | "any-arch", 106 | "dep:arm-attr", 107 | "dep:cpp_demangle", 108 | "dep:unarm", 109 | ] 110 | arm64 = [ 111 | "any-arch", 112 | "dep:cpp_demangle", 113 | "dep:yaxpeax-arch", 114 | "dep:yaxpeax-arm", 115 | ] 116 | superh = [ 117 | "any-arch", 118 | ] 119 | 120 | [package.metadata.docs.rs] 121 | features = ["all"] 122 | 123 | [dependencies] 124 | anyhow = { version = "1.0", default-features = false } 125 | filetime = { version = "0.2", optional = true } 126 | flagset = { version = "0.4", default-features = false, optional = true, git = "https://github.com/enarx/flagset.git", rev = "a1fe9369b3741e43fec45da1998e83b9d78966a2" } 127 | itertools = { version = "0.14", default-features = false, features = ["use_alloc"] } 128 | log = { version = "0.4", default-features = false, optional = true } 129 | memmap2 = { version = "0.9", optional = true } 130 | num-traits = { version = "0.2", default-features = false, optional = true } 131 | object = { git = "https://github.com/gimli-rs/object", rev = "a74579249e21ab8fcd3a86be588de336f18297cb", default-features = false, features = ["read_core", "elf", "pe"] } 132 | pbjson = { version = "0.7", default-features = false, optional = true } 133 | prost = { version = "0.13", default-features = false, features = ["prost-derive"], optional = true } 134 | regex = { version = "1.11", default-features = false, features = [], optional = true } 135 | serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } 136 | similar = { version = "2.7", default-features = false, features = ["hashbrown"], optional = true, git = "https://github.com/encounter/similar.git", branch = "no_std" } 137 | typed-path = { version = "0.11", default-features = false, optional = true } 138 | 139 | # config 140 | globset = { version = "0.4", default-features = false, optional = true } 141 | semver = { version = "1.0", default-features = false, optional = true } 142 | serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true } 143 | 144 | # dwarf 145 | gimli = { version = "0.31", default-features = false, features = ["read"], optional = true } 146 | 147 | # ppc 148 | cwdemangle = { version = "1.0", optional = true } 149 | cwextab = { version = "1.0", optional = true } 150 | ppc750cl = { version = "0.3", optional = true } 151 | rlwinmdec = { version = "1.1", optional = true } 152 | 153 | # mips 154 | rabbitizer = { version = "2.0.0-alpha.1", default-features = false, features = ["all_extensions"], optional = true } 155 | 156 | # x86 157 | cpp_demangle = { version = "0.4", default-features = false, features = ["alloc"], optional = true } 158 | iced-x86 = { version = "1.21", default-features = false, features = ["decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums", "no_std"], optional = true } 159 | msvc-demangler = { version = "0.11", optional = true } 160 | 161 | # arm 162 | unarm = { version = "1.8", optional = true } 163 | arm-attr = { version = "0.2", optional = true } 164 | 165 | # arm64 166 | yaxpeax-arch = { version = "0.3", default-features = false, optional = true } 167 | yaxpeax-arm = { version = "0.3", default-features = false, optional = true } 168 | 169 | # build 170 | notify = { version = "8.0.0", optional = true } 171 | notify-debouncer-full = { version = "0.5.0", optional = true } 172 | shell-escape = { version = "0.1", optional = true } 173 | tempfile = { version = "3.19", optional = true } 174 | time = { version = "0.3", optional = true } 175 | encoding_rs = { version = "0.8.35", optional = true } 176 | 177 | [target.'cfg(windows)'.dependencies] 178 | winapi = { version = "0.3", optional = true } 179 | 180 | # For Linux static binaries, use rustls 181 | [target.'cfg(target_os = "linux")'.dependencies] 182 | reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "rustls-tls"], optional = true } 183 | self_update = { version = "0.42", default-features = false, features = ["rustls"], optional = true } 184 | 185 | # For all other platforms, use native TLS 186 | [target.'cfg(not(target_os = "linux"))'.dependencies] 187 | reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "multipart", "default-tls"], optional = true } 188 | self_update = { version = "0.42", optional = true } 189 | 190 | [build-dependencies] 191 | heck = { version = "0.5", optional = true } 192 | pbjson-build = { version = "0.7", optional = true } 193 | prettyplease = { version = "0.2", optional = true } 194 | proc-macro2 = { version = "1.0", optional = true } 195 | prost-build = { version = "0.13", optional = true } 196 | quote = { version = "1.0", optional = true } 197 | serde = { version = "1.0", features = ["derive"] } 198 | serde_json = { version = "1.0" } 199 | syn = { version = "2.0", optional = true } 200 | 201 | [dev-dependencies] 202 | # Enable all features for tests 203 | objdiff-core = { path = ".", features = ["all"] } 204 | insta = "1.43" 205 | -------------------------------------------------------------------------------- /objdiff-core/README.md: -------------------------------------------------------------------------------- 1 | # objdiff-core 2 | 3 | objdiff-core contains the core functionality of [objdiff](https://github.com/encounter/objdiff), a tool for comparing object files in decompilation projects. See the main repository for more information. 4 | 5 | ## Crate feature flags 6 | 7 | - **`all`**: Enables all main features. 8 | - **`bindings`**: Enables serialization and deserialization of objdiff data structures. 9 | - **`config`**: Enables objdiff configuration file support. 10 | - **`dwarf`**: Enables extraction of line number information from DWARF debug sections. 11 | - **`arm64`**: Enables the ARM64 backend powered by [yaxpeax-arm](https://github.com/iximeow/yaxpeax-arm). 12 | - **`arm`**: Enables the ARM backend powered by [unarm](https://github.com/AetiasHax/unarm). 13 | - **`mips`**: Enables the MIPS backend powered by [rabbitizer](https://github.com/Decompollaborate/rabbitizer). 14 | - **`ppc`**: Enables the PowerPC backend powered by [ppc750cl](https://github.com/encounter/ppc750cl). 15 | - **`superh`**: Enables the SuperH backend powered by an included disassembler. 16 | - **`x86`**: Enables the x86 backend powered by [iced-x86](https://crates.io/crates/iced-x86). 17 | -------------------------------------------------------------------------------- /objdiff-core/build.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "any-arch")] 2 | mod config_gen; 3 | 4 | fn main() -> Result<(), Box> { 5 | #[cfg(feature = "bindings")] 6 | compile_protos(); 7 | #[cfg(feature = "any-arch")] 8 | config_gen::generate_diff_config(); 9 | Ok(()) 10 | } 11 | 12 | #[cfg(feature = "bindings")] 13 | fn compile_protos() { 14 | use std::path::{Path, PathBuf}; 15 | let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos"); 16 | let descriptor_path = root.join("proto_descriptor.bin"); 17 | println!("cargo:rerun-if-changed={}", descriptor_path.display()); 18 | let descriptor_mtime = std::fs::metadata(&descriptor_path) 19 | .map(|m| m.modified().unwrap()) 20 | .unwrap_or(std::time::SystemTime::UNIX_EPOCH); 21 | let mut run_protoc = false; 22 | let proto_files = root 23 | .read_dir() 24 | .unwrap() 25 | .filter_map(|e| { 26 | let e = e.unwrap(); 27 | let path = e.path(); 28 | (path.extension() == Some(std::ffi::OsStr::new("proto"))).then_some(path) 29 | }) 30 | .collect::>(); 31 | for proto_file in &proto_files { 32 | println!("cargo:rerun-if-changed={}", proto_file.display()); 33 | let mtime = match std::fs::metadata(proto_file) { 34 | Ok(m) => m.modified().unwrap(), 35 | Err(e) => panic!("Failed to stat proto file {}: {:?}", proto_file.display(), e), 36 | }; 37 | if mtime > descriptor_mtime { 38 | run_protoc = true; 39 | } 40 | } 41 | 42 | fn prost_config(descriptor_path: &Path, run_protoc: bool) -> prost_build::Config { 43 | let mut config = prost_build::Config::new(); 44 | config.file_descriptor_set_path(descriptor_path); 45 | // If our cached descriptor is up-to-date, we don't need to run protoc. 46 | // This is helpful so that users don't need to have protoc installed 47 | // unless they're updating the protos. 48 | if !run_protoc { 49 | config.skip_protoc_run(); 50 | } 51 | config 52 | } 53 | if let Err(e) = 54 | prost_config(&descriptor_path, run_protoc).compile_protos(&proto_files, &[root.as_path()]) 55 | { 56 | if e.kind() == std::io::ErrorKind::NotFound && e.to_string().contains("protoc") { 57 | eprintln!("protoc not found, skipping protobuf compilation"); 58 | prost_config(&descriptor_path, false) 59 | .compile_protos(&proto_files, &[root.as_path()]) 60 | .expect("Failed to compile protos"); 61 | } else { 62 | panic!("Failed to compile protos: {e:?}"); 63 | } 64 | } 65 | 66 | #[cfg(feature = "serde")] 67 | { 68 | let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set"); 69 | pbjson_build::Builder::new() 70 | .register_descriptors(&descriptor_set) 71 | .expect("Failed to register descriptors") 72 | .preserve_proto_field_names() 73 | .build(&[".objdiff"]) 74 | .expect("Failed to build pbjson"); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /objdiff-core/config-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "id": "functionRelocDiffs", 5 | "type": "choice", 6 | "default": "name_address", 7 | "name": "Function relocation diffs", 8 | "description": "How relocation targets will be diffed in the function view.", 9 | "items": [ 10 | { 11 | "value": "none", 12 | "name": "None" 13 | }, 14 | { 15 | "value": "name_address", 16 | "name": "Name or address" 17 | }, 18 | { 19 | "value": "data_value", 20 | "name": "Data value" 21 | }, 22 | { 23 | "value": "all", 24 | "name": "Name or address, data value" 25 | } 26 | ] 27 | }, 28 | { 29 | "id": "spaceBetweenArgs", 30 | "type": "boolean", 31 | "default": true, 32 | "name": "Space between args", 33 | "description": "Adds a space between arguments in the diff output." 34 | }, 35 | { 36 | "id": "combineDataSections", 37 | "type": "boolean", 38 | "default": false, 39 | "name": "Combine data sections", 40 | "description": "Combines data sections with equal names." 41 | }, 42 | { 43 | "id": "combineTextSections", 44 | "type": "boolean", 45 | "default": false, 46 | "name": "Combine text sections", 47 | "description": "Combines all text sections into one." 48 | }, 49 | { 50 | "id": "arm.archVersion", 51 | "type": "choice", 52 | "default": "auto", 53 | "name": "Architecture version", 54 | "description": "ARM architecture version to use for disassembly.", 55 | "items": [ 56 | { 57 | "value": "auto", 58 | "name": "Auto" 59 | }, 60 | { 61 | "value": "v4t", 62 | "name": "ARMv4T (GBA)" 63 | }, 64 | { 65 | "value": "v5te", 66 | "name": "ARMv5TE (DS)" 67 | }, 68 | { 69 | "value": "v6k", 70 | "name": "ARMv6K (3DS)" 71 | } 72 | ] 73 | }, 74 | { 75 | "id": "arm.unifiedSyntax", 76 | "type": "boolean", 77 | "default": false, 78 | "name": "Unified syntax", 79 | "description": "Disassemble as unified assembly language (UAL)." 80 | }, 81 | { 82 | "id": "arm.avRegisters", 83 | "type": "boolean", 84 | "default": false, 85 | "name": "Use A/V registers", 86 | "description": "Display R0-R3 as A1-A4 and R4-R11 as V1-V8." 87 | }, 88 | { 89 | "id": "arm.r9Usage", 90 | "type": "choice", 91 | "default": "generalPurpose", 92 | "name": "Display R9 as", 93 | "items": [ 94 | { 95 | "value": "generalPurpose", 96 | "name": "R9 or V6", 97 | "description": "Use R9 as a general-purpose register." 98 | }, 99 | { 100 | "value": "sb", 101 | "name": "SB (static base)", 102 | "description": "Used for position-independent data (PID)." 103 | }, 104 | { 105 | "value": "tr", 106 | "name": "TR (TLS register)", 107 | "description": "Used for thread-local storage." 108 | } 109 | ] 110 | }, 111 | { 112 | "id": "arm.slUsage", 113 | "type": "boolean", 114 | "default": false, 115 | "name": "Display R10 as SL", 116 | "description": "Used for explicit stack limits." 117 | }, 118 | { 119 | "id": "arm.fpUsage", 120 | "type": "boolean", 121 | "default": false, 122 | "name": "Display R11 as FP", 123 | "description": "Used for frame pointers." 124 | }, 125 | { 126 | "id": "arm.ipUsage", 127 | "type": "boolean", 128 | "default": false, 129 | "name": "Display R12 as IP", 130 | "description": "Used for interworking and long branches." 131 | }, 132 | { 133 | "id": "mips.abi", 134 | "type": "choice", 135 | "default": "auto", 136 | "name": "ABI", 137 | "description": "MIPS ABI to use for disassembly.", 138 | "items": [ 139 | { 140 | "value": "auto", 141 | "name": "Auto" 142 | }, 143 | { 144 | "value": "o32", 145 | "name": "O32" 146 | }, 147 | { 148 | "value": "n32", 149 | "name": "N32" 150 | }, 151 | { 152 | "value": "n64", 153 | "name": "N64" 154 | } 155 | ] 156 | }, 157 | { 158 | "id": "mips.instrCategory", 159 | "type": "choice", 160 | "default": "auto", 161 | "name": "Instruction category", 162 | "description": "MIPS instruction category to use for disassembly.", 163 | "items": [ 164 | { 165 | "value": "auto", 166 | "name": "Auto" 167 | }, 168 | { 169 | "value": "cpu", 170 | "name": "CPU" 171 | }, 172 | { 173 | "value": "rsp", 174 | "name": "RSP (N64)" 175 | }, 176 | { 177 | "value": "r3000gte", 178 | "name": "R3000 GTE (PS1)" 179 | }, 180 | { 181 | "value": "r4000allegrex", 182 | "name": "R4000 ALLEGREX (PSP)" 183 | }, 184 | { 185 | "value": "r5900", 186 | "name": "R5900 EE (PS2)" 187 | } 188 | ] 189 | }, 190 | { 191 | "id": "mips.registerPrefix", 192 | "type": "boolean", 193 | "default": false, 194 | "name": "Register '$' prefix", 195 | "description": "Display MIPS register names with a '$' prefix." 196 | }, 197 | { 198 | "id": "ppc.calculatePoolRelocations", 199 | "type": "boolean", 200 | "default": true, 201 | "name": "Calculate pooled data references", 202 | "description": "Display pooled data references in functions as fake relocations." 203 | }, 204 | { 205 | "id": "x86.formatter", 206 | "type": "choice", 207 | "default": "intel", 208 | "name": "Format", 209 | "description": "x86 disassembly syntax.", 210 | "items": [ 211 | { 212 | "value": "intel", 213 | "name": "Intel" 214 | }, 215 | { 216 | "value": "gas", 217 | "name": "AT&T" 218 | }, 219 | { 220 | "value": "nasm", 221 | "name": "NASM" 222 | }, 223 | { 224 | "value": "masm", 225 | "name": "MASM" 226 | } 227 | ] 228 | } 229 | ], 230 | "groups": [ 231 | { 232 | "id": "general", 233 | "name": "General", 234 | "properties": [ 235 | "functionRelocDiffs", 236 | "spaceBetweenArgs", 237 | "combineDataSections", 238 | "combineTextSections" 239 | ] 240 | }, 241 | { 242 | "id": "arm", 243 | "name": "ARM", 244 | "properties": [ 245 | "arm.archVersion", 246 | "arm.unifiedSyntax", 247 | "arm.avRegisters", 248 | "arm.r9Usage", 249 | "arm.slUsage", 250 | "arm.fpUsage", 251 | "arm.ipUsage" 252 | ] 253 | }, 254 | { 255 | "id": "mips", 256 | "name": "MIPS", 257 | "properties": [ 258 | "mips.abi", 259 | "mips.instrCategory", 260 | "mips.registerPrefix" 261 | ] 262 | }, 263 | { 264 | "id": "ppc", 265 | "name": "PowerPC", 266 | "properties": [ 267 | "ppc.calculatePoolRelocations" 268 | ] 269 | }, 270 | { 271 | "id": "x86", 272 | "name": "x86", 273 | "properties": [ 274 | "x86.formatter" 275 | ] 276 | } 277 | ] 278 | } 279 | -------------------------------------------------------------------------------- /objdiff-core/protos/changes.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "report.proto"; 4 | 5 | package objdiff.report; 6 | 7 | // A pair of reports to compare and generate changes 8 | message ChangesInput { 9 | // The previous report 10 | Report from = 1; 11 | // The current report 12 | Report to = 2; 13 | } 14 | 15 | // Changes between two reports 16 | message Changes { 17 | // The progress info for the previous report 18 | Measures from = 1; 19 | // The progress info for the current report 20 | Measures to = 2; 21 | // Units that changed 22 | repeated ChangeUnit units = 3; 23 | } 24 | 25 | // A changed unit 26 | message ChangeUnit { 27 | // The name of the unit 28 | string name = 1; 29 | // The previous progress info (omitted if new) 30 | optional Measures from = 2; 31 | // The current progress info (omitted if removed) 32 | optional Measures to = 3; 33 | // Sections that changed 34 | repeated ChangeItem sections = 4; 35 | // Functions that changed 36 | repeated ChangeItem functions = 5; 37 | // Extra metadata for this unit 38 | optional ReportUnitMetadata metadata = 6; 39 | } 40 | 41 | // A changed section or function 42 | message ChangeItem { 43 | // The name of the item 44 | string name = 1; 45 | // The previous progress info (omitted if new) 46 | optional ChangeItemInfo from = 2; 47 | // The current progress info (omitted if removed) 48 | optional ChangeItemInfo to = 3; 49 | // Extra metadata for this item 50 | optional ReportItemMetadata metadata = 4; 51 | } 52 | 53 | // Progress info for a section or function 54 | message ChangeItemInfo { 55 | // The overall match percent for this item 56 | float fuzzy_match_percent = 1; 57 | // The size of the item in bytes 58 | uint64 size = 2; 59 | } 60 | -------------------------------------------------------------------------------- /objdiff-core/protos/diff.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package objdiff.diff; 4 | 5 | // A symbol 6 | message Symbol { 7 | // Name of the symbol 8 | string name = 1; 9 | // Demangled name of the symbol 10 | optional string demangled_name = 2; 11 | // Symbol address 12 | uint64 address = 3; 13 | // Symbol size 14 | uint64 size = 4; 15 | // Bitmask of SymbolFlag 16 | uint32 flags = 5; 17 | } 18 | 19 | // Symbol visibility flags 20 | enum SymbolFlag { 21 | SYMBOL_NONE = 0; 22 | SYMBOL_GLOBAL = 1; 23 | SYMBOL_LOCAL = 2; 24 | SYMBOL_WEAK = 4; 25 | SYMBOL_COMMON = 8; 26 | SYMBOL_HIDDEN = 16; 27 | } 28 | 29 | // A single parsed instruction 30 | message Instruction { 31 | // Instruction address 32 | uint64 address = 1; 33 | // Instruction size 34 | uint32 size = 2; 35 | // Instruction opcode 36 | uint32 opcode = 3; 37 | // Instruction mnemonic 38 | string mnemonic = 4; 39 | // Instruction formatted string 40 | string formatted = 5; 41 | // Original (unsimplified) instruction string 42 | optional string original = 6; 43 | // Instruction arguments 44 | repeated Argument arguments = 7; 45 | // Instruction relocation 46 | optional Relocation relocation = 8; 47 | // Instruction branch destination 48 | optional uint64 branch_dest = 9; 49 | // Instruction line number 50 | optional uint32 line_number = 10; 51 | } 52 | 53 | // An instruction argument 54 | message Argument { 55 | oneof value { 56 | // Plain text 57 | string plain_text = 1; 58 | // Value 59 | ArgumentValue argument = 2; 60 | // Relocation 61 | ArgumentRelocation relocation = 3; 62 | // Branch destination 63 | uint64 branch_dest = 4; 64 | } 65 | } 66 | 67 | // An instruction argument value 68 | message ArgumentValue { 69 | oneof value { 70 | // Signed integer 71 | int64 signed = 1; 72 | // Unsigned integer 73 | uint64 unsigned = 2; 74 | // Opaque value 75 | string opaque = 3; 76 | } 77 | } 78 | 79 | // Marker type for relocation arguments 80 | message ArgumentRelocation { 81 | } 82 | 83 | message Relocation { 84 | uint32 type = 1; 85 | string type_name = 2; 86 | RelocationTarget target = 3; 87 | } 88 | 89 | message RelocationTarget { 90 | uint32 symbol_index = 1; 91 | int64 addend = 2; 92 | } 93 | 94 | message InstructionDiffRow { 95 | DiffKind diff_kind = 1; 96 | optional Instruction instruction = 2; 97 | optional InstructionBranchFrom branch_from = 3; 98 | optional InstructionBranchTo branch_to = 4; 99 | repeated ArgumentDiff arg_diff = 5; 100 | } 101 | 102 | message ArgumentDiff { 103 | optional uint32 diff_index = 1; 104 | } 105 | 106 | enum DiffKind { 107 | DIFF_NONE = 0; 108 | DIFF_REPLACE = 1; 109 | DIFF_DELETE = 2; 110 | DIFF_INSERT = 3; 111 | DIFF_OP_MISMATCH = 4; 112 | DIFF_ARG_MISMATCH = 5; 113 | } 114 | 115 | message InstructionBranchFrom { 116 | repeated uint32 instruction_index = 1; 117 | uint32 branch_index = 2; 118 | } 119 | 120 | message InstructionBranchTo { 121 | uint32 instruction_index = 1; 122 | uint32 branch_index = 2; 123 | } 124 | 125 | message SymbolDiff { 126 | Symbol symbol = 1; 127 | repeated InstructionDiffRow instruction_rows = 2; 128 | optional float match_percent = 3; 129 | // The symbol index in the _other_ object that this symbol was diffed against 130 | optional uint32 target_symbol = 5; 131 | } 132 | 133 | message DataDiff { 134 | DiffKind kind = 1; 135 | bytes data = 2; 136 | // May be larger than data 137 | uint64 size = 3; 138 | } 139 | 140 | message SectionDiff { 141 | string name = 1; 142 | SectionKind kind = 2; 143 | uint64 size = 3; 144 | uint64 address = 4; 145 | reserved 5; 146 | repeated DataDiff data = 6; 147 | optional float match_percent = 7; 148 | } 149 | 150 | enum SectionKind { 151 | SECTION_UNKNOWN = 0; 152 | SECTION_TEXT = 1; 153 | SECTION_DATA = 2; 154 | SECTION_BSS = 3; 155 | } 156 | 157 | message ObjectDiff { 158 | repeated SectionDiff sections = 1; 159 | repeated SymbolDiff symbols = 2; 160 | } 161 | 162 | message DiffResult { 163 | optional ObjectDiff left = 1; 164 | optional ObjectDiff right = 2; 165 | } 166 | -------------------------------------------------------------------------------- /objdiff-core/protos/proto_descriptor.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/protos/proto_descriptor.bin -------------------------------------------------------------------------------- /objdiff-core/protos/report.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package objdiff.report; 4 | 5 | // Project progress report 6 | message Report { 7 | // Overall progress info 8 | Measures measures = 1; 9 | // Units within this report 10 | repeated ReportUnit units = 2; 11 | // Report version 12 | uint32 version = 3; 13 | // Progress categories 14 | repeated ReportCategory categories = 4; 15 | } 16 | 17 | // Progress info for a report or unit 18 | message Measures { 19 | // Overall match percent, including partially matched functions and data 20 | float fuzzy_match_percent = 1; 21 | // Total size of code in bytes 22 | uint64 total_code = 2; 23 | // Fully matched code size in bytes 24 | uint64 matched_code = 3; 25 | // Fully matched code percent 26 | float matched_code_percent = 4; 27 | // Total size of data in bytes 28 | uint64 total_data = 5; 29 | // Fully matched data size in bytes 30 | uint64 matched_data = 6; 31 | // Fully matched data percent 32 | float matched_data_percent = 7; 33 | // Total number of functions 34 | uint32 total_functions = 8; 35 | // Fully matched functions 36 | uint32 matched_functions = 9; 37 | // Fully matched functions percent 38 | float matched_functions_percent = 10; 39 | // Completed (or "linked") code size in bytes 40 | uint64 complete_code = 11; 41 | // Completed (or "linked") code percent 42 | float complete_code_percent = 12; 43 | // Completed (or "linked") data size in bytes 44 | uint64 complete_data = 13; 45 | // Completed (or "linked") data percent 46 | float complete_data_percent = 14; 47 | // Total number of units 48 | uint32 total_units = 15; 49 | // Completed (or "linked") units 50 | uint32 complete_units = 16; 51 | } 52 | 53 | message ReportCategory { 54 | // The ID of the category 55 | string id = 1; 56 | // The name of the category 57 | string name = 2; 58 | // Progress info for this category 59 | Measures measures = 3; 60 | } 61 | 62 | // A unit of the report (usually a translation unit) 63 | message ReportUnit { 64 | // The name of the unit 65 | string name = 1; 66 | // Progress info for this unit 67 | Measures measures = 2; 68 | // Sections within this unit 69 | repeated ReportItem sections = 3; 70 | // Functions within this unit 71 | repeated ReportItem functions = 4; 72 | // Extra metadata for this unit 73 | optional ReportUnitMetadata metadata = 5; 74 | } 75 | 76 | // Extra metadata for a unit 77 | message ReportUnitMetadata { 78 | // Whether this unit is marked as complete (or "linked") 79 | optional bool complete = 1; 80 | // The name of the module this unit belongs to 81 | optional string module_name = 2; 82 | // The ID of the module this unit belongs to 83 | optional uint32 module_id = 3; 84 | // The path to the source file of this unit 85 | optional string source_path = 4; 86 | // Progress categories for this unit 87 | repeated string progress_categories = 5; 88 | // Whether this unit is automatically generated (not user-provided) 89 | optional bool auto_generated = 6; 90 | } 91 | 92 | // A section or function within a unit 93 | message ReportItem { 94 | // The name of the item 95 | string name = 1; 96 | // The size of the item in bytes 97 | uint64 size = 2; 98 | // The overall match percent for this item 99 | float fuzzy_match_percent = 3; 100 | // Extra metadata for this item 101 | optional ReportItemMetadata metadata = 4; 102 | // Address of the item (section-relative offset) 103 | optional uint64 address = 5; 104 | } 105 | 106 | // Extra metadata for an item 107 | message ReportItemMetadata { 108 | // The demangled name of the function 109 | optional string demangled_name = 1; 110 | // The virtual address of the function or section 111 | optional uint64 virtual_address = 2; 112 | } 113 | -------------------------------------------------------------------------------- /objdiff-core/src/bindings/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "any-arch")] 2 | pub mod diff; 3 | pub mod report; 4 | -------------------------------------------------------------------------------- /objdiff-core/src/build/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod watcher; 2 | 3 | use std::process::Command; 4 | 5 | use typed_path::{Utf8PlatformPathBuf, Utf8UnixPath}; 6 | 7 | pub struct BuildStatus { 8 | pub success: bool, 9 | pub cmdline: String, 10 | pub stdout: String, 11 | pub stderr: String, 12 | } 13 | 14 | impl Default for BuildStatus { 15 | fn default() -> Self { 16 | BuildStatus { 17 | success: true, 18 | cmdline: String::new(), 19 | stdout: String::new(), 20 | stderr: String::new(), 21 | } 22 | } 23 | } 24 | 25 | #[derive(Debug, Clone)] 26 | pub struct BuildConfig { 27 | pub project_dir: Option, 28 | pub custom_make: Option, 29 | pub custom_args: Option>, 30 | #[allow(unused)] 31 | pub selected_wsl_distro: Option, 32 | } 33 | 34 | pub fn run_make(config: &BuildConfig, arg: &Utf8UnixPath) -> BuildStatus { 35 | let Some(cwd) = &config.project_dir else { 36 | return BuildStatus { 37 | success: false, 38 | stderr: "Missing project dir".to_string(), 39 | ..Default::default() 40 | }; 41 | }; 42 | let make = config.custom_make.as_deref().unwrap_or("make"); 43 | let make_args = config.custom_args.as_deref().unwrap_or(&[]); 44 | #[cfg(not(windows))] 45 | let mut command = { 46 | let mut command = Command::new(make); 47 | command.current_dir(cwd).args(make_args).arg(arg); 48 | command 49 | }; 50 | #[cfg(windows)] 51 | let mut command = { 52 | use std::os::windows::process::CommandExt; 53 | 54 | let mut command = if config.selected_wsl_distro.is_some() { 55 | Command::new("wsl") 56 | } else { 57 | Command::new(make) 58 | }; 59 | if let Some(distro) = &config.selected_wsl_distro { 60 | // Strip distro root prefix \\wsl.localhost\{distro} 61 | let wsl_path_prefix = format!("\\\\wsl.localhost\\{}", distro); 62 | let cwd = match cwd.strip_prefix(wsl_path_prefix) { 63 | Ok(new_cwd) => Utf8UnixPath::new("/").join(new_cwd.with_unix_encoding()), 64 | Err(_) => cwd.with_unix_encoding(), 65 | }; 66 | 67 | command 68 | .arg("--cd") 69 | .arg(cwd.as_str()) 70 | .arg("-d") 71 | .arg(distro) 72 | .arg("--") 73 | .arg(make) 74 | .args(make_args) 75 | .arg(arg.as_str()); 76 | } else { 77 | command.current_dir(cwd).args(make_args).arg(arg.as_str()); 78 | } 79 | command.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW); 80 | command 81 | }; 82 | let mut cmdline = shell_escape::escape(command.get_program().to_string_lossy()).into_owned(); 83 | for arg in command.get_args() { 84 | cmdline.push(' '); 85 | cmdline.push_str(shell_escape::escape(arg.to_string_lossy()).as_ref()); 86 | } 87 | let output = match command.output() { 88 | Ok(output) => output, 89 | Err(e) => { 90 | return BuildStatus { 91 | success: false, 92 | cmdline, 93 | stdout: Default::default(), 94 | stderr: e.to_string(), 95 | }; 96 | } 97 | }; 98 | // Try from_utf8 first to avoid copying the buffer if it's valid, then fall back to from_utf8_lossy 99 | let stdout = String::from_utf8(output.stdout) 100 | .unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()); 101 | let stderr = String::from_utf8(output.stderr) 102 | .unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned()); 103 | BuildStatus { success: output.status.success(), cmdline, stdout, stderr } 104 | } 105 | -------------------------------------------------------------------------------- /objdiff-core/src/build/watcher.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | path::{Path, PathBuf}, 4 | sync::{ 5 | Arc, 6 | atomic::{AtomicBool, Ordering}, 7 | }, 8 | task::Waker, 9 | time::Duration, 10 | }; 11 | 12 | use globset::GlobSet; 13 | use notify::RecursiveMode; 14 | use notify_debouncer_full::{DebounceEventResult, new_debouncer_opt}; 15 | 16 | pub type Watcher = notify_debouncer_full::Debouncer< 17 | notify::RecommendedWatcher, 18 | notify_debouncer_full::RecommendedCache, 19 | >; 20 | 21 | pub struct WatcherState { 22 | pub config_path: Option, 23 | pub left_obj_path: Option, 24 | pub right_obj_path: Option, 25 | pub patterns: GlobSet, 26 | } 27 | 28 | pub fn create_watcher( 29 | modified: Arc, 30 | project_dir: &Path, 31 | patterns: GlobSet, 32 | waker: Waker, 33 | ) -> notify::Result { 34 | let base_dir = fs::canonicalize(project_dir)?; 35 | let base_dir_clone = base_dir.clone(); 36 | let timeout = Duration::from_millis(200); 37 | let config = notify::Config::default().with_poll_interval(Duration::from_secs(2)); 38 | let mut debouncer = new_debouncer_opt( 39 | timeout, 40 | None, 41 | move |result: DebounceEventResult| match result { 42 | Ok(events) => { 43 | let mut any_match = false; 44 | for event in events.iter() { 45 | if !matches!( 46 | event.kind, 47 | notify::EventKind::Modify(..) 48 | | notify::EventKind::Create(..) 49 | | notify::EventKind::Remove(..) 50 | ) { 51 | continue; 52 | } 53 | for path in &event.paths { 54 | let Ok(path) = path.strip_prefix(&base_dir_clone) else { 55 | continue; 56 | }; 57 | if patterns.is_match(path) { 58 | // log::info!("File modified: {}", path.display()); 59 | any_match = true; 60 | } 61 | } 62 | } 63 | if any_match { 64 | modified.store(true, Ordering::Relaxed); 65 | waker.wake_by_ref(); 66 | } 67 | } 68 | Err(errors) => errors.iter().for_each(|e| log::error!("Watch error: {e:?}")), 69 | }, 70 | notify_debouncer_full::RecommendedCache::new(), 71 | config, 72 | )?; 73 | debouncer.watch(base_dir, RecursiveMode::Recursive)?; 74 | Ok(debouncer) 75 | } 76 | -------------------------------------------------------------------------------- /objdiff-core/src/config/path.rs: -------------------------------------------------------------------------------- 1 | // For argp::FromArgs 2 | #[cfg(feature = "std")] 3 | pub fn platform_path(value: &str) -> Result { 4 | Ok(typed_path::Utf8PlatformPathBuf::from(value)) 5 | } 6 | 7 | /// Checks if the path is valid UTF-8 and returns it as a [`Utf8PlatformPath`]. 8 | #[cfg(feature = "std")] 9 | pub fn check_path( 10 | path: &std::path::Path, 11 | ) -> Result<&typed_path::Utf8PlatformPath, core::str::Utf8Error> { 12 | typed_path::Utf8PlatformPath::from_bytes_path(typed_path::PlatformPath::new( 13 | path.as_os_str().as_encoded_bytes(), 14 | )) 15 | } 16 | 17 | /// Checks if the path is valid UTF-8 and returns it as a [`Utf8NativePathBuf`]. 18 | #[cfg(feature = "std")] 19 | pub fn check_path_buf( 20 | path: std::path::PathBuf, 21 | ) -> Result { 22 | typed_path::Utf8PlatformPathBuf::from_bytes_path_buf(typed_path::PlatformPathBuf::from( 23 | path.into_os_string().into_encoded_bytes(), 24 | )) 25 | } 26 | 27 | #[cfg(feature = "serde")] 28 | pub mod unix_path_serde_option { 29 | use serde::{Deserialize, Deserializer, Serializer}; 30 | use typed_path::Utf8UnixPathBuf; 31 | 32 | pub fn serialize(path: &Option, s: S) -> Result 33 | where S: Serializer { 34 | if let Some(path) = path { s.serialize_some(path.as_str()) } else { s.serialize_none() } 35 | } 36 | 37 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 38 | where D: Deserializer<'de> { 39 | Ok(Option::::deserialize(deserializer)?.map(Utf8UnixPathBuf::from)) 40 | } 41 | } 42 | 43 | #[cfg(all(feature = "serde", feature = "std"))] 44 | pub mod platform_path_serde_option { 45 | use serde::{Deserialize, Deserializer, Serializer}; 46 | use typed_path::Utf8PlatformPathBuf; 47 | 48 | pub fn serialize(path: &Option, s: S) -> Result 49 | where S: Serializer { 50 | if let Some(path) = path { s.serialize_some(path.as_str()) } else { s.serialize_none() } 51 | } 52 | 53 | pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> 54 | where D: Deserializer<'de> { 55 | Ok(Option::::deserialize(deserializer)?.map(Utf8PlatformPathBuf::from)) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /objdiff-core/src/jobs/check_update.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::mpsc::Receiver, task::Waker}; 2 | 3 | use anyhow::{Context, Result}; 4 | use self_update::{ 5 | cargo_crate_version, 6 | update::{Release, ReleaseUpdate}, 7 | }; 8 | 9 | use crate::jobs::{Job, JobContext, JobResult, JobState, start_job, update_status}; 10 | 11 | pub struct CheckUpdateConfig { 12 | pub build_updater: fn() -> Result>, 13 | pub bin_names: Vec, 14 | } 15 | 16 | pub struct CheckUpdateResult { 17 | pub update_available: bool, 18 | pub latest_release: Release, 19 | pub found_binary: Option, 20 | } 21 | 22 | fn run_check_update( 23 | context: &JobContext, 24 | cancel: Receiver<()>, 25 | config: CheckUpdateConfig, 26 | ) -> Result> { 27 | update_status(context, "Fetching latest release".to_string(), 0, 1, &cancel)?; 28 | let updater = (config.build_updater)().context("Failed to create release updater")?; 29 | let latest_release = updater.get_latest_release()?; 30 | let update_available = 31 | self_update::version::bump_is_greater(cargo_crate_version!(), &latest_release.version)?; 32 | // Find the binary name in the release assets 33 | let mut found_binary = None; 34 | for bin_name in &config.bin_names { 35 | if latest_release.assets.iter().any(|a| &a.name == bin_name) { 36 | found_binary = Some(bin_name.clone()); 37 | break; 38 | } 39 | } 40 | 41 | update_status(context, "Complete".to_string(), 1, 1, &cancel)?; 42 | Ok(Box::new(CheckUpdateResult { update_available, latest_release, found_binary })) 43 | } 44 | 45 | pub fn start_check_update(waker: Waker, config: CheckUpdateConfig) -> JobState { 46 | start_job(waker, "Check for updates", Job::CheckUpdate, move |context, cancel| { 47 | run_check_update(&context, cancel, config) 48 | .map(|result| JobResult::CheckUpdate(Some(result))) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /objdiff-core/src/jobs/create_scratch.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, sync::mpsc::Receiver, task::Waker}; 2 | 3 | use anyhow::{Context, Result, anyhow, bail}; 4 | use typed_path::{Utf8PlatformPathBuf, Utf8UnixPathBuf}; 5 | 6 | use crate::{ 7 | build::{BuildConfig, BuildStatus, run_make}, 8 | jobs::{Job, JobContext, JobResult, JobState, start_job, update_status}, 9 | }; 10 | 11 | #[derive(Debug, Clone)] 12 | pub struct CreateScratchConfig { 13 | pub build_config: BuildConfig, 14 | pub context_path: Option, 15 | pub build_context: bool, 16 | 17 | // Scratch fields 18 | pub compiler: String, 19 | pub platform: String, 20 | pub compiler_flags: String, 21 | pub function_name: String, 22 | pub target_obj: Utf8PlatformPathBuf, 23 | pub preset_id: Option, 24 | } 25 | 26 | #[derive(Default, Debug, Clone)] 27 | pub struct CreateScratchResult { 28 | pub scratch_url: String, 29 | } 30 | 31 | #[derive(Debug, Default, Clone, serde::Deserialize)] 32 | struct CreateScratchResponse { 33 | pub slug: String, 34 | pub claim_token: String, 35 | } 36 | 37 | const API_HOST: &str = "https://decomp.me"; 38 | 39 | fn run_create_scratch( 40 | status: &JobContext, 41 | cancel: Receiver<()>, 42 | config: CreateScratchConfig, 43 | ) -> Result> { 44 | let project_dir = 45 | config.build_config.project_dir.as_ref().ok_or_else(|| anyhow!("Missing project dir"))?; 46 | 47 | let mut context = None; 48 | if let Some(context_path) = &config.context_path { 49 | if config.build_context { 50 | update_status(status, "Building context".to_string(), 0, 2, &cancel)?; 51 | match run_make(&config.build_config, context_path.as_ref()) { 52 | BuildStatus { success: true, .. } => {} 53 | BuildStatus { success: false, stdout, stderr, .. } => { 54 | bail!("Failed to build context:\n{stdout}\n{stderr}") 55 | } 56 | } 57 | } 58 | let context_path = project_dir.join(context_path.with_platform_encoding()); 59 | context = Some( 60 | fs::read_to_string(&context_path) 61 | .map_err(|e| anyhow!("Failed to read {}: {}", context_path, e))?, 62 | ); 63 | } 64 | 65 | update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?; 66 | let diff_flags = [format!("--disassemble={}", config.function_name)]; 67 | let diff_flags = serde_json::to_string(&diff_flags)?; 68 | let file = reqwest::blocking::multipart::Part::file(&config.target_obj) 69 | .with_context(|| format!("Failed to open {}", config.target_obj))?; 70 | let mut form = reqwest::blocking::multipart::Form::new() 71 | .text("compiler", config.compiler.clone()) 72 | .text("platform", config.platform.clone()) 73 | .text("compiler_flags", config.compiler_flags.clone()) 74 | .text("diff_label", config.function_name.clone()) 75 | .text("diff_flags", diff_flags) 76 | .text("context", context.unwrap_or_default()) 77 | .text("source_code", "// Move related code from Context tab to here"); 78 | if let Some(preset) = config.preset_id { 79 | form = form.text("preset", preset.to_string()); 80 | } 81 | form = form.part("target_obj", file); 82 | let client = reqwest::blocking::Client::new(); 83 | let response = client 84 | .post(format!("{API_HOST}/api/scratch")) 85 | .multipart(form) 86 | .send() 87 | .map_err(|e| anyhow!("Failed to send request: {}", e))?; 88 | if !response.status().is_success() { 89 | return Err(anyhow!("Failed to create scratch: {}", response.text()?)); 90 | } 91 | let body: CreateScratchResponse = response.json().context("Failed to parse response")?; 92 | let scratch_url = format!("{API_HOST}/scratch/{}/claim?token={}", body.slug, body.claim_token); 93 | 94 | update_status(status, "Complete".to_string(), 2, 2, &cancel)?; 95 | Ok(Box::from(CreateScratchResult { scratch_url })) 96 | } 97 | 98 | pub fn start_create_scratch(waker: Waker, config: CreateScratchConfig) -> JobState { 99 | start_job(waker, "Create scratch", Job::CreateScratch, move |context, cancel| { 100 | run_create_scratch(&context, cancel, config) 101 | .map(|result| JobResult::CreateScratch(Some(result))) 102 | }) 103 | } 104 | -------------------------------------------------------------------------------- /objdiff-core/src/jobs/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::{ 3 | Arc, RwLock, 4 | atomic::{AtomicUsize, Ordering}, 5 | mpsc::{Receiver, Sender, TryRecvError}, 6 | }, 7 | task::Waker, 8 | thread::JoinHandle, 9 | }; 10 | 11 | use anyhow::Result; 12 | 13 | use crate::jobs::{ 14 | check_update::CheckUpdateResult, create_scratch::CreateScratchResult, objdiff::ObjDiffResult, 15 | update::UpdateResult, 16 | }; 17 | 18 | pub mod check_update; 19 | pub mod create_scratch; 20 | pub mod objdiff; 21 | pub mod update; 22 | 23 | #[derive(Debug, Eq, PartialEq, Copy, Clone)] 24 | pub enum Job { 25 | ObjDiff, 26 | CheckUpdate, 27 | Update, 28 | CreateScratch, 29 | } 30 | pub static JOB_ID: AtomicUsize = AtomicUsize::new(0); 31 | 32 | #[derive(Default)] 33 | pub struct JobQueue { 34 | pub jobs: Vec, 35 | pub results: Vec, 36 | } 37 | 38 | impl JobQueue { 39 | /// Adds a job to the queue. 40 | #[inline] 41 | pub fn push(&mut self, state: JobState) { self.jobs.push(state); } 42 | 43 | /// Adds a job to the queue if a job of the given kind is not already running. 44 | #[inline] 45 | pub fn push_once(&mut self, job: Job, func: impl FnOnce() -> JobState) { 46 | if !self.is_running(job) { 47 | self.push(func()); 48 | } 49 | } 50 | 51 | /// Returns whether a job of the given kind is running. 52 | pub fn is_running(&self, kind: Job) -> bool { 53 | self.jobs.iter().any(|j| j.kind == kind && j.handle.is_some()) 54 | } 55 | 56 | /// Returns whether any job is running. 57 | pub fn any_running(&self) -> bool { 58 | self.jobs.iter().any(|job| { 59 | if let Some(handle) = &job.handle { 60 | return !handle.is_finished(); 61 | } 62 | false 63 | }) 64 | } 65 | 66 | /// Iterates over all jobs mutably. 67 | pub fn iter_mut(&mut self) -> impl Iterator + '_ { self.jobs.iter_mut() } 68 | 69 | /// Iterates over all finished jobs, returning the job state and the result. 70 | pub fn iter_finished( 71 | &mut self, 72 | ) -> impl Iterator)> + '_ { 73 | self.jobs.iter_mut().filter_map(|job| { 74 | if let Some(handle) = &job.handle { 75 | if !handle.is_finished() { 76 | return None; 77 | } 78 | let result = job.handle.take().unwrap().join(); 79 | return Some((job, result)); 80 | } 81 | None 82 | }) 83 | } 84 | 85 | /// Clears all finished jobs. 86 | pub fn clear_finished(&mut self) { 87 | self.jobs.retain(|job| { 88 | !(job.handle.is_none() && job.context.status.read().unwrap().error.is_none()) 89 | }); 90 | } 91 | 92 | /// Clears all errored jobs. 93 | pub fn clear_errored(&mut self) { 94 | self.jobs.retain(|job| job.context.status.read().unwrap().error.is_none()); 95 | } 96 | 97 | /// Removes a job from the queue given its ID. 98 | pub fn remove(&mut self, id: usize) { self.jobs.retain(|job| job.id != id); } 99 | 100 | /// Collects the results of all finished jobs and handles any errors. 101 | pub fn collect_results(&mut self) { 102 | let mut results = vec![]; 103 | for (job, result) in self.iter_finished() { 104 | match result { 105 | Ok(result) => { 106 | match result { 107 | JobResult::None => { 108 | // Job context contains the error 109 | } 110 | _ => results.push(result), 111 | } 112 | } 113 | Err(err) => { 114 | let err = if let Some(msg) = err.downcast_ref::<&'static str>() { 115 | anyhow::Error::msg(*msg) 116 | } else if let Some(msg) = err.downcast_ref::() { 117 | anyhow::Error::msg(msg.clone()) 118 | } else { 119 | anyhow::Error::msg("Thread panicked") 120 | }; 121 | let result = job.context.status.write(); 122 | if let Ok(mut guard) = result { 123 | guard.error = Some(err); 124 | } else { 125 | drop(result); 126 | job.context.status = Arc::new(RwLock::new(JobStatus { 127 | title: "Error".to_string(), 128 | progress_percent: 0.0, 129 | progress_items: None, 130 | status: String::new(), 131 | error: Some(err), 132 | })); 133 | } 134 | } 135 | } 136 | } 137 | self.results.append(&mut results); 138 | self.clear_finished(); 139 | } 140 | } 141 | 142 | #[derive(Clone)] 143 | pub struct JobContext { 144 | pub status: Arc>, 145 | pub waker: Waker, 146 | } 147 | 148 | pub struct JobState { 149 | pub id: usize, 150 | pub kind: Job, 151 | pub handle: Option>, 152 | pub context: JobContext, 153 | pub cancel: Sender<()>, 154 | } 155 | 156 | #[derive(Default)] 157 | pub struct JobStatus { 158 | pub title: String, 159 | pub progress_percent: f32, 160 | pub progress_items: Option<[u32; 2]>, 161 | pub status: String, 162 | pub error: Option, 163 | } 164 | 165 | pub enum JobResult { 166 | None, 167 | ObjDiff(Option>), 168 | CheckUpdate(Option>), 169 | Update(Box), 170 | CreateScratch(Option>), 171 | } 172 | 173 | fn should_cancel(rx: &Receiver<()>) -> bool { 174 | match rx.try_recv() { 175 | Ok(_) | Err(TryRecvError::Disconnected) => true, 176 | Err(_) => false, 177 | } 178 | } 179 | 180 | fn start_job( 181 | waker: Waker, 182 | title: &str, 183 | kind: Job, 184 | run: impl FnOnce(JobContext, Receiver<()>) -> Result + Send + 'static, 185 | ) -> JobState { 186 | let status = Arc::new(RwLock::new(JobStatus { 187 | title: title.to_string(), 188 | progress_percent: 0.0, 189 | progress_items: None, 190 | status: String::new(), 191 | error: None, 192 | })); 193 | let context = JobContext { status: status.clone(), waker: waker.clone() }; 194 | let context_inner = JobContext { status: status.clone(), waker }; 195 | let (tx, rx) = std::sync::mpsc::channel(); 196 | let handle = std::thread::spawn(move || match run(context_inner, rx) { 197 | Ok(state) => state, 198 | Err(e) => { 199 | if let Ok(mut w) = status.write() { 200 | w.error = Some(e); 201 | } 202 | JobResult::None 203 | } 204 | }); 205 | let id = JOB_ID.fetch_add(1, Ordering::Relaxed); 206 | // log::info!("Started job {}", id); TODO 207 | JobState { id, kind, handle: Some(handle), context, cancel: tx } 208 | } 209 | 210 | fn update_status( 211 | context: &JobContext, 212 | str: String, 213 | count: u32, 214 | total: u32, 215 | cancel: &Receiver<()>, 216 | ) -> Result<()> { 217 | let mut w = 218 | context.status.write().map_err(|_| anyhow::Error::msg("Failed to lock job status"))?; 219 | w.progress_items = Some([count, total]); 220 | w.progress_percent = count as f32 / total as f32; 221 | if should_cancel(cancel) { 222 | w.status = "Cancelled".to_string(); 223 | return Err(anyhow::Error::msg("Cancelled")); 224 | } else { 225 | w.status = str; 226 | } 227 | drop(w); 228 | context.waker.wake_by_ref(); 229 | Ok(()) 230 | } 231 | -------------------------------------------------------------------------------- /objdiff-core/src/jobs/objdiff.rs: -------------------------------------------------------------------------------- 1 | use std::{sync::mpsc::Receiver, task::Waker}; 2 | 3 | use anyhow::{Error, Result, bail}; 4 | use time::OffsetDateTime; 5 | use typed_path::Utf8PlatformPathBuf; 6 | 7 | use crate::{ 8 | build::{BuildConfig, BuildStatus, run_make}, 9 | diff::{DiffObjConfig, MappingConfig, ObjectDiff, diff_objs}, 10 | jobs::{Job, JobContext, JobResult, JobState, start_job, update_status}, 11 | obj::{Object, read}, 12 | }; 13 | 14 | pub struct ObjDiffConfig { 15 | pub build_config: BuildConfig, 16 | pub build_base: bool, 17 | pub build_target: bool, 18 | pub target_path: Option, 19 | pub base_path: Option, 20 | pub diff_obj_config: DiffObjConfig, 21 | pub mapping_config: MappingConfig, 22 | } 23 | 24 | pub struct ObjDiffResult { 25 | pub first_status: BuildStatus, 26 | pub second_status: BuildStatus, 27 | pub first_obj: Option<(Object, ObjectDiff)>, 28 | pub second_obj: Option<(Object, ObjectDiff)>, 29 | pub time: OffsetDateTime, 30 | } 31 | 32 | fn run_build( 33 | context: &JobContext, 34 | cancel: Receiver<()>, 35 | config: ObjDiffConfig, 36 | ) -> Result> { 37 | let mut target_path_rel = None; 38 | let mut base_path_rel = None; 39 | if config.build_target || config.build_base { 40 | let project_dir = config 41 | .build_config 42 | .project_dir 43 | .as_ref() 44 | .ok_or_else(|| Error::msg("Missing project dir"))?; 45 | if let Some(target_path) = &config.target_path { 46 | target_path_rel = match target_path.strip_prefix(project_dir) { 47 | Ok(p) => Some(p.with_unix_encoding()), 48 | Err(_) => { 49 | bail!("Target path '{}' doesn't begin with '{}'", target_path, project_dir); 50 | } 51 | }; 52 | } 53 | if let Some(base_path) = &config.base_path { 54 | base_path_rel = match base_path.strip_prefix(project_dir) { 55 | Ok(p) => Some(p.with_unix_encoding()), 56 | Err(_) => { 57 | bail!("Base path '{}' doesn't begin with '{}'", base_path, project_dir); 58 | } 59 | }; 60 | }; 61 | } 62 | 63 | let mut total = 1; 64 | if config.build_target && target_path_rel.is_some() { 65 | total += 1; 66 | } 67 | if config.build_base && base_path_rel.is_some() { 68 | total += 1; 69 | } 70 | if config.target_path.is_some() { 71 | total += 1; 72 | } 73 | if config.base_path.is_some() { 74 | total += 1; 75 | } 76 | 77 | let mut step_idx = 0; 78 | let mut first_status = match target_path_rel { 79 | Some(target_path_rel) if config.build_target => { 80 | update_status( 81 | context, 82 | format!("Building target {}", target_path_rel), 83 | step_idx, 84 | total, 85 | &cancel, 86 | )?; 87 | step_idx += 1; 88 | run_make(&config.build_config, target_path_rel.as_ref()) 89 | } 90 | _ => BuildStatus::default(), 91 | }; 92 | 93 | let mut second_status = match base_path_rel { 94 | Some(base_path_rel) if config.build_base => { 95 | update_status( 96 | context, 97 | format!("Building base {}", base_path_rel), 98 | step_idx, 99 | total, 100 | &cancel, 101 | )?; 102 | step_idx += 1; 103 | run_make(&config.build_config, base_path_rel.as_ref()) 104 | } 105 | _ => BuildStatus::default(), 106 | }; 107 | 108 | let time = OffsetDateTime::now_utc(); 109 | 110 | let first_obj = match &config.target_path { 111 | Some(target_path) if first_status.success => { 112 | update_status( 113 | context, 114 | format!("Loading target {}", target_path), 115 | step_idx, 116 | total, 117 | &cancel, 118 | )?; 119 | step_idx += 1; 120 | match read::read(target_path.as_ref(), &config.diff_obj_config) { 121 | Ok(obj) => Some(obj), 122 | Err(e) => { 123 | first_status = BuildStatus { 124 | success: false, 125 | stdout: format!("Loading object '{}'", target_path), 126 | stderr: format!("{:#}", e), 127 | ..Default::default() 128 | }; 129 | None 130 | } 131 | } 132 | } 133 | Some(_) => { 134 | step_idx += 1; 135 | None 136 | } 137 | _ => None, 138 | }; 139 | 140 | let second_obj = match &config.base_path { 141 | Some(base_path) if second_status.success => { 142 | update_status( 143 | context, 144 | format!("Loading base {}", base_path), 145 | step_idx, 146 | total, 147 | &cancel, 148 | )?; 149 | step_idx += 1; 150 | match read::read(base_path.as_ref(), &config.diff_obj_config) { 151 | Ok(obj) => Some(obj), 152 | Err(e) => { 153 | second_status = BuildStatus { 154 | success: false, 155 | stdout: format!("Loading object '{}'", base_path), 156 | stderr: format!("{:#}", e), 157 | ..Default::default() 158 | }; 159 | None 160 | } 161 | } 162 | } 163 | Some(_) => { 164 | step_idx += 1; 165 | None 166 | } 167 | _ => None, 168 | }; 169 | 170 | update_status(context, "Performing diff".to_string(), step_idx, total, &cancel)?; 171 | step_idx += 1; 172 | let result = diff_objs( 173 | first_obj.as_ref(), 174 | second_obj.as_ref(), 175 | None, 176 | &config.diff_obj_config, 177 | &config.mapping_config, 178 | )?; 179 | 180 | update_status(context, "Complete".to_string(), step_idx, total, &cancel)?; 181 | Ok(Box::new(ObjDiffResult { 182 | first_status, 183 | second_status, 184 | first_obj: first_obj.and_then(|o| result.left.map(|d| (o, d))), 185 | second_obj: second_obj.and_then(|o| result.right.map(|d| (o, d))), 186 | time, 187 | })) 188 | } 189 | 190 | pub fn start_build(waker: Waker, config: ObjDiffConfig) -> JobState { 191 | start_job(waker, "Build", Job::ObjDiff, move |context, cancel| { 192 | run_build(&context, cancel, config).map(|result| JobResult::ObjDiff(Some(result))) 193 | }) 194 | } 195 | -------------------------------------------------------------------------------- /objdiff-core/src/jobs/update.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env::{current_dir, current_exe}, 3 | fs::File, 4 | path::PathBuf, 5 | sync::mpsc::Receiver, 6 | task::Waker, 7 | }; 8 | 9 | use anyhow::{Context, Result}; 10 | pub use self_update; // Re-export self_update crate 11 | use self_update::update::ReleaseUpdate; 12 | 13 | use crate::jobs::{Job, JobContext, JobResult, JobState, start_job, update_status}; 14 | 15 | pub struct UpdateConfig { 16 | pub build_updater: fn() -> Result>, 17 | pub bin_name: String, 18 | } 19 | 20 | pub struct UpdateResult { 21 | pub exe_path: PathBuf, 22 | } 23 | 24 | fn run_update( 25 | status: &JobContext, 26 | cancel: Receiver<()>, 27 | config: UpdateConfig, 28 | ) -> Result> { 29 | update_status(status, "Fetching latest release".to_string(), 0, 3, &cancel)?; 30 | let updater = (config.build_updater)().context("Failed to create release updater")?; 31 | let latest_release = updater.get_latest_release()?; 32 | let asset = 33 | latest_release.assets.iter().find(|a| a.name == config.bin_name).ok_or_else(|| { 34 | anyhow::Error::msg(format!("No release asset for {}", config.bin_name)) 35 | })?; 36 | 37 | update_status(status, "Downloading release".to_string(), 1, 3, &cancel)?; 38 | let tmp_dir = tempfile::Builder::new().prefix("update").tempdir_in(current_dir()?)?; 39 | let tmp_path = tmp_dir.path().join(&asset.name); 40 | let tmp_file = File::create(&tmp_path)?; 41 | self_update::Download::from_url(&asset.download_url) 42 | .set_header(reqwest::header::ACCEPT, "application/octet-stream".parse()?) 43 | .download_to(tmp_file)?; 44 | 45 | update_status(status, "Extracting release".to_string(), 2, 3, &cancel)?; 46 | let tmp_file = tmp_dir.path().join("replacement_tmp"); 47 | let target_file = current_exe()?; 48 | self_update::Move::from_source(&tmp_path) 49 | .replace_using_temp(&tmp_file) 50 | .to_dest(&target_file)?; 51 | #[cfg(unix)] 52 | { 53 | use std::{fs, os::unix::fs::PermissionsExt}; 54 | fs::set_permissions(&target_file, fs::Permissions::from_mode(0o755))?; 55 | } 56 | tmp_dir.close()?; 57 | 58 | update_status(status, "Complete".to_string(), 3, 3, &cancel)?; 59 | Ok(Box::from(UpdateResult { exe_path: target_file })) 60 | } 61 | 62 | pub fn start_update(waker: Waker, config: UpdateConfig) -> JobState { 63 | start_job(waker, "Update app", Job::Update, move |context, cancel| { 64 | run_update(&context, cancel, config).map(JobResult::Update) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /objdiff-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::too_many_arguments)] 2 | #![cfg_attr(not(feature = "std"), no_std)] 3 | extern crate alloc; 4 | 5 | #[cfg(feature = "any-arch")] 6 | pub mod arch; 7 | #[cfg(feature = "bindings")] 8 | pub mod bindings; 9 | #[cfg(feature = "build")] 10 | pub mod build; 11 | #[cfg(feature = "config")] 12 | pub mod config; 13 | #[cfg(feature = "any-arch")] 14 | pub mod diff; 15 | #[cfg(feature = "build")] 16 | pub mod jobs; 17 | #[cfg(feature = "any-arch")] 18 | pub mod obj; 19 | #[cfg(feature = "any-arch")] 20 | pub mod util; 21 | -------------------------------------------------------------------------------- /objdiff-core/src/obj/snapshots/objdiff_core__obj__read__test__combine_sections.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: objdiff-core/src/obj/read.rs 3 | expression: "(sections, symbols)" 4 | --- 5 | ( 6 | [ 7 | Section { 8 | id: ".text-0", 9 | name: ".text", 10 | address: 0, 11 | size: 8, 12 | kind: Code, 13 | data: SectionData( 14 | 8, 15 | ), 16 | flags: FlagSet(), 17 | align: None, 18 | relocations: [ 19 | Relocation { 20 | flags: Elf( 21 | 0, 22 | ), 23 | address: 0, 24 | target_symbol: 0, 25 | addend: 4, 26 | }, 27 | Relocation { 28 | flags: Elf( 29 | 0, 30 | ), 31 | address: 2, 32 | target_symbol: 1, 33 | addend: 0, 34 | }, 35 | Relocation { 36 | flags: Elf( 37 | 0, 38 | ), 39 | address: 4, 40 | target_symbol: 0, 41 | addend: 10, 42 | }, 43 | ], 44 | line_info: {}, 45 | virtual_address: None, 46 | }, 47 | Section { 48 | id: ".data-combined", 49 | name: ".data", 50 | address: 0, 51 | size: 12, 52 | kind: Data, 53 | data: SectionData( 54 | 12, 55 | ), 56 | flags: FlagSet(Combined), 57 | align: None, 58 | relocations: [ 59 | Relocation { 60 | flags: Elf( 61 | 0, 62 | ), 63 | address: 0, 64 | target_symbol: 2, 65 | addend: 0, 66 | }, 67 | Relocation { 68 | flags: Elf( 69 | 0, 70 | ), 71 | address: 4, 72 | target_symbol: 2, 73 | addend: 0, 74 | }, 75 | ], 76 | line_info: { 77 | 0: 1, 78 | 8: 2, 79 | }, 80 | virtual_address: None, 81 | }, 82 | Section { 83 | id: ".data-1", 84 | name: ".data", 85 | address: 0, 86 | size: 0, 87 | kind: Unknown, 88 | data: SectionData( 89 | 0, 90 | ), 91 | flags: FlagSet(), 92 | align: None, 93 | relocations: [], 94 | line_info: {}, 95 | virtual_address: None, 96 | }, 97 | Section { 98 | id: ".data-2", 99 | name: ".data", 100 | address: 0, 101 | size: 0, 102 | kind: Unknown, 103 | data: SectionData( 104 | 0, 105 | ), 106 | flags: FlagSet(), 107 | align: None, 108 | relocations: [], 109 | line_info: {}, 110 | virtual_address: None, 111 | }, 112 | ], 113 | [ 114 | Symbol { 115 | name: ".data", 116 | demangled_name: None, 117 | address: 0, 118 | size: 0, 119 | kind: Section, 120 | section: Some( 121 | 1, 122 | ), 123 | flags: FlagSet(), 124 | align: None, 125 | virtual_address: None, 126 | }, 127 | Symbol { 128 | name: "symbol", 129 | demangled_name: None, 130 | address: 4, 131 | size: 4, 132 | kind: Object, 133 | section: Some( 134 | 1, 135 | ), 136 | flags: FlagSet(), 137 | align: None, 138 | virtual_address: None, 139 | }, 140 | Symbol { 141 | name: "function", 142 | demangled_name: None, 143 | address: 0, 144 | size: 8, 145 | kind: Function, 146 | section: Some( 147 | 0, 148 | ), 149 | flags: FlagSet(), 150 | align: None, 151 | virtual_address: None, 152 | }, 153 | Symbol { 154 | name: ".data", 155 | demangled_name: None, 156 | address: 0, 157 | size: 0, 158 | kind: Unknown, 159 | section: None, 160 | flags: FlagSet(), 161 | align: None, 162 | virtual_address: None, 163 | }, 164 | ], 165 | ) 166 | -------------------------------------------------------------------------------- /objdiff-core/src/util.rs: -------------------------------------------------------------------------------- 1 | use alloc::{format, vec::Vec}; 2 | use core::fmt; 3 | 4 | use anyhow::{Result, ensure}; 5 | use num_traits::PrimInt; 6 | use object::{Endian, Object}; 7 | 8 | // https://stackoverflow.com/questions/44711012/how-do-i-format-a-signed-integer-to-a-sign-aware-hexadecimal-representation 9 | pub struct ReallySigned(pub N); 10 | 11 | impl fmt::LowerHex for ReallySigned { 12 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 13 | let num = self.0.to_i64().unwrap(); 14 | let prefix = if f.alternate() { "0x" } else { "" }; 15 | let bare_hex = format!("{:x}", num.abs()); 16 | f.pad_integral(num >= 0, prefix, &bare_hex) 17 | } 18 | } 19 | 20 | impl fmt::UpperHex for ReallySigned { 21 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 22 | let num = self.0.to_i64().unwrap(); 23 | let prefix = if f.alternate() { "0x" } else { "" }; 24 | let bare_hex = format!("{:X}", num.abs()); 25 | f.pad_integral(num >= 0, prefix, &bare_hex) 26 | } 27 | } 28 | 29 | pub fn read_u32(obj_file: &object::File, reader: &mut &[u8]) -> Result { 30 | ensure!(reader.len() >= 4, "Not enough bytes to read u32"); 31 | let value = u32::from_ne_bytes(reader[..4].try_into()?); 32 | *reader = &reader[4..]; 33 | Ok(obj_file.endianness().read_u32(value)) 34 | } 35 | 36 | pub fn read_u16(obj_file: &object::File, reader: &mut &[u8]) -> Result { 37 | ensure!(reader.len() >= 2, "Not enough bytes to read u16"); 38 | let value = u16::from_ne_bytes(reader[..2].try_into()?); 39 | *reader = &reader[2..]; 40 | Ok(obj_file.endianness().read_u16(value)) 41 | } 42 | 43 | pub fn align_size_to_4(size: usize) -> usize { (size + 3) & !3 } 44 | 45 | #[cfg(feature = "std")] 46 | pub fn align_data_to_4( 47 | writer: &mut W, 48 | len: usize, 49 | ) -> std::io::Result<()> { 50 | const ALIGN_BYTES: &[u8] = &[0; 4]; 51 | if len % 4 != 0 { 52 | writer.write_all(&ALIGN_BYTES[..4 - len % 4])?; 53 | } 54 | Ok(()) 55 | } 56 | 57 | pub fn align_u64_to(len: u64, align: u64) -> u64 { len + ((align - (len % align)) % align) } 58 | 59 | pub fn align_data_slice_to(data: &mut Vec, align: u64) { 60 | data.resize(align_u64_to(data.len() as u64, align) as usize, 0); 61 | } 62 | -------------------------------------------------------------------------------- /objdiff-core/tests/arch_arm.rs: -------------------------------------------------------------------------------- 1 | use objdiff_core::{diff, obj}; 2 | 3 | mod common; 4 | 5 | #[test] 6 | #[cfg(feature = "arm")] 7 | fn read_arm() { 8 | let diff_config = diff::DiffObjConfig { ..Default::default() }; 9 | let obj = obj::read::parse(include_object!("data/arm/LinkStateItem.o"), &diff_config).unwrap(); 10 | insta::assert_debug_snapshot!(obj); 11 | let symbol_idx = 12 | obj.symbols.iter().position(|s| s.name == "_ZN13LinkStateItem12OnStateLeaveEi").unwrap(); 13 | let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap(); 14 | insta::assert_debug_snapshot!(diff.instruction_rows); 15 | let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config); 16 | insta::assert_snapshot!(output); 17 | } 18 | 19 | #[test] 20 | #[cfg(feature = "arm")] 21 | fn read_thumb() { 22 | let diff_config = diff::DiffObjConfig { ..Default::default() }; 23 | let obj = obj::read::parse(include_object!("data/arm/thumb.o"), &diff_config).unwrap(); 24 | insta::assert_debug_snapshot!(obj); 25 | let symbol_idx = obj 26 | .symbols 27 | .iter() 28 | .position(|s| s.name == "THUMB_BRANCH_ServerDisplay_UncategorizedMove") 29 | .unwrap(); 30 | let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap(); 31 | insta::assert_debug_snapshot!(diff.instruction_rows); 32 | let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config); 33 | insta::assert_snapshot!(output); 34 | } 35 | 36 | #[test] 37 | #[cfg(feature = "arm")] 38 | fn combine_text_sections() { 39 | let diff_config = diff::DiffObjConfig { combine_text_sections: true, ..Default::default() }; 40 | let obj = obj::read::parse(include_object!("data/arm/enemy300.o"), &diff_config).unwrap(); 41 | let symbol_idx = obj.symbols.iter().position(|s| s.name == "Enemy300Draw").unwrap(); 42 | let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap(); 43 | insta::assert_debug_snapshot!(diff.instruction_rows); 44 | let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config); 45 | insta::assert_snapshot!(output); 46 | } 47 | -------------------------------------------------------------------------------- /objdiff-core/tests/arch_mips.rs: -------------------------------------------------------------------------------- 1 | use objdiff_core::{diff, obj}; 2 | 3 | mod common; 4 | 5 | #[test] 6 | #[cfg(feature = "mips")] 7 | fn read_mips() { 8 | let diff_config = diff::DiffObjConfig { mips_register_prefix: true, ..Default::default() }; 9 | let obj = obj::read::parse(include_object!("data/mips/main.c.o"), &diff_config).unwrap(); 10 | insta::assert_debug_snapshot!(obj); 11 | let symbol_idx = obj.symbols.iter().position(|s| s.name == "ControlEntry").unwrap(); 12 | let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap(); 13 | insta::assert_debug_snapshot!(diff.instruction_rows); 14 | let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config); 15 | insta::assert_snapshot!(output); 16 | } 17 | 18 | #[test] 19 | #[cfg(feature = "mips")] 20 | fn cross_endian_diff() { 21 | let diff_config = diff::DiffObjConfig::default(); 22 | let obj_be = obj::read::parse(include_object!("data/mips/code_be.o"), &diff_config).unwrap(); 23 | assert_eq!(obj_be.endianness, object::Endianness::Big); 24 | let obj_le = obj::read::parse(include_object!("data/mips/code_le.o"), &diff_config).unwrap(); 25 | assert_eq!(obj_le.endianness, object::Endianness::Little); 26 | let left_symbol_idx = obj_be.symbols.iter().position(|s| s.name == "func_00000000").unwrap(); 27 | let right_symbol_idx = 28 | obj_le.symbols.iter().position(|s| s.name == "func_00000000__FPcPc").unwrap(); 29 | let (left_diff, right_diff) = 30 | diff::code::diff_code(&obj_be, &obj_le, left_symbol_idx, right_symbol_idx, &diff_config) 31 | .unwrap(); 32 | // Although the objects differ in endianness, the instructions should match. 33 | assert_eq!(left_diff.instruction_rows[0].kind, diff::InstructionDiffKind::None); 34 | assert_eq!(right_diff.instruction_rows[0].kind, diff::InstructionDiffKind::None); 35 | assert_eq!(left_diff.instruction_rows[1].kind, diff::InstructionDiffKind::None); 36 | assert_eq!(right_diff.instruction_rows[1].kind, diff::InstructionDiffKind::None); 37 | assert_eq!(left_diff.instruction_rows[2].kind, diff::InstructionDiffKind::None); 38 | assert_eq!(right_diff.instruction_rows[2].kind, diff::InstructionDiffKind::None); 39 | } 40 | 41 | #[test] 42 | #[cfg(feature = "mips")] 43 | fn filter_non_matching() { 44 | let diff_config = diff::DiffObjConfig::default(); 45 | let obj = obj::read::parse(include_object!("data/mips/vw_main.c.o"), &diff_config).unwrap(); 46 | insta::assert_debug_snapshot!(obj.symbols); 47 | } 48 | -------------------------------------------------------------------------------- /objdiff-core/tests/arch_ppc.rs: -------------------------------------------------------------------------------- 1 | use objdiff_core::{ 2 | diff::{self, display}, 3 | obj, 4 | obj::SectionKind, 5 | }; 6 | 7 | mod common; 8 | 9 | #[test] 10 | #[cfg(feature = "ppc")] 11 | fn read_ppc() { 12 | let diff_config = diff::DiffObjConfig::default(); 13 | let obj = obj::read::parse(include_object!("data/ppc/IObj.o"), &diff_config).unwrap(); 14 | insta::assert_debug_snapshot!(obj); 15 | let symbol_idx = 16 | obj.symbols.iter().position(|s| s.name == "Type2Text__10SObjectTagFUi").unwrap(); 17 | let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap(); 18 | insta::assert_debug_snapshot!(diff.instruction_rows); 19 | let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config); 20 | insta::assert_snapshot!(output); 21 | } 22 | 23 | #[test] 24 | #[cfg(feature = "ppc")] 25 | fn read_dwarf1_line_info() { 26 | let diff_config = diff::DiffObjConfig::default(); 27 | let obj = obj::read::parse(include_object!("data/ppc/m_Do_hostIO.o"), &diff_config).unwrap(); 28 | let line_infos = obj 29 | .sections 30 | .iter() 31 | .filter(|s| s.kind == SectionKind::Code) 32 | .map(|s| s.line_info.clone()) 33 | .collect::>(); 34 | insta::assert_debug_snapshot!(line_infos); 35 | } 36 | 37 | #[test] 38 | #[cfg(feature = "ppc")] 39 | fn read_extab() { 40 | let diff_config = diff::DiffObjConfig::default(); 41 | let obj = obj::read::parse(include_object!("data/ppc/NMWException.o"), &diff_config).unwrap(); 42 | insta::assert_debug_snapshot!(obj); 43 | } 44 | 45 | #[test] 46 | #[cfg(feature = "ppc")] 47 | fn diff_ppc() { 48 | let diff_config = diff::DiffObjConfig::default(); 49 | let mapping_config = diff::MappingConfig::default(); 50 | let target_obj = 51 | obj::read::parse(include_object!("data/ppc/CDamageVulnerability_target.o"), &diff_config) 52 | .unwrap(); 53 | let base_obj = 54 | obj::read::parse(include_object!("data/ppc/CDamageVulnerability_base.o"), &diff_config) 55 | .unwrap(); 56 | let diff = 57 | diff::diff_objs(Some(&target_obj), Some(&base_obj), None, &diff_config, &mapping_config) 58 | .unwrap(); 59 | 60 | let target_diff = diff.left.as_ref().unwrap(); 61 | let base_diff = diff.right.as_ref().unwrap(); 62 | let sections_display = display::display_sections( 63 | &target_obj, 64 | target_diff, 65 | display::SymbolFilter::None, 66 | false, 67 | false, 68 | true, 69 | ); 70 | insta::assert_debug_snapshot!(sections_display); 71 | 72 | let target_symbol_idx = target_obj 73 | .symbols 74 | .iter() 75 | .position(|s| s.name == "WeaponHurts__20CDamageVulnerabilityCFRC11CWeaponModei") 76 | .unwrap(); 77 | let target_symbol_diff = &target_diff.symbols[target_symbol_idx]; 78 | let base_symbol_idx = base_obj 79 | .symbols 80 | .iter() 81 | .position(|s| s.name == "WeaponHurts__20CDamageVulnerabilityCFRC11CWeaponModei") 82 | .unwrap(); 83 | let base_symbol_diff = &base_diff.symbols[base_symbol_idx]; 84 | assert_eq!(target_symbol_diff.target_symbol, Some(base_symbol_idx)); 85 | assert_eq!(base_symbol_diff.target_symbol, Some(target_symbol_idx)); 86 | insta::assert_debug_snapshot!((target_symbol_diff, base_symbol_diff)); 87 | } 88 | -------------------------------------------------------------------------------- /objdiff-core/tests/arch_x86.rs: -------------------------------------------------------------------------------- 1 | use objdiff_core::{diff, diff::display::SymbolFilter, obj}; 2 | 3 | mod common; 4 | 5 | #[test] 6 | #[cfg(feature = "x86")] 7 | fn read_x86() { 8 | let diff_config = diff::DiffObjConfig::default(); 9 | let obj = obj::read::parse(include_object!("data/x86/staticdebug.obj"), &diff_config).unwrap(); 10 | insta::assert_debug_snapshot!(obj); 11 | let symbol_idx = obj.symbols.iter().position(|s| s.name == "?PrintThing@@YAXXZ").unwrap(); 12 | let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap(); 13 | insta::assert_debug_snapshot!(diff.instruction_rows); 14 | let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config); 15 | insta::assert_snapshot!(output); 16 | } 17 | 18 | #[test] 19 | #[cfg(feature = "x86")] 20 | fn read_x86_combine_sections() { 21 | let diff_config = diff::DiffObjConfig { 22 | combine_data_sections: true, 23 | combine_text_sections: true, 24 | ..Default::default() 25 | }; 26 | let obj = obj::read::parse(include_object!("data/x86/rtest.obj"), &diff_config).unwrap(); 27 | insta::assert_debug_snapshot!(obj.sections); 28 | } 29 | 30 | #[test] 31 | #[cfg(feature = "x86")] 32 | fn read_x86_64() { 33 | let diff_config = diff::DiffObjConfig::default(); 34 | let obj = obj::read::parse(include_object!("data/x86_64/vs2022.o"), &diff_config).unwrap(); 35 | insta::assert_debug_snapshot!(obj); 36 | let symbol_idx = 37 | obj.symbols.iter().position(|s| s.name == "?Dot@Vector@@QEAAMPEAU1@@Z").unwrap(); 38 | let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap(); 39 | insta::assert_debug_snapshot!(diff.instruction_rows); 40 | let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config); 41 | insta::assert_snapshot!(output); 42 | } 43 | 44 | #[test] 45 | #[cfg(feature = "x86")] 46 | fn display_section_ordering() { 47 | let diff_config = diff::DiffObjConfig::default(); 48 | let obj = obj::read::parse(include_object!("data/x86/basenode.obj"), &diff_config).unwrap(); 49 | let obj_diff = 50 | diff::diff_objs(Some(&obj), None, None, &diff_config, &diff::MappingConfig::default()) 51 | .unwrap() 52 | .left 53 | .unwrap(); 54 | let section_display = 55 | diff::display::display_sections(&obj, &obj_diff, SymbolFilter::None, false, false, false); 56 | insta::assert_debug_snapshot!(section_display); 57 | } 58 | 59 | #[test] 60 | #[cfg(feature = "x86")] 61 | fn read_x86_jumptable() { 62 | let diff_config = diff::DiffObjConfig::default(); 63 | let obj = obj::read::parse(include_object!("data/x86/jumptable.o"), &diff_config).unwrap(); 64 | insta::assert_debug_snapshot!(obj); 65 | let symbol_idx = obj.symbols.iter().position(|s| s.name == "?test@@YAHH@Z").unwrap(); 66 | let diff = diff::code::no_diff_code(&obj, symbol_idx, &diff_config).unwrap(); 67 | insta::assert_debug_snapshot!(diff.instruction_rows); 68 | let output = common::display_diff(&obj, &diff, symbol_idx, &diff_config); 69 | insta::assert_snapshot!(output); 70 | } 71 | 72 | // Inferred size of functions should ignore symbols with specific prefixes 73 | #[test] 74 | #[cfg(feature = "x86")] 75 | fn read_x86_local_labels() { 76 | let diff_config = diff::DiffObjConfig::default(); 77 | let obj = obj::read::parse(include_object!("data/x86/local_labels.obj"), &diff_config).unwrap(); 78 | insta::assert_debug_snapshot!(obj); 79 | } 80 | -------------------------------------------------------------------------------- /objdiff-core/tests/common.rs: -------------------------------------------------------------------------------- 1 | use objdiff_core::{ 2 | diff::{DiffObjConfig, SymbolDiff, display::DiffTextSegment}, 3 | obj::Object, 4 | }; 5 | 6 | pub fn display_diff( 7 | obj: &Object, 8 | diff: &SymbolDiff, 9 | symbol_idx: usize, 10 | diff_config: &DiffObjConfig, 11 | ) -> String { 12 | let mut output = String::new(); 13 | for row in &diff.instruction_rows { 14 | output.push('['); 15 | let mut separator = false; 16 | objdiff_core::diff::display::display_row(obj, symbol_idx, row, diff_config, |segment| { 17 | if separator { 18 | output.push_str(", "); 19 | } else { 20 | separator = true; 21 | } 22 | let DiffTextSegment { text, color, pad_to } = segment; 23 | output.push_str(&format!("({:?}, {:?}, {:?})", text, color, pad_to)); 24 | Ok(()) 25 | }) 26 | .unwrap(); 27 | output.push_str("]\n"); 28 | } 29 | output 30 | } 31 | 32 | #[repr(C)] 33 | pub struct AlignedAs { 34 | pub _align: [Align; 0], 35 | pub bytes: Bytes, 36 | } 37 | 38 | #[macro_export] 39 | macro_rules! include_bytes_align_as { 40 | ($align_ty:ty, $path:literal) => {{ 41 | static ALIGNED: &common::AlignedAs<$align_ty, [u8]> = 42 | &common::AlignedAs { _align: [], bytes: *include_bytes!($path) }; 43 | &ALIGNED.bytes 44 | }}; 45 | } 46 | 47 | #[macro_export] 48 | macro_rules! include_object { 49 | ($path:literal) => { 50 | include_bytes_align_as!(u64, $path) 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /objdiff-core/tests/data/arm/LinkStateItem.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/arm/LinkStateItem.o -------------------------------------------------------------------------------- /objdiff-core/tests/data/arm/enemy300.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/arm/enemy300.o -------------------------------------------------------------------------------- /objdiff-core/tests/data/arm/thumb.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/arm/thumb.o -------------------------------------------------------------------------------- /objdiff-core/tests/data/mips/code_be.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/mips/code_be.o -------------------------------------------------------------------------------- /objdiff-core/tests/data/mips/code_le.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/mips/code_le.o -------------------------------------------------------------------------------- /objdiff-core/tests/data/mips/main.c.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/mips/main.c.o -------------------------------------------------------------------------------- /objdiff-core/tests/data/mips/vw_main.c.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/mips/vw_main.c.o -------------------------------------------------------------------------------- /objdiff-core/tests/data/ppc/CDamageVulnerability_base.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/ppc/CDamageVulnerability_base.o -------------------------------------------------------------------------------- /objdiff-core/tests/data/ppc/CDamageVulnerability_target.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/ppc/CDamageVulnerability_target.o -------------------------------------------------------------------------------- /objdiff-core/tests/data/ppc/IObj.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/ppc/IObj.o -------------------------------------------------------------------------------- /objdiff-core/tests/data/ppc/NMWException.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/ppc/NMWException.o -------------------------------------------------------------------------------- /objdiff-core/tests/data/ppc/m_Do_hostIO.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/ppc/m_Do_hostIO.o -------------------------------------------------------------------------------- /objdiff-core/tests/data/x86/basenode.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/x86/basenode.obj -------------------------------------------------------------------------------- /objdiff-core/tests/data/x86/jumptable.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/x86/jumptable.o -------------------------------------------------------------------------------- /objdiff-core/tests/data/x86/local_labels.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/x86/local_labels.obj -------------------------------------------------------------------------------- /objdiff-core/tests/data/x86/rtest.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/x86/rtest.obj -------------------------------------------------------------------------------- /objdiff-core/tests/data/x86/staticdebug.obj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/x86/staticdebug.obj -------------------------------------------------------------------------------- /objdiff-core/tests/data/x86_64/vs2022.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-core/tests/data/x86_64/vs2022.o -------------------------------------------------------------------------------- /objdiff-core/tests/snapshots/arch_arm__combine_text_sections-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: objdiff-core/tests/arch_arm.rs 3 | expression: output 4 | --- 5 | [(Line(90), Dim, 5), (Address(0), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ldr", 32799), Normal, 10), (Argument(Opaque("r12")), Normal, 0), (Basic(", "), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("pc")), Normal, 0), (Basic(", "), Normal, 0), (Basic("#"), Normal, 0), (Argument(Signed(0)), Normal, 0), (Basic("]"), Normal, 0), (Basic(" (->"), Normal, 0), (BranchDest(8), Normal, 0), (Basic(")"), Normal, 0), (Eol, Normal, 0)] 6 | [(Line(90), Dim, 5), (Address(4), Normal, 5), (Spacing(4), Normal, 0), (Opcode("bx", 32779), Normal, 10), (Argument(Opaque("r12")), Normal, 0), (Eol, Normal, 0)] 7 | [(Line(90), Dim, 5), (Address(8), Normal, 5), (Spacing(4), Normal, 0), (Opcode(".word", 65535), Normal, 10), (Symbol(Symbol { name: "esEnemyDraw", demangled_name: None, address: 0, size: 0, kind: Unknown, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)] 8 | -------------------------------------------------------------------------------- /objdiff-core/tests/snapshots/arch_arm__combine_text_sections.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: objdiff-core/tests/arch_arm.rs 3 | expression: diff.instruction_rows 4 | --- 5 | [ 6 | InstructionDiffRow { 7 | ins_ref: Some( 8 | InstructionRef { 9 | address: 76, 10 | size: 4, 11 | opcode: 32799, 12 | branch_dest: None, 13 | }, 14 | ), 15 | kind: None, 16 | branch_from: None, 17 | branch_to: None, 18 | arg_diff: [], 19 | }, 20 | InstructionDiffRow { 21 | ins_ref: Some( 22 | InstructionRef { 23 | address: 80, 24 | size: 4, 25 | opcode: 32779, 26 | branch_dest: None, 27 | }, 28 | ), 29 | kind: None, 30 | branch_from: None, 31 | branch_to: None, 32 | arg_diff: [], 33 | }, 34 | InstructionDiffRow { 35 | ins_ref: Some( 36 | InstructionRef { 37 | address: 84, 38 | size: 4, 39 | opcode: 65535, 40 | branch_dest: None, 41 | }, 42 | ), 43 | kind: None, 44 | branch_from: None, 45 | branch_to: None, 46 | arg_diff: [], 47 | }, 48 | ] 49 | -------------------------------------------------------------------------------- /objdiff-core/tests/snapshots/arch_ppc__diff_ppc.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: objdiff-core/tests/arch_ppc.rs 3 | assertion_line: 70 4 | expression: sections_display 5 | --- 6 | [ 7 | SectionDisplay { 8 | id: ".comm", 9 | name: ".comm", 10 | size: 0, 11 | match_percent: None, 12 | symbols: [ 13 | SectionDisplaySymbol { 14 | symbol: 11, 15 | is_mapping_symbol: false, 16 | }, 17 | SectionDisplaySymbol { 18 | symbol: 12, 19 | is_mapping_symbol: false, 20 | }, 21 | SectionDisplaySymbol { 22 | symbol: 13, 23 | is_mapping_symbol: false, 24 | }, 25 | SectionDisplaySymbol { 26 | symbol: 14, 27 | is_mapping_symbol: false, 28 | }, 29 | ], 30 | }, 31 | SectionDisplay { 32 | id: ".ctors-0", 33 | name: ".ctors", 34 | size: 4, 35 | match_percent: Some( 36 | 100.0, 37 | ), 38 | symbols: [ 39 | SectionDisplaySymbol { 40 | symbol: 2, 41 | is_mapping_symbol: false, 42 | }, 43 | ], 44 | }, 45 | SectionDisplay { 46 | id: ".text-0", 47 | name: ".text", 48 | size: 3060, 49 | match_percent: Some( 50 | 58.662746, 51 | ), 52 | symbols: [ 53 | SectionDisplaySymbol { 54 | symbol: 3, 55 | is_mapping_symbol: false, 56 | }, 57 | SectionDisplaySymbol { 58 | symbol: 10, 59 | is_mapping_symbol: false, 60 | }, 61 | SectionDisplaySymbol { 62 | symbol: 9, 63 | is_mapping_symbol: false, 64 | }, 65 | SectionDisplaySymbol { 66 | symbol: 8, 67 | is_mapping_symbol: false, 68 | }, 69 | SectionDisplaySymbol { 70 | symbol: 7, 71 | is_mapping_symbol: false, 72 | }, 73 | SectionDisplaySymbol { 74 | symbol: 6, 75 | is_mapping_symbol: false, 76 | }, 77 | SectionDisplaySymbol { 78 | symbol: 5, 79 | is_mapping_symbol: false, 80 | }, 81 | SectionDisplaySymbol { 82 | symbol: 4, 83 | is_mapping_symbol: false, 84 | }, 85 | ], 86 | }, 87 | ] 88 | -------------------------------------------------------------------------------- /objdiff-core/tests/snapshots/arch_ppc__read_dwarf1_line_info.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: objdiff-core/tests/arch_ppc.rs 3 | expression: line_infos 4 | --- 5 | [ 6 | { 7 | 0: 13, 8 | 4: 16, 9 | 32: 17, 10 | 44: 18, 11 | 60: 20, 12 | 76: 21, 13 | 84: 23, 14 | 92: 25, 15 | 108: 26, 16 | 124: 27, 17 | 136: 28, 18 | 144: 29, 19 | 152: 31, 20 | 164: 34, 21 | 184: 35, 22 | 212: 39, 23 | 228: 40, 24 | 236: 41, 25 | 260: 43, 26 | 288: 44, 27 | 292: 45, 28 | 300: 48, 29 | 436: 0, 30 | }, 31 | { 32 | 0: 48, 33 | 132: 35, 34 | 244: 26, 35 | 304: 22, 36 | 312: 23, 37 | 316: 24, 38 | 320: 0, 39 | }, 40 | ] 41 | -------------------------------------------------------------------------------- /objdiff-core/tests/snapshots/arch_x86__display_section_ordering.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: objdiff-core/tests/arch_x86.rs 3 | expression: section_display 4 | --- 5 | [ 6 | SectionDisplay { 7 | id: ".text-0", 8 | name: ".text", 9 | size: 47, 10 | match_percent: None, 11 | symbols: [ 12 | SectionDisplaySymbol { 13 | symbol: 28, 14 | is_mapping_symbol: false, 15 | }, 16 | ], 17 | }, 18 | SectionDisplay { 19 | id: ".text-1", 20 | name: ".text", 21 | size: 65, 22 | match_percent: None, 23 | symbols: [ 24 | SectionDisplaySymbol { 25 | symbol: 29, 26 | is_mapping_symbol: false, 27 | }, 28 | ], 29 | }, 30 | SectionDisplay { 31 | id: ".text-2", 32 | name: ".text", 33 | size: 5, 34 | match_percent: None, 35 | symbols: [ 36 | SectionDisplaySymbol { 37 | symbol: 30, 38 | is_mapping_symbol: false, 39 | }, 40 | ], 41 | }, 42 | SectionDisplay { 43 | id: ".text-3", 44 | name: ".text", 45 | size: 141, 46 | match_percent: None, 47 | symbols: [ 48 | SectionDisplaySymbol { 49 | symbol: 31, 50 | is_mapping_symbol: false, 51 | }, 52 | ], 53 | }, 54 | SectionDisplay { 55 | id: ".text-4", 56 | name: ".text", 57 | size: 120, 58 | match_percent: None, 59 | symbols: [ 60 | SectionDisplaySymbol { 61 | symbol: 34, 62 | is_mapping_symbol: false, 63 | }, 64 | ], 65 | }, 66 | SectionDisplay { 67 | id: ".text-5", 68 | name: ".text", 69 | size: 378, 70 | match_percent: None, 71 | symbols: [ 72 | SectionDisplaySymbol { 73 | symbol: 37, 74 | is_mapping_symbol: false, 75 | }, 76 | ], 77 | }, 78 | SectionDisplay { 79 | id: ".text-6", 80 | name: ".text", 81 | size: 130, 82 | match_percent: None, 83 | symbols: [ 84 | SectionDisplaySymbol { 85 | symbol: 38, 86 | is_mapping_symbol: false, 87 | }, 88 | ], 89 | }, 90 | SectionDisplay { 91 | id: ".text-7", 92 | name: ".text", 93 | size: 123, 94 | match_percent: None, 95 | symbols: [ 96 | SectionDisplaySymbol { 97 | symbol: 39, 98 | is_mapping_symbol: false, 99 | }, 100 | ], 101 | }, 102 | SectionDisplay { 103 | id: ".text-8", 104 | name: ".text", 105 | size: 70, 106 | match_percent: None, 107 | symbols: [ 108 | SectionDisplaySymbol { 109 | symbol: 40, 110 | is_mapping_symbol: false, 111 | }, 112 | ], 113 | }, 114 | SectionDisplay { 115 | id: ".text-9", 116 | name: ".text", 117 | size: 90, 118 | match_percent: None, 119 | symbols: [ 120 | SectionDisplaySymbol { 121 | symbol: 41, 122 | is_mapping_symbol: false, 123 | }, 124 | ], 125 | }, 126 | SectionDisplay { 127 | id: ".text-10", 128 | name: ".text", 129 | size: 82, 130 | match_percent: None, 131 | symbols: [ 132 | SectionDisplaySymbol { 133 | symbol: 42, 134 | is_mapping_symbol: false, 135 | }, 136 | ], 137 | }, 138 | SectionDisplay { 139 | id: ".text-11", 140 | name: ".text", 141 | size: 336, 142 | match_percent: None, 143 | symbols: [ 144 | SectionDisplaySymbol { 145 | symbol: 43, 146 | is_mapping_symbol: false, 147 | }, 148 | ], 149 | }, 150 | SectionDisplay { 151 | id: ".text-12", 152 | name: ".text", 153 | size: 193, 154 | match_percent: None, 155 | symbols: [ 156 | SectionDisplaySymbol { 157 | symbol: 50, 158 | is_mapping_symbol: false, 159 | }, 160 | ], 161 | }, 162 | SectionDisplay { 163 | id: ".text-13", 164 | name: ".text", 165 | size: 544, 166 | match_percent: None, 167 | symbols: [ 168 | SectionDisplaySymbol { 169 | symbol: 53, 170 | is_mapping_symbol: false, 171 | }, 172 | ], 173 | }, 174 | SectionDisplay { 175 | id: ".text-14", 176 | name: ".text", 177 | size: 250, 178 | match_percent: None, 179 | symbols: [ 180 | SectionDisplaySymbol { 181 | symbol: 56, 182 | is_mapping_symbol: false, 183 | }, 184 | ], 185 | }, 186 | SectionDisplay { 187 | id: ".text-15", 188 | name: ".text", 189 | size: 89, 190 | match_percent: None, 191 | symbols: [ 192 | SectionDisplaySymbol { 193 | symbol: 57, 194 | is_mapping_symbol: false, 195 | }, 196 | ], 197 | }, 198 | SectionDisplay { 199 | id: ".text-16", 200 | name: ".text", 201 | size: 119, 202 | match_percent: None, 203 | symbols: [ 204 | SectionDisplaySymbol { 205 | symbol: 60, 206 | is_mapping_symbol: false, 207 | }, 208 | ], 209 | }, 210 | ] 211 | -------------------------------------------------------------------------------- /objdiff-core/tests/snapshots/arch_x86__read_x86-2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: objdiff-core/tests/arch_x86.rs 3 | expression: diff.instruction_rows 4 | --- 5 | [ 6 | InstructionDiffRow { 7 | ins_ref: Some( 8 | InstructionRef { 9 | address: 0, 10 | size: 1, 11 | opcode: 640, 12 | branch_dest: None, 13 | }, 14 | ), 15 | kind: None, 16 | branch_from: None, 17 | branch_to: None, 18 | arg_diff: [], 19 | }, 20 | InstructionDiffRow { 21 | ins_ref: Some( 22 | InstructionRef { 23 | address: 1, 24 | size: 2, 25 | opcode: 414, 26 | branch_dest: None, 27 | }, 28 | ), 29 | kind: None, 30 | branch_from: None, 31 | branch_to: None, 32 | arg_diff: [], 33 | }, 34 | InstructionDiffRow { 35 | ins_ref: Some( 36 | InstructionRef { 37 | address: 3, 38 | size: 5, 39 | opcode: 640, 40 | branch_dest: None, 41 | }, 42 | ), 43 | kind: None, 44 | branch_from: None, 45 | branch_to: None, 46 | arg_diff: [], 47 | }, 48 | InstructionDiffRow { 49 | ins_ref: Some( 50 | InstructionRef { 51 | address: 8, 52 | size: 5, 53 | opcode: 59, 54 | branch_dest: None, 55 | }, 56 | ), 57 | kind: None, 58 | branch_from: None, 59 | branch_to: None, 60 | arg_diff: [], 61 | }, 62 | InstructionDiffRow { 63 | ins_ref: Some( 64 | InstructionRef { 65 | address: 13, 66 | size: 3, 67 | opcode: 7, 68 | branch_dest: None, 69 | }, 70 | ), 71 | kind: None, 72 | branch_from: None, 73 | branch_to: None, 74 | arg_diff: [], 75 | }, 76 | InstructionDiffRow { 77 | ins_ref: Some( 78 | InstructionRef { 79 | address: 16, 80 | size: 1, 81 | opcode: 590, 82 | branch_dest: None, 83 | }, 84 | ), 85 | kind: None, 86 | branch_from: None, 87 | branch_to: None, 88 | arg_diff: [], 89 | }, 90 | InstructionDiffRow { 91 | ins_ref: Some( 92 | InstructionRef { 93 | address: 17, 94 | size: 1, 95 | opcode: 662, 96 | branch_dest: None, 97 | }, 98 | ), 99 | kind: None, 100 | branch_from: None, 101 | branch_to: None, 102 | arg_diff: [], 103 | }, 104 | ] 105 | -------------------------------------------------------------------------------- /objdiff-core/tests/snapshots/arch_x86__read_x86-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: objdiff-core/tests/arch_x86.rs 3 | expression: output 4 | --- 5 | [(Address(0), Normal, 5), (Spacing(4), Normal, 0), (Opcode("push", 640), Normal, 10), (Argument(Opaque("ebp")), Normal, 0), (Eol, Normal, 0)] 6 | [(Address(1), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("ebp")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Opaque("esp")), Normal, 0), (Eol, Normal, 0)] 7 | [(Address(3), Normal, 5), (Spacing(4), Normal, 0), (Opcode("push", 640), Normal, 10), (Symbol(Symbol { name: "$SG526", demangled_name: None, address: 4, size: 6, kind: Object, section: Some(1), flags: FlagSet(Local | SizeInferred), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)] 8 | [(Address(8), Normal, 5), (Spacing(4), Normal, 0), (Opcode("call", 59), Normal, 10), (Symbol(Symbol { name: "_printf", demangled_name: None, address: 0, size: 0, kind: Function, section: None, flags: FlagSet(Global), align: None, virtual_address: None }), Bright, 0), (Eol, Normal, 0)] 9 | [(Address(13), Normal, 5), (Spacing(4), Normal, 0), (Opcode("add", 7), Normal, 10), (Argument(Opaque("esp")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Unsigned(4)), Normal, 0), (Eol, Normal, 0)] 10 | [(Address(16), Normal, 5), (Spacing(4), Normal, 0), (Opcode("pop", 590), Normal, 10), (Argument(Opaque("ebp")), Normal, 0), (Eol, Normal, 0)] 11 | [(Address(17), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ret", 662), Normal, 10), (Eol, Normal, 0)] 12 | -------------------------------------------------------------------------------- /objdiff-core/tests/snapshots/arch_x86__read_x86.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: objdiff-core/tests/arch_x86.rs 3 | expression: obj 4 | --- 5 | Object { 6 | arch: ArchX86 { 7 | arch: X86, 8 | endianness: Little, 9 | }, 10 | endianness: Little, 11 | symbols: [ 12 | Symbol { 13 | name: "objdiffstaticdebug.cpp", 14 | demangled_name: None, 15 | address: 0, 16 | size: 0, 17 | kind: Unknown, 18 | section: None, 19 | flags: FlagSet(Local), 20 | align: None, 21 | virtual_address: None, 22 | }, 23 | Symbol { 24 | name: "@comp.id", 25 | demangled_name: None, 26 | address: 0, 27 | size: 0, 28 | kind: Object, 29 | section: None, 30 | flags: FlagSet(Local), 31 | align: None, 32 | virtual_address: None, 33 | }, 34 | Symbol { 35 | name: "[.drectve]", 36 | demangled_name: None, 37 | address: 0, 38 | size: 38, 39 | kind: Section, 40 | section: Some( 41 | 0, 42 | ), 43 | flags: FlagSet(Local), 44 | align: None, 45 | virtual_address: None, 46 | }, 47 | Symbol { 48 | name: "[.data]", 49 | demangled_name: None, 50 | address: 0, 51 | size: 0, 52 | kind: Section, 53 | section: Some( 54 | 1, 55 | ), 56 | flags: FlagSet(Local), 57 | align: None, 58 | virtual_address: None, 59 | }, 60 | Symbol { 61 | name: "?a@@3PAXA", 62 | demangled_name: Some( 63 | "void *a", 64 | ), 65 | address: 0, 66 | size: 4, 67 | kind: Object, 68 | section: Some( 69 | 1, 70 | ), 71 | flags: FlagSet(Global | SizeInferred), 72 | align: None, 73 | virtual_address: None, 74 | }, 75 | Symbol { 76 | name: "[.text]", 77 | demangled_name: None, 78 | address: 0, 79 | size: 0, 80 | kind: Section, 81 | section: Some( 82 | 2, 83 | ), 84 | flags: FlagSet(Local), 85 | align: None, 86 | virtual_address: None, 87 | }, 88 | Symbol { 89 | name: "?PrintThing@@YAXXZ", 90 | demangled_name: Some( 91 | "void __cdecl PrintThing(void)", 92 | ), 93 | address: 0, 94 | size: 18, 95 | kind: Function, 96 | section: Some( 97 | 2, 98 | ), 99 | flags: FlagSet(Local | SizeInferred), 100 | align: None, 101 | virtual_address: None, 102 | }, 103 | Symbol { 104 | name: "_printf", 105 | demangled_name: None, 106 | address: 0, 107 | size: 0, 108 | kind: Function, 109 | section: None, 110 | flags: FlagSet(Global), 111 | align: None, 112 | virtual_address: None, 113 | }, 114 | Symbol { 115 | name: "$SG526", 116 | demangled_name: None, 117 | address: 4, 118 | size: 6, 119 | kind: Object, 120 | section: Some( 121 | 1, 122 | ), 123 | flags: FlagSet(Local | SizeInferred), 124 | align: None, 125 | virtual_address: None, 126 | }, 127 | ], 128 | sections: [ 129 | Section { 130 | id: ".drectve-0", 131 | name: ".drectve", 132 | address: 0, 133 | size: 38, 134 | kind: Unknown, 135 | data: SectionData( 136 | 0, 137 | ), 138 | flags: FlagSet(), 139 | align: Some( 140 | 1, 141 | ), 142 | relocations: [], 143 | line_info: {}, 144 | virtual_address: None, 145 | }, 146 | Section { 147 | id: ".data-0", 148 | name: ".data", 149 | address: 0, 150 | size: 10, 151 | kind: Data, 152 | data: SectionData( 153 | 10, 154 | ), 155 | flags: FlagSet(), 156 | align: Some( 157 | 4, 158 | ), 159 | relocations: [ 160 | Relocation { 161 | flags: Coff( 162 | 6, 163 | ), 164 | address: 0, 165 | target_symbol: 6, 166 | addend: 0, 167 | }, 168 | ], 169 | line_info: {}, 170 | virtual_address: None, 171 | }, 172 | Section { 173 | id: ".text-0", 174 | name: ".text", 175 | address: 0, 176 | size: 18, 177 | kind: Code, 178 | data: SectionData( 179 | 18, 180 | ), 181 | flags: FlagSet(), 182 | align: Some( 183 | 16, 184 | ), 185 | relocations: [ 186 | Relocation { 187 | flags: Coff( 188 | 6, 189 | ), 190 | address: 4, 191 | target_symbol: 8, 192 | addend: 0, 193 | }, 194 | Relocation { 195 | flags: Coff( 196 | 20, 197 | ), 198 | address: 9, 199 | target_symbol: 7, 200 | addend: 0, 201 | }, 202 | ], 203 | line_info: {}, 204 | virtual_address: None, 205 | }, 206 | ], 207 | split_meta: None, 208 | path: None, 209 | timestamp: None, 210 | } 211 | -------------------------------------------------------------------------------- /objdiff-core/tests/snapshots/arch_x86__read_x86_64-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: objdiff-core/tests/arch_x86.rs 3 | expression: output 4 | --- 5 | [(Address(0), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 414), Normal, 10), (Basic("["), Normal, 0), (Argument(Opaque("rsp")), Normal, 0), (Argument(Opaque("+")), Normal, 0), (Argument(Signed(16)), Normal, 0), (Basic("]"), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Opaque("rdx")), Normal, 0), (Eol, Normal, 0)] 6 | [(Address(5), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 414), Normal, 10), (Basic("["), Normal, 0), (Argument(Opaque("rsp")), Normal, 0), (Argument(Opaque("+")), Normal, 0), (Argument(Signed(8)), Normal, 0), (Basic("]"), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Opaque("rcx")), Normal, 0), (Eol, Normal, 0)] 7 | [(Address(10), Normal, 5), (Spacing(4), Normal, 0), (Opcode("push", 640), Normal, 10), (Argument(Opaque("rdi")), Normal, 0), (Eol, Normal, 0)] 8 | [(Address(11), Normal, 5), (Spacing(4), Normal, 0), (Opcode("sub", 740), Normal, 10), (Argument(Opaque("rsp")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Unsigned(16)), Normal, 0), (Eol, Normal, 0)] 9 | [(Address(15), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("rax")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("rsp")), Normal, 0), (Argument(Opaque("+")), Normal, 0), (Argument(Signed(32)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)] 10 | [(Address(20), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("rcx")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("rsp")), Normal, 0), (Argument(Opaque("+")), Normal, 0), (Argument(Signed(40)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)] 11 | [(Address(25), Normal, 5), (Spacing(4), Normal, 0), (Opcode("movss", 448), Normal, 10), (Argument(Opaque("xmm0")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("rax")), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)] 12 | [(Address(29), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mulss", 460), Normal, 10), (Argument(Opaque("xmm0")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("rcx")), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)] 13 | [(Address(33), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("rax")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("rsp")), Normal, 0), (Argument(Opaque("+")), Normal, 0), (Argument(Signed(32)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)] 14 | [(Address(38), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("rcx")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("rsp")), Normal, 0), (Argument(Opaque("+")), Normal, 0), (Argument(Signed(40)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)] 15 | [(Address(43), Normal, 5), (Spacing(4), Normal, 0), (Opcode("movss", 448), Normal, 10), (Argument(Opaque("xmm1")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("rax")), Normal, 0), (Argument(Opaque("+")), Normal, 0), (Argument(Signed(4)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)] 16 | [(Address(48), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mulss", 460), Normal, 10), (Argument(Opaque("xmm1")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("rcx")), Normal, 0), (Argument(Opaque("+")), Normal, 0), (Argument(Signed(4)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)] 17 | [(Address(53), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addss", 11), Normal, 10), (Argument(Opaque("xmm0")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Opaque("xmm1")), Normal, 0), (Eol, Normal, 0)] 18 | [(Address(57), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("rax")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("rsp")), Normal, 0), (Argument(Opaque("+")), Normal, 0), (Argument(Signed(32)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)] 19 | [(Address(62), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("rcx")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("rsp")), Normal, 0), (Argument(Opaque("+")), Normal, 0), (Argument(Signed(40)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)] 20 | [(Address(67), Normal, 5), (Spacing(4), Normal, 0), (Opcode("movss", 448), Normal, 10), (Argument(Opaque("xmm1")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("rax")), Normal, 0), (Argument(Opaque("+")), Normal, 0), (Argument(Signed(8)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)] 21 | [(Address(72), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mulss", 460), Normal, 10), (Argument(Opaque("xmm1")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("rcx")), Normal, 0), (Argument(Opaque("+")), Normal, 0), (Argument(Signed(8)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)] 22 | [(Address(77), Normal, 5), (Spacing(4), Normal, 0), (Opcode("addss", 11), Normal, 10), (Argument(Opaque("xmm0")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Opaque("xmm1")), Normal, 0), (Eol, Normal, 0)] 23 | [(Address(81), Normal, 5), (Spacing(4), Normal, 0), (Opcode("add", 7), Normal, 10), (Argument(Opaque("rsp")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Unsigned(16)), Normal, 0), (Eol, Normal, 0)] 24 | [(Address(85), Normal, 5), (Spacing(4), Normal, 0), (Opcode("pop", 590), Normal, 10), (Argument(Opaque("rdi")), Normal, 0), (Eol, Normal, 0)] 25 | [(Address(86), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ret", 662), Normal, 10), (Eol, Normal, 0)] 26 | -------------------------------------------------------------------------------- /objdiff-core/tests/snapshots/arch_x86__read_x86_jumptable-3.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: objdiff-core/tests/arch_x86.rs 3 | expression: output 4 | --- 5 | [(Address(0), Normal, 5), (Spacing(4), Normal, 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("eax")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("esp")), Normal, 0), (Argument(Opaque("+")), Normal, 0), (Argument(Signed(4)), Normal, 0), (Basic("]"), Normal, 0), (Eol, Normal, 0)] 6 | [(Address(4), Normal, 5), (Spacing(4), Normal, 0), (Opcode("dec", 137), Normal, 10), (Argument(Opaque("eax")), Normal, 0), (Eol, Normal, 0)] 7 | [(Address(5), Normal, 5), (Spacing(4), Normal, 0), (Opcode("cmp", 93), Normal, 10), (Argument(Opaque("eax")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Unsigned(6)), Normal, 0), (Eol, Normal, 0)] 8 | [(Address(8), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ja", 297), Normal, 10), (Argument(Opaque("short")), Normal, 0), (Spacing(1), Normal, 0), (BranchDest(58), Normal, 0), (Basic(" ~>"), Rotating(0), 0), (Eol, Normal, 0)] 9 | [(Address(10), Normal, 5), (Spacing(4), Normal, 0), (Opcode("jmp", 308), Normal, 10), (Argument(Opaque("dword")), Normal, 0), (Spacing(1), Normal, 0), (Argument(Opaque("ptr")), Normal, 0), (Spacing(1), Normal, 0), (Basic("["), Normal, 0), (Argument(Opaque("eax")), Normal, 0), (Argument(Opaque("*")), Normal, 0), (Argument(Signed(4)), Normal, 0), (Argument(Opaque("+")), Normal, 0), (BranchDest(60), Normal, 0), (Basic("]"), Normal, 0), (Basic(" ~>"), Rotating(1), 0), (Eol, Normal, 0)] 10 | [(Address(17), Normal, 5), (Basic(" ~> "), Rotating(2), 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("eax")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Unsigned(8)), Normal, 0), (Eol, Normal, 0)] 11 | [(Address(22), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ret", 662), Normal, 10), (Eol, Normal, 0)] 12 | [(Address(23), Normal, 5), (Basic(" ~> "), Rotating(3), 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("eax")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Unsigned(7)), Normal, 0), (Eol, Normal, 0)] 13 | [(Address(28), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ret", 662), Normal, 10), (Eol, Normal, 0)] 14 | [(Address(29), Normal, 5), (Basic(" ~> "), Rotating(4), 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("eax")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Unsigned(6)), Normal, 0), (Eol, Normal, 0)] 15 | [(Address(34), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ret", 662), Normal, 10), (Eol, Normal, 0)] 16 | [(Address(35), Normal, 5), (Basic(" ~> "), Rotating(5), 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("eax")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Unsigned(5)), Normal, 0), (Eol, Normal, 0)] 17 | [(Address(40), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ret", 662), Normal, 10), (Eol, Normal, 0)] 18 | [(Address(41), Normal, 5), (Basic(" ~> "), Rotating(6), 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("eax")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Unsigned(4)), Normal, 0), (Eol, Normal, 0)] 19 | [(Address(46), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ret", 662), Normal, 10), (Eol, Normal, 0)] 20 | [(Address(47), Normal, 5), (Basic(" ~> "), Rotating(7), 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("eax")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Unsigned(3)), Normal, 0), (Eol, Normal, 0)] 21 | [(Address(52), Normal, 5), (Spacing(4), Normal, 0), (Opcode("ret", 662), Normal, 10), (Eol, Normal, 0)] 22 | [(Address(53), Normal, 5), (Basic(" ~> "), Rotating(8), 0), (Opcode("mov", 414), Normal, 10), (Argument(Opaque("eax")), Normal, 0), (Basic(","), Normal, 0), (Spacing(1), Normal, 0), (Argument(Unsigned(2)), Normal, 0), (Eol, Normal, 0)] 23 | [(Address(58), Normal, 5), (Basic(" ~> "), Rotating(0), 0), (Opcode("ret", 662), Normal, 10), (Eol, Normal, 0)] 24 | [(Address(59), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 465), Normal, 10), (Eol, Normal, 0)] 25 | [(Address(60), Normal, 5), (Basic(" ~> "), Rotating(1), 0), (Opcode(".dword", 65534), Normal, 10), (BranchDest(17), Normal, 0), (Basic(" ~>"), Rotating(2), 0), (Eol, Normal, 0)] 26 | [(Address(64), Normal, 5), (Spacing(4), Normal, 0), (Opcode(".dword", 65534), Normal, 10), (BranchDest(23), Normal, 0), (Basic(" ~>"), Rotating(3), 0), (Eol, Normal, 0)] 27 | [(Address(68), Normal, 5), (Spacing(4), Normal, 0), (Opcode(".dword", 65534), Normal, 10), (BranchDest(29), Normal, 0), (Basic(" ~>"), Rotating(4), 0), (Eol, Normal, 0)] 28 | [(Address(72), Normal, 5), (Spacing(4), Normal, 0), (Opcode(".dword", 65534), Normal, 10), (BranchDest(35), Normal, 0), (Basic(" ~>"), Rotating(5), 0), (Eol, Normal, 0)] 29 | [(Address(76), Normal, 5), (Spacing(4), Normal, 0), (Opcode(".dword", 65534), Normal, 10), (BranchDest(41), Normal, 0), (Basic(" ~>"), Rotating(6), 0), (Eol, Normal, 0)] 30 | [(Address(80), Normal, 5), (Spacing(4), Normal, 0), (Opcode(".dword", 65534), Normal, 10), (BranchDest(47), Normal, 0), (Basic(" ~>"), Rotating(7), 0), (Eol, Normal, 0)] 31 | [(Address(84), Normal, 5), (Spacing(4), Normal, 0), (Opcode(".dword", 65534), Normal, 10), (BranchDest(53), Normal, 0), (Basic(" ~>"), Rotating(8), 0), (Eol, Normal, 0)] 32 | [(Address(88), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 465), Normal, 10), (Eol, Normal, 0)] 33 | [(Address(89), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 465), Normal, 10), (Eol, Normal, 0)] 34 | [(Address(90), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 465), Normal, 10), (Eol, Normal, 0)] 35 | [(Address(91), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 465), Normal, 10), (Eol, Normal, 0)] 36 | [(Address(92), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 465), Normal, 10), (Eol, Normal, 0)] 37 | [(Address(93), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 465), Normal, 10), (Eol, Normal, 0)] 38 | [(Address(94), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 465), Normal, 10), (Eol, Normal, 0)] 39 | [(Address(95), Normal, 5), (Spacing(4), Normal, 0), (Opcode("nop", 465), Normal, 10), (Eol, Normal, 0)] 40 | -------------------------------------------------------------------------------- /objdiff-core/tests/snapshots/arch_x86__read_x86_local_labels.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: objdiff-core/tests/arch_x86.rs 3 | expression: obj 4 | --- 5 | Object { 6 | arch: ArchX86 { 7 | arch: X86, 8 | endianness: Little, 9 | }, 10 | endianness: Little, 11 | symbols: [ 12 | Symbol { 13 | name: "42b830_convertToUppercaseShiftJIS.obj", 14 | demangled_name: None, 15 | address: 0, 16 | size: 0, 17 | kind: Unknown, 18 | section: None, 19 | flags: FlagSet(Local), 20 | align: None, 21 | virtual_address: None, 22 | }, 23 | Symbol { 24 | name: "[.text]", 25 | demangled_name: None, 26 | address: 0, 27 | size: 0, 28 | kind: Section, 29 | section: Some( 30 | 0, 31 | ), 32 | flags: FlagSet(Local), 33 | align: None, 34 | virtual_address: None, 35 | }, 36 | Symbol { 37 | name: "LAB_0042b850", 38 | demangled_name: None, 39 | address: 32, 40 | size: 0, 41 | kind: Object, 42 | section: Some( 43 | 0, 44 | ), 45 | flags: FlagSet(Local), 46 | align: None, 47 | virtual_address: None, 48 | }, 49 | Symbol { 50 | name: "LAB_0042b883", 51 | demangled_name: None, 52 | address: 83, 53 | size: 0, 54 | kind: Object, 55 | section: Some( 56 | 0, 57 | ), 58 | flags: FlagSet(Local), 59 | align: None, 60 | virtual_address: None, 61 | }, 62 | Symbol { 63 | name: "LAB_0042b87c", 64 | demangled_name: None, 65 | address: 76, 66 | size: 0, 67 | kind: Object, 68 | section: Some( 69 | 0, 70 | ), 71 | flags: FlagSet(Local), 72 | align: None, 73 | virtual_address: None, 74 | }, 75 | Symbol { 76 | name: "LAB_0042b884", 77 | demangled_name: None, 78 | address: 84, 79 | size: 0, 80 | kind: Object, 81 | section: Some( 82 | 0, 83 | ), 84 | flags: FlagSet(Local), 85 | align: None, 86 | virtual_address: None, 87 | }, 88 | Symbol { 89 | name: "LAB_0042b889", 90 | demangled_name: None, 91 | address: 89, 92 | size: 0, 93 | kind: Object, 94 | section: Some( 95 | 0, 96 | ), 97 | flags: FlagSet(Local), 98 | align: None, 99 | virtual_address: None, 100 | }, 101 | Symbol { 102 | name: "LAB_0042b845", 103 | demangled_name: None, 104 | address: 21, 105 | size: 0, 106 | kind: Object, 107 | section: Some( 108 | 0, 109 | ), 110 | flags: FlagSet(Local), 111 | align: None, 112 | virtual_address: None, 113 | }, 114 | Symbol { 115 | name: "LAB_0042b869", 116 | demangled_name: None, 117 | address: 57, 118 | size: 0, 119 | kind: Object, 120 | section: Some( 121 | 0, 122 | ), 123 | flags: FlagSet(Local), 124 | align: None, 125 | virtual_address: None, 126 | }, 127 | Symbol { 128 | name: "ConvertToUppercaseShiftJIS", 129 | demangled_name: None, 130 | address: 0, 131 | size: 92, 132 | kind: Function, 133 | section: Some( 134 | 0, 135 | ), 136 | flags: FlagSet(Global | SizeInferred), 137 | align: None, 138 | virtual_address: None, 139 | }, 140 | ], 141 | sections: [ 142 | Section { 143 | id: ".text-0", 144 | name: ".text", 145 | address: 0, 146 | size: 92, 147 | kind: Code, 148 | data: SectionData( 149 | 92, 150 | ), 151 | flags: FlagSet(), 152 | align: Some( 153 | 16, 154 | ), 155 | relocations: [], 156 | line_info: {}, 157 | virtual_address: None, 158 | }, 159 | ], 160 | split_meta: None, 161 | path: None, 162 | timestamp: None, 163 | } 164 | -------------------------------------------------------------------------------- /objdiff-gui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "objdiff-gui" 3 | version.workspace = true 4 | edition.workspace = true 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | readme = "../README.md" 10 | description = """ 11 | A local diffing tool for decompilation projects. 12 | """ 13 | publish = false 14 | build = "build.rs" 15 | 16 | [[bin]] 17 | name = "objdiff" 18 | path = "src/main.rs" 19 | 20 | [features] 21 | default = ["glow", "wgpu", "wsl"] 22 | glow = ["eframe/glow"] 23 | wgpu = ["eframe/wgpu", "dep:wgpu"] 24 | wsl = [] 25 | 26 | [dependencies] 27 | anyhow = "1.0" 28 | cfg-if = "1.0" 29 | const_format = "0.2" 30 | cwdemangle = "1.0" 31 | dirs = "6.0" 32 | egui = "0.31" 33 | egui_extras = "0.31" 34 | filetime = "0.2" 35 | float-ord = "0.3" 36 | font-kit = "0.14" 37 | globset = { version = "0.4", features = ["serde1"] } 38 | log = "0.4" 39 | objdiff-core = { path = "../objdiff-core", features = ["all"] } 40 | open = "5.3" 41 | png = "0.17" 42 | pollster = "0.4" 43 | regex = "1.11" 44 | rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal'] 45 | rlwinmdec = "1.1" 46 | ron = "0.8" 47 | serde = { version = "1.0", features = ["derive"] } 48 | time = { version = "0.3", features = ["formatting", "local-offset"] } 49 | typed-path = "0.11" 50 | winit = { version = "0.30", features = ["wayland-csd-adwaita"] } 51 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 52 | 53 | # Keep version in sync with egui 54 | [dependencies.eframe] 55 | version = "0.31" 56 | features = [ 57 | "default_fonts", 58 | "persistence", 59 | "wayland", 60 | "x11", 61 | ] 62 | default-features = false 63 | 64 | # Keep version in sync with eframe 65 | [dependencies.wgpu] 66 | version = "24.0" 67 | features = [ 68 | "dx12", 69 | "metal", 70 | "webgpu", 71 | ] 72 | optional = true 73 | default-features = false 74 | 75 | [target.'cfg(windows)'.dependencies] 76 | winapi = "0.3" 77 | 78 | [target.'cfg(unix)'.dependencies] 79 | exec = "0.3" 80 | 81 | [build-dependencies] 82 | anyhow = "1.0" 83 | 84 | [target.'cfg(windows)'.build-dependencies] 85 | tauri-winres = "0.3" 86 | -------------------------------------------------------------------------------- /objdiff-gui/assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-gui/assets/icon.ico -------------------------------------------------------------------------------- /objdiff-gui/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-gui/assets/icon.png -------------------------------------------------------------------------------- /objdiff-gui/assets/icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/encounter/objdiff/f58616b6dd6fcf7fa0047105a84bf1540eab9d35/objdiff-gui/assets/icon_64.png -------------------------------------------------------------------------------- /objdiff-gui/build.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | fn main() -> Result<()> { 4 | #[cfg(windows)] 5 | { 6 | let mut res = tauri_winres::WindowsResource::new(); 7 | res.set_icon("assets/icon.ico"); 8 | res.set_language(0x0409); // US English 9 | res.compile()?; 10 | } 11 | Ok(()) 12 | } 13 | -------------------------------------------------------------------------------- /objdiff-gui/src/config.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use globset::Glob; 3 | use objdiff_core::config::{DEFAULT_WATCH_PATTERNS, try_project_config}; 4 | use typed_path::{Utf8UnixComponent, Utf8UnixPath}; 5 | 6 | use crate::app::{AppState, ObjectConfig}; 7 | 8 | #[derive(Clone)] 9 | pub enum ProjectObjectNode { 10 | Unit(String, usize), 11 | Dir(String, Vec), 12 | } 13 | 14 | fn join_single_dir_entries(nodes: &mut Vec) { 15 | for node in nodes { 16 | if let ProjectObjectNode::Dir(my_name, my_nodes) = node { 17 | join_single_dir_entries(my_nodes); 18 | // If this directory consists of a single sub-directory... 19 | if let [ProjectObjectNode::Dir(sub_name, sub_nodes)] = &mut my_nodes[..] { 20 | // ... join the two names with a path separator and eliminate the layer 21 | *my_name += "/"; 22 | *my_name += sub_name; 23 | *my_nodes = std::mem::take(sub_nodes); 24 | } 25 | } 26 | } 27 | } 28 | 29 | fn find_dir<'a>( 30 | name: &str, 31 | nodes: &'a mut Vec, 32 | ) -> &'a mut Vec { 33 | if let Some(index) = nodes 34 | .iter() 35 | .position(|node| matches!(node, ProjectObjectNode::Dir(dir_name, _) if dir_name == name)) 36 | { 37 | if let ProjectObjectNode::Dir(_, children) = &mut nodes[index] { 38 | return children; 39 | } 40 | } else { 41 | nodes.push(ProjectObjectNode::Dir(name.to_string(), vec![])); 42 | if let Some(ProjectObjectNode::Dir(_, children)) = nodes.last_mut() { 43 | return children; 44 | } 45 | } 46 | unreachable!(); 47 | } 48 | 49 | fn build_nodes(units: &mut [ObjectConfig]) -> Vec { 50 | let mut nodes = vec![]; 51 | for (idx, unit) in units.iter_mut().enumerate() { 52 | let mut out_nodes = &mut nodes; 53 | let path = Utf8UnixPath::new(&unit.name); 54 | if let Some(parent) = path.parent() { 55 | for component in parent.components() { 56 | if let Utf8UnixComponent::Normal(name) = component { 57 | out_nodes = find_dir(name, out_nodes); 58 | } 59 | } 60 | } 61 | let filename = path.file_name().unwrap().to_string(); 62 | out_nodes.push(ProjectObjectNode::Unit(filename, idx)); 63 | } 64 | // Within the top-level module directories, join paths. Leave the 65 | // top-level name intact though since it's the module name. 66 | for node in &mut nodes { 67 | if let ProjectObjectNode::Dir(_, sub_nodes) = node { 68 | join_single_dir_entries(sub_nodes); 69 | } 70 | } 71 | 72 | nodes 73 | } 74 | 75 | pub fn load_project_config(state: &mut AppState) -> Result<()> { 76 | let Some(project_dir) = &state.config.project_dir else { 77 | return Ok(()); 78 | }; 79 | if let Some((result, info)) = try_project_config(project_dir.as_ref()) { 80 | let project_config = result?; 81 | state.config.custom_make = project_config.custom_make.clone(); 82 | state.config.custom_args = project_config.custom_args.clone(); 83 | state.config.target_obj_dir = project_config 84 | .target_dir 85 | .as_deref() 86 | .map(|p| project_dir.join(p.with_platform_encoding())); 87 | state.config.base_obj_dir = project_config 88 | .base_dir 89 | .as_deref() 90 | .map(|p| project_dir.join(p.with_platform_encoding())); 91 | state.config.build_base = project_config.build_base.unwrap_or(true); 92 | state.config.build_target = project_config.build_target.unwrap_or(false); 93 | if let Some(watch_patterns) = &project_config.watch_patterns { 94 | state.config.watch_patterns = watch_patterns 95 | .iter() 96 | .map(|s| Glob::new(s)) 97 | .collect::, globset::Error>>()?; 98 | } else { 99 | state.config.watch_patterns = 100 | DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(); 101 | } 102 | state.watcher_change = true; 103 | state.objects = project_config 104 | .units 105 | .as_deref() 106 | .unwrap_or_default() 107 | .iter() 108 | .map(|o| { 109 | ObjectConfig::new( 110 | o, 111 | project_dir, 112 | state.config.target_obj_dir.as_deref(), 113 | state.config.base_obj_dir.as_deref(), 114 | ) 115 | }) 116 | .collect::>(); 117 | state.object_nodes = build_nodes(&mut state.objects); 118 | state.current_project_config = Some(project_config); 119 | state.project_config_info = Some(info); 120 | 121 | // Reload selected object 122 | if let Some(selected_obj) = &state.config.selected_obj { 123 | if let Some(obj) = state.objects.iter().find(|o| o.name == selected_obj.name) { 124 | state.set_selected_obj(obj.clone()); 125 | } else { 126 | state.clear_selected_obj(); 127 | } 128 | } 129 | } 130 | Ok(()) 131 | } 132 | -------------------------------------------------------------------------------- /objdiff-gui/src/fonts/matching.rs: -------------------------------------------------------------------------------- 1 | // font-kit/src/matching.rs 2 | // 3 | // Copyright © 2018 The Pathfinder Project Developers. 4 | // 5 | // Licensed under the Apache License, Version 2.0 or the MIT license 7 | // , at your 8 | // option. This file may not be copied, modified, or distributed 9 | // except according to those terms. 10 | 11 | //! Determines the closest font matching a description per the CSS Fonts Level 3 specification. 12 | 13 | use float_ord::FloatOrd; 14 | use font_kit::{ 15 | error::SelectionError, 16 | properties::{Properties, Stretch, Style, Weight}, 17 | }; 18 | 19 | /// This follows CSS Fonts Level 3 § 5.2 [1]. 20 | /// 21 | /// https://drafts.csswg.org/css-fonts-3/#font-style-matching 22 | pub fn find_best_match( 23 | candidates: &[Properties], 24 | query: &Properties, 25 | ) -> Result { 26 | // Step 4. 27 | let mut matching_set: Vec = (0..candidates.len()).collect(); 28 | if matching_set.is_empty() { 29 | return Err(SelectionError::NotFound); 30 | } 31 | 32 | // Step 4a (`font-stretch`). 33 | let matching_stretch = if matching_set 34 | .iter() 35 | .any(|&index| candidates[index].stretch == query.stretch) 36 | { 37 | // Exact match. 38 | query.stretch 39 | } else if query.stretch <= Stretch::NORMAL { 40 | // Closest width, first checking narrower values and then wider values. 41 | match matching_set 42 | .iter() 43 | .filter(|&&index| candidates[index].stretch < query.stretch) 44 | .min_by_key(|&&index| FloatOrd(query.stretch.0 - candidates[index].stretch.0)) 45 | { 46 | Some(&matching_index) => candidates[matching_index].stretch, 47 | None => { 48 | let matching_index = *matching_set 49 | .iter() 50 | .min_by_key(|&&index| FloatOrd(candidates[index].stretch.0 - query.stretch.0)) 51 | .unwrap(); 52 | candidates[matching_index].stretch 53 | } 54 | } 55 | } else { 56 | // Closest width, first checking wider values and then narrower values. 57 | match matching_set 58 | .iter() 59 | .filter(|&&index| candidates[index].stretch > query.stretch) 60 | .min_by_key(|&&index| FloatOrd(candidates[index].stretch.0 - query.stretch.0)) 61 | { 62 | Some(&matching_index) => candidates[matching_index].stretch, 63 | None => { 64 | let matching_index = *matching_set 65 | .iter() 66 | .min_by_key(|&&index| FloatOrd(query.stretch.0 - candidates[index].stretch.0)) 67 | .unwrap(); 68 | candidates[matching_index].stretch 69 | } 70 | } 71 | }; 72 | matching_set.retain(|&index| candidates[index].stretch == matching_stretch); 73 | 74 | // Step 4b (`font-style`). 75 | let style_preference = match query.style { 76 | Style::Italic => [Style::Italic, Style::Oblique, Style::Normal], 77 | Style::Oblique => [Style::Oblique, Style::Italic, Style::Normal], 78 | Style::Normal => [Style::Normal, Style::Oblique, Style::Italic], 79 | }; 80 | let matching_style = *style_preference 81 | .iter() 82 | .find(|&query_style| { 83 | matching_set.iter().any(|&index| candidates[index].style == *query_style) 84 | }) 85 | .unwrap(); 86 | matching_set.retain(|&index| candidates[index].style == matching_style); 87 | 88 | // Step 4c (`font-weight`). 89 | // 90 | // The spec doesn't say what to do if the weight is between 400 and 500 exclusive, so we 91 | // just use 450 as the cutoff. 92 | let matching_weight = 93 | if matching_set.iter().any(|&index| candidates[index].weight == query.weight) { 94 | query.weight 95 | } else if query.weight >= Weight(400.0) 96 | && query.weight < Weight(450.0) 97 | && matching_set.iter().any(|&index| candidates[index].weight == Weight(500.0)) 98 | { 99 | // Check 500 first. 100 | Weight(500.0) 101 | } else if query.weight >= Weight(450.0) 102 | && query.weight <= Weight(500.0) 103 | && matching_set.iter().any(|&index| candidates[index].weight == Weight(400.0)) 104 | { 105 | // Check 400 first. 106 | Weight(400.0) 107 | } else if query.weight <= Weight(500.0) { 108 | // Closest weight, first checking thinner values and then fatter ones. 109 | match matching_set 110 | .iter() 111 | .filter(|&&index| candidates[index].weight <= query.weight) 112 | .min_by_key(|&&index| FloatOrd(query.weight.0 - candidates[index].weight.0)) 113 | { 114 | Some(&matching_index) => candidates[matching_index].weight, 115 | None => { 116 | let matching_index = *matching_set 117 | .iter() 118 | .min_by_key(|&&index| FloatOrd(candidates[index].weight.0 - query.weight.0)) 119 | .unwrap(); 120 | candidates[matching_index].weight 121 | } 122 | } 123 | } else { 124 | // Closest weight, first checking fatter values and then thinner ones. 125 | match matching_set 126 | .iter() 127 | .filter(|&&index| candidates[index].weight >= query.weight) 128 | .min_by_key(|&&index| FloatOrd(candidates[index].weight.0 - query.weight.0)) 129 | { 130 | Some(&matching_index) => candidates[matching_index].weight, 131 | None => { 132 | let matching_index = *matching_set 133 | .iter() 134 | .min_by_key(|&&index| FloatOrd(query.weight.0 - candidates[index].weight.0)) 135 | .unwrap(); 136 | candidates[matching_index].weight 137 | } 138 | } 139 | }; 140 | matching_set.retain(|&index| candidates[index].weight == matching_weight); 141 | 142 | // Step 4d concerns `font-size`, but fonts in `font-kit` are unsized, so we ignore that. 143 | 144 | // Return the result. 145 | matching_set.into_iter().next().ok_or(SelectionError::NotFound) 146 | } 147 | -------------------------------------------------------------------------------- /objdiff-gui/src/fonts/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod matching; 2 | 3 | use std::{borrow::Cow, fs, sync::Arc}; 4 | 5 | use anyhow::{Context, Result}; 6 | 7 | use crate::fonts::matching::find_best_match; 8 | 9 | pub struct LoadedFontFamily { 10 | pub family_name: String, 11 | pub fonts: Vec, 12 | pub handles: Vec, 13 | // pub properties: Vec, 14 | pub default_index: usize, 15 | } 16 | 17 | pub struct LoadedFont { 18 | // pub font_name: String, 19 | pub font_data: egui::FontData, 20 | } 21 | 22 | pub fn load_font_family( 23 | source: &font_kit::source::SystemSource, 24 | name: &str, 25 | ) -> Option { 26 | let family_handle = source.select_family_by_name(name).ok()?; 27 | if family_handle.fonts().is_empty() { 28 | log::warn!("No fonts found for family '{}'", name); 29 | return None; 30 | } 31 | let handles = family_handle.fonts().to_vec(); 32 | let mut loaded = Vec::with_capacity(handles.len()); 33 | for handle in handles.iter() { 34 | match font_kit::loaders::default::Font::from_handle(handle) { 35 | Ok(font) => loaded.push(font), 36 | Err(err) => { 37 | log::warn!("Failed to load font '{}': {}", name, err); 38 | return None; 39 | } 40 | } 41 | } 42 | let properties = loaded.iter().map(|f| f.properties()).collect::>(); 43 | let default_index = 44 | find_best_match(&properties, &font_kit::properties::Properties::new()).unwrap_or(0); 45 | let font_family_name = 46 | loaded.first().map(|f| f.family_name()).unwrap_or_else(|| name.to_string()); 47 | Some(LoadedFontFamily { 48 | family_name: font_family_name, 49 | fonts: loaded, 50 | handles, 51 | // properties, 52 | default_index, 53 | }) 54 | } 55 | 56 | pub fn load_font(handle: &font_kit::handle::Handle) -> Result { 57 | // let loaded = font_kit::loaders::default::Font::from_handle(handle)?; 58 | let data = match handle { 59 | font_kit::handle::Handle::Memory { bytes, font_index } => egui::FontData { 60 | font: Cow::Owned(bytes.to_vec()), 61 | index: *font_index, 62 | tweak: Default::default(), 63 | }, 64 | font_kit::handle::Handle::Path { path, font_index } => { 65 | let vec = fs::read(path).with_context(|| { 66 | format!("Failed to load font '{}' (index {})", path.display(), font_index) 67 | })?; 68 | egui::FontData { font: Cow::Owned(vec), index: *font_index, tweak: Default::default() } 69 | } 70 | }; 71 | Ok(LoadedFont { 72 | // font_name: loaded.full_name(), 73 | font_data: data, 74 | }) 75 | } 76 | 77 | pub fn load_font_if_needed( 78 | ctx: &egui::Context, 79 | source: &font_kit::source::SystemSource, 80 | font_id: &egui::FontId, 81 | base_family: egui::FontFamily, 82 | fonts: &mut egui::FontDefinitions, 83 | ) -> Result<()> { 84 | if fonts.families.contains_key(&font_id.family) { 85 | return Ok(()); 86 | } 87 | let family_name = match &font_id.family { 88 | egui::FontFamily::Proportional | egui::FontFamily::Monospace => return Ok(()), 89 | egui::FontFamily::Name(v) => v, 90 | }; 91 | let family = load_font_family(source, family_name) 92 | .with_context(|| format!("Failed to load font family '{}'", family_name))?; 93 | let default_fonts = fonts.families.get(&base_family).cloned().unwrap_or_default(); 94 | // FIXME clean up 95 | let default_font_ref = family.fonts.get(family.default_index).unwrap(); 96 | let default_font = family.handles.get(family.default_index).unwrap(); 97 | let default_font_data = load_font(default_font)?; 98 | log::info!("Loaded font family '{}'", family.family_name); 99 | fonts.font_data.insert(default_font_ref.full_name(), Arc::new(default_font_data.font_data)); 100 | fonts 101 | .families 102 | .entry(egui::FontFamily::Name(Arc::from(family.family_name))) 103 | .or_insert_with(|| default_fonts) 104 | .insert(0, default_font_ref.full_name()); 105 | ctx.set_fonts(fonts.clone()); 106 | Ok(()) 107 | } 108 | -------------------------------------------------------------------------------- /objdiff-gui/src/hotkeys.rs: -------------------------------------------------------------------------------- 1 | use egui::{ 2 | Context, Key, KeyboardShortcut, Modifiers, PointerButton, style::ScrollAnimation, vec2, 3 | }; 4 | 5 | fn any_widget_focused(ctx: &Context) -> bool { ctx.memory(|mem| mem.focused().is_some()) } 6 | 7 | pub fn enter_pressed(ctx: &Context) -> bool { 8 | if any_widget_focused(ctx) { 9 | return false; 10 | } 11 | ctx.input_mut(|i| { 12 | i.key_pressed(Key::Enter) 13 | || i.key_pressed(Key::Space) 14 | || i.pointer.button_pressed(PointerButton::Extra2) 15 | }) 16 | } 17 | 18 | pub fn back_pressed(ctx: &Context) -> bool { 19 | if any_widget_focused(ctx) { 20 | return false; 21 | } 22 | ctx.input_mut(|i| { 23 | i.key_pressed(Key::Backspace) 24 | || i.key_pressed(Key::Escape) 25 | || i.pointer.button_pressed(PointerButton::Extra1) 26 | }) 27 | } 28 | 29 | pub fn up_pressed(ctx: &Context) -> bool { 30 | if any_widget_focused(ctx) { 31 | return false; 32 | } 33 | ctx.input_mut(|i| i.key_pressed(Key::ArrowUp) || i.key_pressed(Key::W)) 34 | } 35 | 36 | pub fn down_pressed(ctx: &Context) -> bool { 37 | if any_widget_focused(ctx) { 38 | return false; 39 | } 40 | ctx.input_mut(|i| i.key_pressed(Key::ArrowDown) || i.key_pressed(Key::S)) 41 | } 42 | 43 | pub fn page_up_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::PageUp)) } 44 | 45 | pub fn page_down_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::PageDown)) } 46 | 47 | pub fn home_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::Home)) } 48 | 49 | pub fn end_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::End)) } 50 | 51 | pub fn check_scroll_hotkeys(ui: &mut egui::Ui, include_small_increments: bool) { 52 | let ui_height = ui.available_rect_before_wrap().height(); 53 | if up_pressed(ui.ctx()) && include_small_increments { 54 | ui.scroll_with_delta_animation(vec2(0.0, ui_height / 10.0), ScrollAnimation::none()); 55 | } else if down_pressed(ui.ctx()) && include_small_increments { 56 | ui.scroll_with_delta_animation(vec2(0.0, -ui_height / 10.0), ScrollAnimation::none()); 57 | } else if page_up_pressed(ui.ctx()) { 58 | ui.scroll_with_delta_animation(vec2(0.0, ui_height), ScrollAnimation::none()); 59 | } else if page_down_pressed(ui.ctx()) { 60 | ui.scroll_with_delta_animation(vec2(0.0, -ui_height), ScrollAnimation::none()); 61 | } else if home_pressed(ui.ctx()) { 62 | ui.scroll_with_delta_animation(vec2(0.0, f32::INFINITY), ScrollAnimation::none()); 63 | } else if end_pressed(ui.ctx()) { 64 | ui.scroll_with_delta_animation(vec2(0.0, -f32::INFINITY), ScrollAnimation::none()); 65 | } 66 | } 67 | 68 | pub fn consume_up_key(ctx: &Context) -> bool { 69 | if any_widget_focused(ctx) { 70 | return false; 71 | } 72 | ctx.input_mut(|i| { 73 | i.consume_key(Modifiers::NONE, Key::ArrowUp) || i.consume_key(Modifiers::NONE, Key::W) 74 | }) 75 | } 76 | 77 | pub fn consume_down_key(ctx: &Context) -> bool { 78 | if any_widget_focused(ctx) { 79 | return false; 80 | } 81 | ctx.input_mut(|i| { 82 | i.consume_key(Modifiers::NONE, Key::ArrowDown) || i.consume_key(Modifiers::NONE, Key::S) 83 | }) 84 | } 85 | 86 | const OBJECT_FILTER_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::F); 87 | 88 | pub fn consume_object_filter_shortcut(ctx: &Context) -> bool { 89 | ctx.input_mut(|i| i.consume_shortcut(&OBJECT_FILTER_SHORTCUT)) 90 | } 91 | 92 | const SYMBOL_FILTER_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::S); 93 | 94 | pub fn consume_symbol_filter_shortcut(ctx: &Context) -> bool { 95 | ctx.input_mut(|i| i.consume_shortcut(&SYMBOL_FILTER_SHORTCUT)) 96 | } 97 | 98 | const CHANGE_TARGET_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::T); 99 | 100 | pub fn consume_change_target_shortcut(ctx: &Context) -> bool { 101 | ctx.input_mut(|i| i.consume_shortcut(&CHANGE_TARGET_SHORTCUT)) 102 | } 103 | 104 | const CHANGE_BASE_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::B); 105 | 106 | pub fn consume_change_base_shortcut(ctx: &Context) -> bool { 107 | ctx.input_mut(|i| i.consume_shortcut(&CHANGE_BASE_SHORTCUT)) 108 | } 109 | -------------------------------------------------------------------------------- /objdiff-gui/src/jobs.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync::Arc, 3 | task::{Wake, Waker}, 4 | }; 5 | 6 | use anyhow::{Result, bail}; 7 | use jobs::create_scratch; 8 | use objdiff_core::{ 9 | build::BuildConfig, 10 | diff::MappingConfig, 11 | jobs, 12 | jobs::{Job, JobQueue, check_update::CheckUpdateConfig, objdiff, update::UpdateConfig}, 13 | }; 14 | 15 | use crate::{ 16 | app::{AppConfig, AppState}, 17 | update::{BIN_NAME_NEW, BIN_NAME_OLD, build_updater}, 18 | }; 19 | 20 | struct EguiWaker(egui::Context); 21 | 22 | impl Wake for EguiWaker { 23 | fn wake(self: Arc) { self.0.request_repaint(); } 24 | 25 | fn wake_by_ref(self: &Arc) { self.0.request_repaint(); } 26 | } 27 | 28 | pub fn egui_waker(ctx: &egui::Context) -> Waker { Waker::from(Arc::new(EguiWaker(ctx.clone()))) } 29 | 30 | pub fn is_create_scratch_available(config: &AppConfig) -> bool { 31 | let Some(selected_obj) = &config.selected_obj else { 32 | return false; 33 | }; 34 | selected_obj.target_path.is_some() && selected_obj.scratch.is_some() 35 | } 36 | 37 | pub fn start_create_scratch( 38 | ctx: &egui::Context, 39 | jobs: &mut JobQueue, 40 | state: &AppState, 41 | function_name: String, 42 | ) { 43 | match create_scratch_config(state, function_name) { 44 | Ok(config) => { 45 | jobs.push_once(Job::CreateScratch, || { 46 | create_scratch::start_create_scratch(egui_waker(ctx), config) 47 | }); 48 | } 49 | Err(err) => { 50 | log::error!("Failed to create scratch config: {err}"); 51 | } 52 | } 53 | } 54 | 55 | fn create_scratch_config( 56 | state: &AppState, 57 | function_name: String, 58 | ) -> Result { 59 | let Some(selected_obj) = &state.config.selected_obj else { 60 | bail!("No object selected"); 61 | }; 62 | let Some(target_path) = &selected_obj.target_path else { 63 | bail!("No target path for {}", selected_obj.name); 64 | }; 65 | let Some(scratch_config) = &selected_obj.scratch else { 66 | bail!("No scratch configuration for {}", selected_obj.name); 67 | }; 68 | Ok(create_scratch::CreateScratchConfig { 69 | build_config: BuildConfig::from(&state.config), 70 | context_path: scratch_config.ctx_path.clone(), 71 | build_context: scratch_config.build_ctx.unwrap_or(false), 72 | compiler: scratch_config.compiler.clone().unwrap_or_default(), 73 | platform: scratch_config.platform.clone().unwrap_or_default(), 74 | compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(), 75 | function_name, 76 | target_obj: target_path.clone(), 77 | preset_id: scratch_config.preset_id, 78 | }) 79 | } 80 | 81 | impl From<&AppConfig> for BuildConfig { 82 | fn from(config: &AppConfig) -> Self { 83 | Self { 84 | project_dir: config.project_dir.clone(), 85 | custom_make: config.custom_make.clone(), 86 | custom_args: config.custom_args.clone(), 87 | selected_wsl_distro: config.selected_wsl_distro.clone(), 88 | } 89 | } 90 | } 91 | 92 | pub fn create_objdiff_config(state: &AppState) -> objdiff::ObjDiffConfig { 93 | objdiff::ObjDiffConfig { 94 | build_config: BuildConfig::from(&state.config), 95 | build_base: state.config.build_base, 96 | build_target: state.config.build_target, 97 | target_path: state 98 | .config 99 | .selected_obj 100 | .as_ref() 101 | .and_then(|obj| obj.target_path.as_ref()) 102 | .cloned(), 103 | base_path: state 104 | .config 105 | .selected_obj 106 | .as_ref() 107 | .and_then(|obj| obj.base_path.as_ref()) 108 | .cloned(), 109 | diff_obj_config: state.config.diff_obj_config.clone(), 110 | mapping_config: MappingConfig { 111 | mappings: state 112 | .config 113 | .selected_obj 114 | .as_ref() 115 | .map(|obj| &obj.symbol_mappings) 116 | .cloned() 117 | .unwrap_or_default(), 118 | selecting_left: state.selecting_left.clone(), 119 | selecting_right: state.selecting_right.clone(), 120 | }, 121 | } 122 | } 123 | 124 | pub fn start_build(ctx: &egui::Context, jobs: &mut JobQueue, config: objdiff::ObjDiffConfig) { 125 | jobs.push_once(Job::ObjDiff, || objdiff::start_build(egui_waker(ctx), config)); 126 | } 127 | 128 | pub fn start_check_update(ctx: &egui::Context, jobs: &mut JobQueue) { 129 | jobs.push_once(Job::Update, || { 130 | jobs::check_update::start_check_update(egui_waker(ctx), CheckUpdateConfig { 131 | build_updater, 132 | bin_names: vec![BIN_NAME_NEW.to_string(), BIN_NAME_OLD.to_string()], 133 | }) 134 | }); 135 | } 136 | 137 | pub fn start_update(ctx: &egui::Context, jobs: &mut JobQueue, bin_name: String) { 138 | jobs.push_once(Job::Update, || { 139 | jobs::update::start_update(egui_waker(ctx), UpdateConfig { build_updater, bin_name }) 140 | }); 141 | } 142 | -------------------------------------------------------------------------------- /objdiff-gui/src/update.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use cfg_if::cfg_if; 3 | use const_format::formatcp; 4 | use objdiff_core::jobs::update::self_update; 5 | use self_update::{cargo_crate_version, update::ReleaseUpdate}; 6 | 7 | pub const OS: &str = std::env::consts::OS; 8 | cfg_if! { 9 | if #[cfg(target_arch = "aarch64")] { 10 | cfg_if! { 11 | if #[cfg(any(windows, target_os = "macos"))] { 12 | pub const ARCH: &str = "arm64"; 13 | } else { 14 | pub const ARCH: &str = std::env::consts::ARCH; 15 | } 16 | } 17 | } else if #[cfg(target_arch = "arm")] { 18 | pub const ARCH: &str = "armv7l"; 19 | } else { 20 | pub const ARCH: &str = std::env::consts::ARCH; 21 | } 22 | } 23 | pub const GITHUB_USER: &str = "encounter"; 24 | pub const GITHUB_REPO: &str = "objdiff"; 25 | pub const BIN_NAME_NEW: &str = 26 | formatcp!("objdiff-gui-{}-{}{}", OS, ARCH, std::env::consts::EXE_SUFFIX); 27 | pub const BIN_NAME_OLD: &str = formatcp!("objdiff-{}-{}{}", OS, ARCH, std::env::consts::EXE_SUFFIX); 28 | pub const RELEASE_URL: &str = 29 | formatcp!("https://github.com/{}/{}/releases/latest", GITHUB_USER, GITHUB_REPO); 30 | 31 | pub fn build_updater() -> Result> { 32 | Ok(self_update::backends::github::Update::configure() 33 | .repo_owner(GITHUB_USER) 34 | .repo_name(GITHUB_REPO) 35 | // bin_name is required, but unused? 36 | .bin_name(BIN_NAME_NEW) 37 | .no_confirm(true) 38 | .show_output(false) 39 | .current_version(cargo_crate_version!()) 40 | .build()?) 41 | } 42 | -------------------------------------------------------------------------------- /objdiff-gui/src/views/column_layout.rs: -------------------------------------------------------------------------------- 1 | use egui::{Align, Layout, Sense, Vec2}; 2 | use egui_extras::{Column, Size, StripBuilder, TableBuilder, TableRow}; 3 | 4 | pub fn render_header( 5 | ui: &mut egui::Ui, 6 | available_width: f32, 7 | num_columns: usize, 8 | mut add_contents: impl FnMut(&mut egui::Ui, usize), 9 | ) { 10 | let column_width = available_width / num_columns as f32; 11 | ui.allocate_ui_with_layout( 12 | Vec2 { x: available_width, y: 100.0 }, 13 | Layout::left_to_right(Align::Min), 14 | |ui| { 15 | ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); 16 | for i in 0..num_columns { 17 | ui.allocate_ui_with_layout( 18 | Vec2 { x: column_width, y: 100.0 }, 19 | Layout::top_down(Align::Min), 20 | |ui| { 21 | ui.set_width(column_width); 22 | add_contents(ui, i); 23 | }, 24 | ); 25 | } 26 | }, 27 | ); 28 | ui.separator(); 29 | } 30 | 31 | pub fn render_table( 32 | ui: &mut egui::Ui, 33 | available_width: f32, 34 | num_columns: usize, 35 | row_height: f32, 36 | total_rows: usize, 37 | mut add_contents: impl FnMut(&mut TableRow, usize), 38 | ) { 39 | ui.style_mut().interaction.selectable_labels = false; 40 | let column_width = available_width / num_columns as f32; 41 | let available_height = ui.available_height(); 42 | let table = TableBuilder::new(ui) 43 | .striped(false) 44 | .cell_layout(Layout::left_to_right(Align::Min)) 45 | .columns(Column::exact(column_width).clip(true), num_columns) 46 | .resizable(false) 47 | .auto_shrink([false, false]) 48 | .min_scrolled_height(available_height) 49 | .sense(Sense::click()); 50 | table.body(|body| { 51 | body.rows(row_height, total_rows, |mut row| { 52 | row.set_hovered(false); // Disable hover effect 53 | for i in 0..num_columns { 54 | add_contents(&mut row, i); 55 | } 56 | }); 57 | }); 58 | } 59 | 60 | pub fn render_strips( 61 | ui: &mut egui::Ui, 62 | available_width: f32, 63 | num_columns: usize, 64 | mut add_contents: impl FnMut(&mut egui::Ui, usize), 65 | ) { 66 | let column_width = available_width / num_columns as f32; 67 | StripBuilder::new(ui).size(Size::remainder()).clip(true).vertical(|mut strip| { 68 | strip.strip(|builder| { 69 | builder.sizes(Size::exact(column_width), num_columns).clip(true).horizontal( 70 | |mut strip| { 71 | for i in 0..num_columns { 72 | strip.cell(|ui| { 73 | ui.push_id(i, |ui| { 74 | add_contents(ui, i); 75 | }); 76 | }); 77 | } 78 | }, 79 | ); 80 | }); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /objdiff-gui/src/views/debug.rs: -------------------------------------------------------------------------------- 1 | use crate::views::{appearance::Appearance, frame_history::FrameHistory}; 2 | 3 | pub fn debug_window( 4 | ctx: &egui::Context, 5 | show: &mut bool, 6 | frame_history: &mut FrameHistory, 7 | appearance: &Appearance, 8 | ) { 9 | egui::Window::new("Debug").open(show).show(ctx, |ui| { 10 | debug_ui(ui, frame_history, appearance); 11 | }); 12 | } 13 | 14 | fn debug_ui(ui: &mut egui::Ui, frame_history: &mut FrameHistory, _appearance: &Appearance) { 15 | if ui.button("Clear memory").clicked() { 16 | ui.memory_mut(|m| *m = Default::default()); 17 | } 18 | ui.label(format!("Repainting the UI each frame. FPS: {:.1}", frame_history.fps())); 19 | frame_history.ui(ui); 20 | } 21 | -------------------------------------------------------------------------------- /objdiff-gui/src/views/demangle.rs: -------------------------------------------------------------------------------- 1 | use egui::TextStyle; 2 | 3 | use crate::views::appearance::Appearance; 4 | 5 | #[derive(Default)] 6 | pub struct DemangleViewState { 7 | pub text: String, 8 | } 9 | 10 | pub fn demangle_window( 11 | ctx: &egui::Context, 12 | show: &mut bool, 13 | state: &mut DemangleViewState, 14 | appearance: &Appearance, 15 | ) { 16 | egui::Window::new("Demangle").open(show).show(ctx, |ui| { 17 | ui.text_edit_singleline(&mut state.text); 18 | ui.add_space(10.0); 19 | if let Some(demangled) = cwdemangle::demangle(&state.text, &Default::default()) { 20 | ui.scope(|ui| { 21 | ui.style_mut().override_text_style = Some(TextStyle::Monospace); 22 | ui.colored_label(appearance.replace_color, &demangled); 23 | }); 24 | if ui.button("Copy").clicked() { 25 | ctx.copy_text(demangled); 26 | } 27 | } else { 28 | ui.scope(|ui| { 29 | ui.style_mut().override_text_style = Some(TextStyle::Monospace); 30 | ui.colored_label(appearance.replace_color, "[invalid]"); 31 | }); 32 | } 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /objdiff-gui/src/views/extab_diff.rs: -------------------------------------------------------------------------------- 1 | use egui::ScrollArea; 2 | use objdiff_core::{ 3 | arch::ppc::ExceptionInfo, 4 | obj::{Object, Symbol}, 5 | }; 6 | 7 | use crate::views::{appearance::Appearance, function_diff::FunctionDiffContext}; 8 | 9 | fn decode_extab(extab: &ExceptionInfo) -> String { 10 | let mut text = String::from(""); 11 | 12 | let mut dtor_names: Vec = vec![]; 13 | for dtor in &extab.dtors { 14 | //For each function name, use the demangled name by default, 15 | //and if not available fallback to the original name 16 | let name: String = match &dtor.demangled_name { 17 | Some(demangled_name) => demangled_name.to_string(), 18 | None => dtor.name.clone(), 19 | }; 20 | dtor_names.push(name); 21 | } 22 | if let Some(decoded) = extab.data.to_string(dtor_names) { 23 | text += decoded.as_str(); 24 | } 25 | 26 | text 27 | } 28 | 29 | fn find_extab_entry<'a>(_obj: &'a Object, _symbol: &Symbol) -> Option<&'a ExceptionInfo> { 30 | // TODO 31 | // obj.arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol)) 32 | None 33 | } 34 | 35 | fn extab_text_ui( 36 | ui: &mut egui::Ui, 37 | ctx: FunctionDiffContext<'_>, 38 | symbol: &Symbol, 39 | appearance: &Appearance, 40 | ) -> Option<()> { 41 | if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) { 42 | let text = decode_extab(extab_entry); 43 | ui.colored_label(appearance.replace_color, &text); 44 | return Some(()); 45 | } 46 | 47 | None 48 | } 49 | 50 | pub(crate) fn extab_ui( 51 | ui: &mut egui::Ui, 52 | ctx: FunctionDiffContext<'_>, 53 | appearance: &Appearance, 54 | _column: usize, 55 | ) { 56 | ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { 57 | ui.scope(|ui| { 58 | ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); 59 | ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); 60 | 61 | if let Some(symbol) = 62 | ctx.symbol_ref.and_then(|symbol_ref| ctx.obj.symbols.get(symbol_ref)) 63 | { 64 | extab_text_ui(ui, ctx, symbol, appearance); 65 | } 66 | }); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /objdiff-gui/src/views/file.rs: -------------------------------------------------------------------------------- 1 | use std::{future::Future, path::PathBuf, pin::Pin, thread::JoinHandle}; 2 | 3 | use objdiff_core::config::path::check_path_buf; 4 | use pollster::FutureExt; 5 | use rfd::FileHandle; 6 | use typed_path::Utf8PlatformPathBuf; 7 | 8 | #[derive(Default)] 9 | pub enum FileDialogResult { 10 | #[default] 11 | None, 12 | ProjectDir(Utf8PlatformPathBuf), 13 | TargetDir(Utf8PlatformPathBuf), 14 | BaseDir(Utf8PlatformPathBuf), 15 | Object(Utf8PlatformPathBuf), 16 | } 17 | 18 | #[derive(Default)] 19 | pub struct FileDialogState { 20 | thread: Option>, 21 | } 22 | 23 | impl FileDialogState { 24 | pub fn queue(&mut self, init: InitCb, result_cb: ResultCb) 25 | where 26 | InitCb: FnOnce() -> Pin> + Send>>, 27 | ResultCb: FnOnce(Utf8PlatformPathBuf) -> FileDialogResult + Send + 'static, 28 | { 29 | if self.thread.is_some() { 30 | return; 31 | } 32 | let future = init(); 33 | self.thread = Some(std::thread::spawn(move || { 34 | if let Some(handle) = future.block_on() { 35 | let path = PathBuf::from(handle); 36 | check_path_buf(path).map(result_cb).unwrap_or(FileDialogResult::None) 37 | } else { 38 | FileDialogResult::None 39 | } 40 | })); 41 | } 42 | 43 | pub fn poll(&mut self) -> FileDialogResult { 44 | if let Some(thread) = &mut self.thread { 45 | if thread.is_finished() { 46 | self.thread.take().unwrap().join().unwrap_or(FileDialogResult::None) 47 | } else { 48 | FileDialogResult::None 49 | } 50 | } else { 51 | FileDialogResult::None 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /objdiff-gui/src/views/frame_history.rs: -------------------------------------------------------------------------------- 1 | // From https://github.com/emilk/egui/blob/e037489ac20a9e419715ae75d205a8baa117c3cf/crates/egui_demo_app/src/frame_history.rs 2 | // Copyright (c) 2018-2021 Emil Ernerfeldt 3 | // 4 | // Permission is hereby granted, free of charge, to any 5 | // person obtaining a copy of this software and associated 6 | // documentation files (the "Software"), to deal in the 7 | // Software without restriction, including without 8 | // limitation the rights to use, copy, modify, merge, 9 | // publish, distribute, sublicense, and/or sell copies of 10 | // the Software, and to permit persons to whom the Software 11 | // is furnished to do so, subject to the following 12 | // conditions: 13 | // 14 | // The above copyright notice and this permission notice 15 | // shall be included in all copies or substantial portions 16 | // of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 19 | // ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 20 | // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 21 | // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 22 | // SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 23 | // CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 25 | // IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 | // DEALINGS IN THE SOFTWARE. 27 | 28 | use egui::util::History; 29 | 30 | pub struct FrameHistory { 31 | frame_times: History, 32 | } 33 | 34 | impl Default for FrameHistory { 35 | fn default() -> Self { 36 | let max_age: f32 = 1.0; 37 | let max_len = (max_age * 300.0).round() as usize; 38 | Self { frame_times: History::new(0..max_len, max_age) } 39 | } 40 | } 41 | 42 | impl FrameHistory { 43 | // Called first 44 | pub fn on_new_frame(&mut self, now: f64, previous_frame_time: Option) { 45 | let previous_frame_time = previous_frame_time.unwrap_or_default(); 46 | if let Some(latest) = self.frame_times.latest_mut() { 47 | *latest = previous_frame_time; // rewrite history now that we know 48 | } 49 | self.frame_times.add(now, previous_frame_time); // projected 50 | } 51 | 52 | pub fn mean_frame_time(&self) -> f32 { self.frame_times.average().unwrap_or_default() } 53 | 54 | pub fn fps(&self) -> f32 { 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() } 55 | 56 | pub fn ui(&mut self, ui: &mut egui::Ui) { 57 | ui.label(format!("Mean CPU usage: {:.2} ms / frame", 1e3 * self.mean_frame_time())) 58 | .on_hover_text( 59 | "Includes egui layout and tessellation time.\n\ 60 | Does not include GPU usage, nor overhead for sending data to GPU.", 61 | ); 62 | egui::warn_if_debug_build(ui); 63 | 64 | egui::CollapsingHeader::new("📊 CPU usage history").default_open(false).show(ui, |ui| { 65 | self.graph(ui); 66 | }); 67 | } 68 | 69 | fn graph(&mut self, ui: &mut egui::Ui) -> egui::Response { 70 | use egui::*; 71 | 72 | ui.label("egui CPU usage history"); 73 | 74 | let history = &self.frame_times; 75 | 76 | // TODO(emilk): we should not use `slider_width` as default graph width. 77 | let height = ui.spacing().slider_width; 78 | let size = vec2(ui.available_size_before_wrap().x, height); 79 | let (rect, response) = ui.allocate_at_least(size, Sense::hover()); 80 | let style = ui.style().noninteractive(); 81 | 82 | let graph_top_cpu_usage = 0.010; 83 | let graph_rect = Rect::from_x_y_ranges(history.max_age()..=0.0, graph_top_cpu_usage..=0.0); 84 | let to_screen = emath::RectTransform::from_to(graph_rect, rect); 85 | 86 | let mut shapes = Vec::with_capacity(3 + 2 * history.len()); 87 | shapes.push(Shape::Rect(epaint::RectShape::new( 88 | rect, 89 | style.corner_radius, 90 | ui.visuals().extreme_bg_color, 91 | ui.style().noninteractive().bg_stroke, 92 | StrokeKind::Inside, 93 | ))); 94 | 95 | let rect = rect.shrink(4.0); 96 | let color = ui.visuals().text_color(); 97 | let line_stroke = Stroke::new(1.0, color); 98 | 99 | if let Some(pointer_pos) = response.hover_pos() { 100 | let y = pointer_pos.y; 101 | shapes.push(Shape::line_segment( 102 | [pos2(rect.left(), y), pos2(rect.right(), y)], 103 | line_stroke, 104 | )); 105 | let cpu_usage = to_screen.inverse().transform_pos(pointer_pos).y; 106 | let text = format!("{:.1} ms", 1e3 * cpu_usage); 107 | shapes.push(ui.fonts(|f| { 108 | Shape::text( 109 | f, 110 | pos2(rect.left(), y), 111 | egui::Align2::LEFT_BOTTOM, 112 | text, 113 | TextStyle::Monospace.resolve(ui.style()), 114 | color, 115 | ) 116 | })); 117 | } 118 | 119 | let circle_color = color; 120 | let radius = 2.0; 121 | let right_side_time = ui.input(|i| i.time); // Time at right side of screen 122 | 123 | for (time, cpu_usage) in history.iter() { 124 | let age = (right_side_time - time) as f32; 125 | let pos = to_screen.transform_pos_clamped(Pos2::new(age, cpu_usage)); 126 | 127 | shapes.push(Shape::line_segment([pos2(pos.x, rect.bottom()), pos], line_stroke)); 128 | 129 | if cpu_usage < graph_top_cpu_usage { 130 | shapes.push(Shape::circle_filled(pos, radius, circle_color)); 131 | } 132 | } 133 | 134 | ui.painter().extend(shapes); 135 | 136 | response 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /objdiff-gui/src/views/graphics.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{BufReader, BufWriter}, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use anyhow::Result; 8 | use egui::{Context, FontId, RichText, TextFormat, TextStyle, Window, text::LayoutJob}; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | use crate::views::{appearance::Appearance, frame_history::FrameHistory}; 12 | 13 | #[derive(Default)] 14 | pub struct GraphicsViewState { 15 | pub active_backend: String, 16 | pub active_device: String, 17 | pub graphics_config: GraphicsConfig, 18 | pub graphics_config_path: Option, 19 | pub should_relaunch: bool, 20 | } 21 | 22 | #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] 23 | pub enum GraphicsBackend { 24 | #[default] 25 | Auto, 26 | Vulkan, 27 | Metal, 28 | Dx12, 29 | OpenGL, 30 | } 31 | 32 | static ALL_BACKENDS: &[GraphicsBackend] = &[ 33 | GraphicsBackend::Auto, 34 | GraphicsBackend::Vulkan, 35 | GraphicsBackend::Metal, 36 | GraphicsBackend::Dx12, 37 | GraphicsBackend::OpenGL, 38 | ]; 39 | 40 | #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] 41 | pub struct GraphicsConfig { 42 | #[serde(default)] 43 | pub desired_backend: GraphicsBackend, 44 | } 45 | 46 | pub fn load_graphics_config(path: &Path) -> Result> { 47 | if !path.exists() { 48 | return Ok(None); 49 | } 50 | let file = BufReader::new(File::open(path)?); 51 | let config: GraphicsConfig = ron::de::from_reader(file)?; 52 | Ok(Some(config)) 53 | } 54 | 55 | pub fn save_graphics_config(path: &Path, config: &GraphicsConfig) -> Result<()> { 56 | let file = BufWriter::new(File::create(path)?); 57 | ron::ser::to_writer(file, config)?; 58 | Ok(()) 59 | } 60 | 61 | impl GraphicsBackend { 62 | pub fn is_supported(&self) -> bool { 63 | match self { 64 | GraphicsBackend::Auto => true, 65 | GraphicsBackend::Vulkan => { 66 | cfg!(all(feature = "wgpu", any(target_os = "windows", target_os = "linux"))) 67 | } 68 | GraphicsBackend::Metal => cfg!(all(feature = "wgpu", target_os = "macos")), 69 | GraphicsBackend::Dx12 => cfg!(all(feature = "wgpu", target_os = "windows")), 70 | GraphicsBackend::OpenGL => true, 71 | } 72 | } 73 | 74 | pub fn display_name(self) -> &'static str { 75 | match self { 76 | GraphicsBackend::Auto => "Auto", 77 | GraphicsBackend::Vulkan => "Vulkan", 78 | GraphicsBackend::Metal => "Metal", 79 | GraphicsBackend::Dx12 => "DirectX 12", 80 | GraphicsBackend::OpenGL => "OpenGL", 81 | } 82 | } 83 | } 84 | 85 | pub fn graphics_window( 86 | ctx: &Context, 87 | show: &mut bool, 88 | frame_history: &mut FrameHistory, 89 | state: &mut GraphicsViewState, 90 | appearance: &Appearance, 91 | ) { 92 | Window::new("Graphics").open(show).show(ctx, |ui| { 93 | ui.label("Graphics backend:"); 94 | ui.label( 95 | RichText::new(&state.active_backend) 96 | .color(appearance.emphasized_text_color) 97 | .text_style(TextStyle::Monospace), 98 | ); 99 | ui.label("Graphics device:"); 100 | ui.label( 101 | RichText::new(&state.active_device) 102 | .color(appearance.emphasized_text_color) 103 | .text_style(TextStyle::Monospace), 104 | ); 105 | ui.label(format!("FPS: {:.1}", frame_history.fps())); 106 | 107 | ui.separator(); 108 | let mut job = LayoutJob::default(); 109 | job.append( 110 | "WARNING: ", 111 | 0.0, 112 | TextFormat::simple(appearance.ui_font.clone(), appearance.delete_color), 113 | ); 114 | job.append( 115 | "Changing the graphics backend may cause the application\nto no longer start or display correctly. Use with caution!", 116 | 0.0, 117 | TextFormat::simple(appearance.ui_font.clone(), appearance.emphasized_text_color), 118 | ); 119 | if let Some(config_path) = &state.graphics_config_path { 120 | job.append( 121 | "\n\nDelete the following file to reset:\n", 122 | 0.0, 123 | TextFormat::simple(appearance.ui_font.clone(), appearance.emphasized_text_color), 124 | ); 125 | job.append( 126 | config_path.to_string_lossy().as_ref(), 127 | 0.0, 128 | TextFormat::simple( 129 | FontId { 130 | family: appearance.code_font.family.clone(), 131 | size: appearance.ui_font.size, 132 | }, 133 | appearance.emphasized_text_color, 134 | ), 135 | ); 136 | } 137 | job.append( 138 | "\n\nChanging the graphics backend will restart the application.", 139 | 0.0, 140 | TextFormat::simple(appearance.ui_font.clone(), appearance.replace_color), 141 | ); 142 | ui.label(job); 143 | 144 | ui.add_enabled_ui(state.graphics_config_path.is_some(), |ui| { 145 | ui.horizontal(|ui| { 146 | ui.label("Desired backend:"); 147 | for backend in ALL_BACKENDS.iter().copied().filter(GraphicsBackend::is_supported) { 148 | let selected = state.graphics_config.desired_backend == backend; 149 | if ui.selectable_label(selected, backend.display_name()).clicked() { 150 | let prev_backend = state.graphics_config.desired_backend; 151 | state.graphics_config.desired_backend = backend; 152 | match save_graphics_config( 153 | state.graphics_config_path.as_ref().unwrap(), 154 | &state.graphics_config, 155 | ) { 156 | Ok(()) => { 157 | state.should_relaunch = true; 158 | } 159 | Err(e) => { 160 | log::error!("Failed to save graphics config: {:?}", e); 161 | state.graphics_config.desired_backend = prev_backend; 162 | } 163 | } 164 | } 165 | } 166 | }); 167 | }); 168 | }); 169 | } 170 | -------------------------------------------------------------------------------- /objdiff-gui/src/views/jobs.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering; 2 | 3 | use egui::{ProgressBar, RichText, Widget}; 4 | use objdiff_core::jobs::{JobQueue, JobStatus}; 5 | 6 | use crate::views::appearance::Appearance; 7 | 8 | pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) { 9 | if ui.button("Clear").clicked() { 10 | jobs.clear_errored(); 11 | } 12 | 13 | let mut remove_job: Option = None; 14 | let mut any_jobs = false; 15 | for job in jobs.iter_mut() { 16 | let Ok(status) = job.context.status.read() else { 17 | continue; 18 | }; 19 | any_jobs = true; 20 | ui.separator(); 21 | ui.horizontal(|ui| { 22 | ui.label(&status.title); 23 | if ui.small_button("✖").clicked() { 24 | if job.handle.is_some() { 25 | if let Err(e) = job.cancel.send(()) { 26 | log::error!("Failed to cancel job: {e:?}"); 27 | } 28 | } else { 29 | remove_job = Some(job.id); 30 | } 31 | } 32 | }); 33 | let mut bar = ProgressBar::new(status.progress_percent); 34 | if let Some(items) = &status.progress_items { 35 | bar = bar.text(format!("{} / {}", items[0], items[1])); 36 | } 37 | bar.ui(ui); 38 | const STATUS_LENGTH: usize = 80; 39 | if let Some(err) = &status.error { 40 | let err_string = format!("{:#}", err); 41 | ui.colored_label( 42 | appearance.delete_color, 43 | if err_string.len() > STATUS_LENGTH - 10 { 44 | format!("Error: {}…", &err_string[0..STATUS_LENGTH - 10]) 45 | } else { 46 | format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7) 47 | }, 48 | ) 49 | .on_hover_text_at_pointer(RichText::new(&err_string).color(appearance.delete_color)) 50 | .context_menu(|ui| { 51 | if ui.button("Copy full message").clicked() { 52 | ui.ctx().copy_text(err_string); 53 | } 54 | }); 55 | } else { 56 | ui.label(if status.status.len() > STATUS_LENGTH - 3 { 57 | format!("{}…", &status.status[0..STATUS_LENGTH - 3]) 58 | } else { 59 | format!("{:width$}", &status.status, width = STATUS_LENGTH) 60 | }) 61 | .on_hover_text_at_pointer(&status.status) 62 | .context_menu(|ui| { 63 | if ui.button("Copy full message").clicked() { 64 | ui.ctx().copy_text(status.status.clone()); 65 | } 66 | }); 67 | } 68 | } 69 | if !any_jobs { 70 | ui.label("No jobs"); 71 | } 72 | 73 | if let Some(idx) = remove_job { 74 | jobs.remove(idx); 75 | } 76 | } 77 | 78 | struct JobStatusDisplay { 79 | title: String, 80 | progress_items: Option<[u32; 2]>, 81 | error: bool, 82 | } 83 | 84 | impl From<&JobStatus> for JobStatusDisplay { 85 | fn from(status: &JobStatus) -> Self { 86 | Self { 87 | title: status.title.clone(), 88 | progress_items: status.progress_items, 89 | error: status.error.is_some(), 90 | } 91 | } 92 | } 93 | 94 | pub fn jobs_menu_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) -> bool { 95 | ui.label("Jobs:"); 96 | let mut statuses = Vec::new(); 97 | for job in jobs.iter_mut() { 98 | let Ok(status) = job.context.status.read() else { 99 | continue; 100 | }; 101 | statuses.push(JobStatusDisplay::from(&*status)); 102 | } 103 | let running_jobs = statuses.iter().filter(|s| !s.error).count(); 104 | let error_jobs = statuses.iter().filter(|s| s.error).count(); 105 | 106 | let mut clicked = false; 107 | let spinner = 108 | egui::Spinner::new().size(appearance.ui_font.size * 0.9).color(appearance.text_color); 109 | match running_jobs.cmp(&1) { 110 | Ordering::Equal => { 111 | spinner.ui(ui); 112 | let running_job = statuses.iter().find(|s| !s.error).unwrap(); 113 | let text = if let Some(items) = running_job.progress_items { 114 | format!("{} ({}/{})", running_job.title, items[0], items[1]) 115 | } else { 116 | running_job.title.clone() 117 | }; 118 | clicked |= ui.link(RichText::new(text)).clicked(); 119 | } 120 | Ordering::Greater => { 121 | spinner.ui(ui); 122 | clicked |= ui.link(format!("{} running", running_jobs)).clicked(); 123 | } 124 | _ => (), 125 | } 126 | match error_jobs.cmp(&1) { 127 | Ordering::Equal => { 128 | let error_job = statuses.iter().find(|s| s.error).unwrap(); 129 | clicked |= ui 130 | .link( 131 | RichText::new(format!("{} error", error_job.title)) 132 | .color(appearance.delete_color), 133 | ) 134 | .clicked(); 135 | } 136 | Ordering::Greater => { 137 | clicked |= ui 138 | .link( 139 | RichText::new(format!("{} errors", error_jobs)).color(appearance.delete_color), 140 | ) 141 | .clicked(); 142 | } 143 | _ => (), 144 | } 145 | if running_jobs == 0 && error_jobs == 0 { 146 | clicked |= ui.link("None").clicked(); 147 | } 148 | clicked 149 | } 150 | 151 | pub fn jobs_window( 152 | ctx: &egui::Context, 153 | show: &mut bool, 154 | jobs: &mut JobQueue, 155 | appearance: &Appearance, 156 | ) { 157 | egui::Window::new("Jobs").open(show).show(ctx, |ui| { 158 | jobs_ui(ui, jobs, appearance); 159 | }); 160 | } 161 | -------------------------------------------------------------------------------- /objdiff-gui/src/views/mod.rs: -------------------------------------------------------------------------------- 1 | use egui::{Color32, FontId, TextFormat, text::LayoutJob}; 2 | 3 | pub(crate) mod appearance; 4 | pub(crate) mod column_layout; 5 | pub(crate) mod config; 6 | pub(crate) mod data_diff; 7 | pub(crate) mod debug; 8 | pub(crate) mod demangle; 9 | pub(crate) mod diff; 10 | pub(crate) mod extab_diff; 11 | pub(crate) mod file; 12 | pub(crate) mod frame_history; 13 | pub(crate) mod function_diff; 14 | pub(crate) mod graphics; 15 | pub(crate) mod jobs; 16 | pub(crate) mod rlwinm; 17 | pub(crate) mod symbol_diff; 18 | 19 | #[inline] 20 | fn write_text(str: &str, color: Color32, job: &mut LayoutJob, font_id: FontId) { 21 | job.append(str, 0.0, TextFormat::simple(font_id, color)); 22 | } 23 | -------------------------------------------------------------------------------- /objdiff-gui/src/views/rlwinm.rs: -------------------------------------------------------------------------------- 1 | use egui::TextStyle; 2 | 3 | use crate::views::appearance::Appearance; 4 | 5 | #[derive(Default)] 6 | pub struct RlwinmDecodeViewState { 7 | pub text: String, 8 | } 9 | 10 | pub fn rlwinm_decode_window( 11 | ctx: &egui::Context, 12 | show: &mut bool, 13 | state: &mut RlwinmDecodeViewState, 14 | appearance: &Appearance, 15 | ) { 16 | egui::Window::new("Rlwinm Decoder").open(show).show(ctx, |ui| { 17 | ui.text_edit_singleline(&mut state.text); 18 | ui.add_space(10.0); 19 | if let Some(decoded) = rlwinmdec::decode(&state.text) { 20 | ui.scope(|ui| { 21 | ui.style_mut().override_text_style = Some(TextStyle::Monospace); 22 | ui.colored_label(appearance.replace_color, decoded.trim()); 23 | }); 24 | if ui.button("Copy").clicked() { 25 | ctx.copy_text(decoded); 26 | } 27 | } else { 28 | ui.scope(|ui| { 29 | ui.style_mut().override_text_style = Some(TextStyle::Monospace); 30 | ui.colored_label(appearance.replace_color, "[invalid]"); 31 | }); 32 | } 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /objdiff-wasm/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | gen/ 3 | node_modules/ 4 | pkg/ 5 | -------------------------------------------------------------------------------- /objdiff-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "objdiff-wasm" 3 | version.workspace = true 4 | edition = "2024" 5 | rust-version.workspace = true 6 | authors.workspace = true 7 | license.workspace = true 8 | repository.workspace = true 9 | readme = "../README.md" 10 | description = """ 11 | A local diffing tool for decompilation projects. 12 | """ 13 | publish = false 14 | build = "build.rs" 15 | 16 | [lib] 17 | crate-type = ["cdylib"] 18 | 19 | [features] 20 | default = ["std"] 21 | std = ["objdiff-core/std"] 22 | 23 | [dependencies] 24 | log = { version = "0.4", default-features = false } 25 | regex = { version = "1.11", default-features = false, features = ["unicode-case"] } 26 | xxhash-rust = { version = "0.8", default-features = false, features = ["xxh3"] } 27 | 28 | [dependencies.objdiff-core] 29 | path = "../objdiff-core" 30 | default-features = false 31 | features = ["arm", "arm64", "mips", "ppc", "superh", "x86", "dwarf"] 32 | 33 | [target.'cfg(target_family = "wasm")'.dependencies] 34 | talc = { version = "4.4", default-features = false, features = ["lock_api"] } 35 | 36 | [target.'cfg(target_os = "wasi")'.dependencies] 37 | wit-bindgen = { version = "0.42", default-features = false, features = ["macros"] } 38 | 39 | [build-dependencies] 40 | wit-deps = "0.5" 41 | -------------------------------------------------------------------------------- /objdiff-wasm/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.8.0/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "vcs": { 7 | "enabled": true, 8 | "clientKind": "git", 9 | "useIgnoreFile": true 10 | }, 11 | "formatter": { 12 | "indentStyle": "space" 13 | }, 14 | "javascript": { 15 | "formatter": { 16 | "quoteStyle": "single" 17 | } 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true, 23 | "a11y": { 24 | "all": false 25 | }, 26 | "suspicious": { 27 | "noExplicitAny": "off" 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /objdiff-wasm/build.rs: -------------------------------------------------------------------------------- 1 | fn main() -> Result<(), Box> { 2 | wit_deps::lock_sync!()?; 3 | Ok(()) 4 | } 5 | -------------------------------------------------------------------------------- /objdiff-wasm/lib/wasi-logging.ts: -------------------------------------------------------------------------------- 1 | import type { WasiLoggingLogging010Draft as logging } from '../pkg/objdiff'; 2 | 3 | export const log: typeof logging.log = (level, context, message) => { 4 | const msg = `[${context}] ${message}`; 5 | switch (level) { 6 | case 'trace': 7 | console.trace(msg); 8 | break; 9 | case 'debug': 10 | console.debug(msg); 11 | break; 12 | case 'info': 13 | console.info(msg); 14 | break; 15 | case 'warn': 16 | console.warn(msg); 17 | break; 18 | case 'error': 19 | console.error(msg); 20 | break; 21 | case 'critical': 22 | console.error(msg); 23 | break; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /objdiff-wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "objdiff-wasm", 3 | "version": "3.0.0-beta.9", 4 | "description": "A local diffing tool for decompilation projects.", 5 | "author": { 6 | "name": "Luke Street", 7 | "email": "luke@street.dev" 8 | }, 9 | "license": "MIT OR Apache-2.0", 10 | "type": "module", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/encounter/objdiff.git" 14 | }, 15 | "files": [ 16 | "dist/*" 17 | ], 18 | "main": "dist/objdiff.js", 19 | "types": "dist/objdiff.d.ts", 20 | "scripts": { 21 | "build": "npm run build:wasm && npm run build:transpile && npm run build:lib", 22 | "build:wasm": "cargo +nightly -Zbuild-std=panic_abort,core,alloc -Zbuild-std-features=compiler-builtins-mem build --target wasm32-wasip2 --release --no-default-features", 23 | "build:transpile": "jco transpile ../target/wasm32-wasip2/release/objdiff_wasm.wasm --no-nodejs-compat --no-wasi-shim --no-namespaced-exports --map wasi:logging/logging=./wasi-logging.js --optimize -o pkg --name objdiff", 24 | "build:lib": "rslib build" 25 | }, 26 | "devDependencies": { 27 | "@biomejs/biome": "^1.9.3", 28 | "@bytecodealliance/jco": "^1.10.2", 29 | "@rslib/core": "^0.4.1", 30 | "typescript": "^5.7.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /objdiff-wasm/rslib.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from '@rslib/core'; 2 | 3 | export default defineConfig({ 4 | source: { 5 | entry: { 6 | 'wasi-logging': 'lib/wasi-logging.ts', 7 | }, 8 | }, 9 | lib: [ 10 | { 11 | format: 'esm', 12 | syntax: 'es2022', 13 | }, 14 | ], 15 | output: { 16 | target: 'web', 17 | copy: [{ from: 'pkg' }, { from: '../objdiff-core/config-schema.json' }], 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /objdiff-wasm/src/cabi_realloc.rs: -------------------------------------------------------------------------------- 1 | //! This module contains a canonical definition of the `cabi_realloc` function 2 | //! for the component model. 3 | //! 4 | //! The component model's canonical ABI for representing datatypes in memory 5 | //! makes use of this function when transferring lists and strings, for example. 6 | //! This function behaves like C's `realloc` but also takes alignment into 7 | //! account. 8 | //! 9 | //! Components are notably not required to export this function, but nearly 10 | //! all components end up doing so currently. This definition in the standard 11 | //! library removes the need for all compilations to define this themselves. 12 | //! 13 | //! More information about the canonical ABI can be found at 14 | //! 15 | //! 16 | //! Note that the name of this function is not standardized in the canonical ABI 17 | //! at this time. Instead it's a convention of the "componentization process" 18 | //! where a core wasm module is converted to a component to use this name. 19 | //! Additionally this is not the only possible definition of this function, so 20 | //! this is defined as a "weak" symbol. This means that other definitions are 21 | //! allowed to overwrite it if they are present in a compilation. 22 | 23 | use alloc::{Layout, alloc}; 24 | use core::ptr; 25 | 26 | #[used] 27 | static FORCE_CODEGEN_OF_CABI_REALLOC: unsafe extern "C" fn( 28 | *mut u8, 29 | usize, 30 | usize, 31 | usize, 32 | ) -> *mut u8 = cabi_realloc; 33 | 34 | #[unsafe(no_mangle)] 35 | pub unsafe extern "C" fn cabi_realloc( 36 | old_ptr: *mut u8, 37 | old_len: usize, 38 | mut align: usize, 39 | new_len: usize, 40 | ) -> *mut u8 { 41 | // HACK: The object crate requires the data alignment for 64-bit objects to be 8, 42 | // but in wasm32, our allocator will have a minimum alignment of 4. We can't specify 43 | // the alignment of `list` in the component model, so we work around this here. 44 | // https://github.com/WebAssembly/component-model/issues/258 45 | #[cfg(target_pointer_width = "32")] 46 | if align == 1 { 47 | align = 8; 48 | } 49 | let layout; 50 | let ptr = if old_len == 0 { 51 | if new_len == 0 { 52 | return ptr::without_provenance_mut(align); 53 | } 54 | layout = unsafe { Layout::from_size_align_unchecked(new_len, align) }; 55 | unsafe { alloc::alloc(layout) } 56 | } else { 57 | debug_assert_ne!(new_len, 0, "non-zero old_len requires non-zero new_len!"); 58 | layout = unsafe { Layout::from_size_align_unchecked(old_len, align) }; 59 | unsafe { alloc::realloc(old_ptr, layout, new_len) } 60 | }; 61 | if ptr.is_null() { 62 | // Print a nice message in debug mode, but in release mode don't 63 | // pull in so many dependencies related to printing so just emit an 64 | // `unreachable` instruction. 65 | if cfg!(debug_assertions) { 66 | alloc::handle_alloc_error(layout); 67 | } else { 68 | core::unreachable!("allocation failed") 69 | } 70 | } 71 | ptr 72 | } 73 | -------------------------------------------------------------------------------- /objdiff-wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | extern crate alloc; 3 | 4 | #[cfg(target_os = "wasi")] 5 | mod api; 6 | #[cfg(target_os = "wasi")] 7 | mod logging; 8 | 9 | #[cfg(all(target_os = "wasi", not(feature = "std")))] 10 | mod cabi_realloc; 11 | 12 | #[cfg(all(target_family = "wasm", not(feature = "std")))] 13 | #[global_allocator] 14 | static ALLOCATOR: talc::TalckWasm = unsafe { talc::TalckWasm::new_global() }; 15 | -------------------------------------------------------------------------------- /objdiff-wasm/src/logging.rs: -------------------------------------------------------------------------------- 1 | wit_bindgen::generate!({ 2 | world: "imports", 3 | path: "wit/deps/logging", 4 | }); 5 | 6 | use alloc::format; 7 | 8 | pub use wasi::logging::logging as wasi_logging; 9 | 10 | struct WasiLogger; 11 | 12 | impl log::Log for WasiLogger { 13 | fn enabled(&self, metadata: &log::Metadata) -> bool { metadata.level() <= log::max_level() } 14 | 15 | fn log(&self, record: &log::Record) { 16 | if !self.enabled(record.metadata()) { 17 | return; 18 | } 19 | let level = match record.level() { 20 | log::Level::Error => wasi_logging::Level::Error, 21 | log::Level::Warn => wasi_logging::Level::Warn, 22 | log::Level::Info => wasi_logging::Level::Info, 23 | log::Level::Debug => wasi_logging::Level::Debug, 24 | log::Level::Trace => wasi_logging::Level::Trace, 25 | }; 26 | wasi_logging::log(level, record.target(), &format!("{}", record.args())); 27 | } 28 | 29 | fn flush(&self) {} 30 | } 31 | 32 | static LOGGER: WasiLogger = WasiLogger; 33 | 34 | pub fn init(level: wasi_logging::Level) { 35 | let _ = log::set_logger(&LOGGER); 36 | log::set_max_level(match level { 37 | wasi_logging::Level::Error => log::LevelFilter::Error, 38 | wasi_logging::Level::Warn => log::LevelFilter::Warn, 39 | wasi_logging::Level::Info => log::LevelFilter::Info, 40 | wasi_logging::Level::Debug => log::LevelFilter::Debug, 41 | wasi_logging::Level::Trace => log::LevelFilter::Trace, 42 | wasi_logging::Level::Critical => log::LevelFilter::Off, 43 | }); 44 | } 45 | 46 | #[cfg(not(feature = "std"))] 47 | #[panic_handler] 48 | fn panic(info: &core::panic::PanicInfo) -> ! { 49 | use alloc::string::ToString; 50 | wasi_logging::log(wasi_logging::Level::Critical, "objdiff_core::panic", &info.to_string()); 51 | core::arch::wasm32::unreachable(); 52 | } 53 | -------------------------------------------------------------------------------- /objdiff-wasm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "ES2023"], 4 | "target": "ES2022", 5 | "noEmit": true, 6 | "skipLibCheck": true, 7 | "useDefineForClassFields": true, 8 | "module": "ESNext", 9 | "isolatedModules": true, 10 | "resolveJsonModule": true, 11 | "moduleResolution": "Bundler", 12 | "strict": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true 15 | }, 16 | "include": ["lib"] 17 | } 18 | -------------------------------------------------------------------------------- /objdiff-wasm/wit/.gitignore: -------------------------------------------------------------------------------- 1 | deps/ 2 | -------------------------------------------------------------------------------- /objdiff-wasm/wit/deps.lock: -------------------------------------------------------------------------------- 1 | [logging] 2 | url = "https://github.com/WebAssembly/wasi-logging/archive/d31c41d0d9eed81aabe02333d0025d42acf3fb75.tar.gz" 3 | sha256 = "ad81d8b7f7a8ceb729cf551f1d24586f0de9560a43eea57a9bb031d2175804e1" 4 | sha512 = "1687ad9a02ab3e689443e67d1a0605f58fc5dea828d2e4d2c7825c6002714fac9bd4289b1a68b61a37dcca6c3b421f4c8ed4b1e6cc29f6460e0913cf1bf11c04" 5 | -------------------------------------------------------------------------------- /objdiff-wasm/wit/deps.toml: -------------------------------------------------------------------------------- 1 | logging = "https://github.com/WebAssembly/wasi-logging/archive/d31c41d0d9eed81aabe02333d0025d42acf3fb75.tar.gz" 2 | -------------------------------------------------------------------------------- /objdiff-wasm/wit/objdiff.wit: -------------------------------------------------------------------------------- 1 | package objdiff:core; 2 | 3 | use wasi:logging/logging@0.1.0-draft; 4 | 5 | interface diff { 6 | resource diff-config { 7 | constructor(); 8 | set-property: func(id: string, value: string) -> result<_, string>; 9 | get-property: func(id: string) -> result; 10 | } 11 | 12 | record mapping-config { 13 | mappings: list>, 14 | selecting-left: option, 15 | selecting-right: option, 16 | } 17 | 18 | resource object { 19 | parse: static func( 20 | data: list, 21 | config: borrow, 22 | ) -> result; 23 | 24 | hash: func() -> u64; 25 | } 26 | 27 | type symbol-ref = u32; 28 | 29 | enum symbol-kind { 30 | unknown, 31 | function, 32 | object, 33 | section, 34 | } 35 | 36 | flags symbol-flags { 37 | global, 38 | local, 39 | weak, 40 | common, 41 | hidden, 42 | has-extra, 43 | size-inferred, 44 | ignored, 45 | } 46 | 47 | record symbol-info { 48 | id: symbol-ref, 49 | name: string, 50 | demangled-name: option, 51 | address: u64, 52 | size: u64, 53 | kind: symbol-kind, 54 | section: option, 55 | section-name: option, 56 | %flags: symbol-flags, 57 | align: option, 58 | virtual-address: option, 59 | } 60 | 61 | resource object-diff { 62 | find-symbol: func( 63 | name: string, 64 | section-name: option 65 | ) -> option; 66 | 67 | get-symbol: func( 68 | id: u32 69 | ) -> option; 70 | } 71 | 72 | record diff-result { 73 | left: option, 74 | right: option, 75 | } 76 | 77 | run-diff: func( 78 | left: option>, 79 | right: option>, 80 | config: borrow, 81 | mapping: mapping-config, 82 | ) -> result; 83 | } 84 | 85 | interface display { 86 | use diff.{ 87 | object, 88 | object-diff, 89 | diff-config, 90 | symbol-info, 91 | symbol-ref 92 | }; 93 | 94 | record display-config { 95 | show-hidden-symbols: bool, 96 | show-mapped-symbols: bool, 97 | reverse-fn-order: bool, 98 | } 99 | 100 | record symbol-filter { 101 | regex: option, 102 | mapping: option, 103 | } 104 | 105 | record section-display { 106 | id: string, 107 | name: string, 108 | size: u64, 109 | match-percent: option, 110 | symbols: list, 111 | } 112 | 113 | record symbol-display { 114 | info: symbol-info, 115 | target-symbol: option, 116 | match-percent: option, 117 | diff-score: option>, 118 | row-count: u32, 119 | } 120 | 121 | enum symbol-navigation-kind { 122 | normal, 123 | extab, 124 | } 125 | 126 | record context-item-copy { 127 | value: string, 128 | label: option, 129 | } 130 | 131 | record context-item-navigate { 132 | label: string, 133 | symbol: symbol-ref, 134 | kind: symbol-navigation-kind, 135 | } 136 | 137 | variant context-item { 138 | copy(context-item-copy), 139 | navigate(context-item-navigate), 140 | separator, 141 | } 142 | 143 | enum hover-item-color { 144 | normal, 145 | emphasized, 146 | special, 147 | delete, 148 | insert, 149 | } 150 | 151 | record hover-item-text { 152 | label: string, 153 | value: string, 154 | color: hover-item-color, 155 | } 156 | 157 | variant hover-item { 158 | text(hover-item-text), 159 | separator, 160 | } 161 | 162 | record diff-text-opcode { 163 | mnemonic: string, 164 | opcode: u16, 165 | } 166 | 167 | record diff-text-symbol { 168 | name: string, 169 | demangled-name: option, 170 | } 171 | 172 | variant diff-text { 173 | // Basic text (not semantically meaningful) 174 | basic(string), 175 | // Line number 176 | line(u32), 177 | // Instruction address 178 | address(u64), 179 | // Instruction mnemonic 180 | opcode(diff-text-opcode), 181 | // Instruction argument (signed) 182 | signed(s64), 183 | // Instruction argument (unsigned) 184 | unsigned(u64), 185 | // Instruction argument (opaque) 186 | opaque(string), 187 | // Instruction argument (branch destination) 188 | branch-dest(u64), 189 | // Relocation target name 190 | symbol(diff-text-symbol), 191 | // Relocation addend 192 | addend(s64), 193 | // Number of spaces 194 | spacing(u8), 195 | // End of line 196 | eol, 197 | } 198 | 199 | variant diff-text-color { 200 | normal, 201 | dim, 202 | bright, 203 | replace, 204 | delete, 205 | insert, 206 | rotating(u8), 207 | } 208 | 209 | record diff-text-segment { 210 | // Text to display 211 | text: diff-text, 212 | // Text color 213 | color: diff-text-color, 214 | // Number of spaces to pad to 215 | pad-to: u8, 216 | } 217 | 218 | record instruction-diff-row { 219 | // Text segments 220 | segments: list, 221 | // Diff kind 222 | diff-kind: instruction-diff-kind, 223 | } 224 | 225 | enum instruction-diff-kind { 226 | none, 227 | op-mismatch, 228 | arg-mismatch, 229 | replace, 230 | insert, 231 | delete, 232 | } 233 | 234 | display-sections: func( 235 | diff: borrow, 236 | filter: symbol-filter, 237 | config: display-config, 238 | ) -> list; 239 | 240 | display-symbol: func( 241 | diff: borrow, 242 | symbol: symbol-ref, 243 | ) -> symbol-display; 244 | 245 | display-instruction-row: func( 246 | diff: borrow, 247 | symbol: symbol-ref, 248 | row-index: u32, 249 | config: borrow, 250 | ) -> instruction-diff-row; 251 | 252 | symbol-context: func( 253 | diff: borrow, 254 | symbol: symbol-ref, 255 | ) -> list; 256 | 257 | symbol-hover: func( 258 | diff: borrow, 259 | symbol: symbol-ref, 260 | ) -> list; 261 | 262 | instruction-context: func( 263 | diff: borrow, 264 | symbol: symbol-ref, 265 | row-index: u32, 266 | config: borrow, 267 | ) -> list; 268 | 269 | instruction-hover: func( 270 | diff: borrow, 271 | symbol: symbol-ref, 272 | row-index: u32, 273 | config: borrow, 274 | ) -> list; 275 | } 276 | 277 | world api { 278 | import logging; 279 | use logging.{level}; 280 | 281 | export diff; 282 | export display; 283 | 284 | export init: func(level: level); 285 | export version: func() -> string; 286 | } 287 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | fn_single_line = true 2 | group_imports = "StdExternalCrate" 3 | imports_granularity = "Crate" 4 | overflow_delimited_expr = true 5 | reorder_impl_items = true 6 | use_field_init_shorthand = true 7 | use_small_heuristics = "Max" 8 | where_single_line = true 9 | --------------------------------------------------------------------------------