Compare commits

..

16 Commits

Author SHA1 Message Date
Luke Street 48305e0380 Add hover/context APIs to wasm component 2025-03-02 21:51:47 -07:00
Luke Street 2eafbb218b Update all dependencies 2025-03-02 16:13:12 -07:00
Luke Street a1f2a535e5 Unify context menu / hover tooltip code + UI improvements 2025-03-02 16:13:12 -07:00
Luke Street 8461b35cd7 Fix WASM CI build 2025-03-02 16:13:12 -07:00
Luke Street 95868f1d19 Reimplement x86 arch, MSVC section group combining
Plus display_row/DiffText refactoring
2025-03-02 16:13:12 -07:00
Luke Street 506c251d68 Various fixes 2025-02-26 22:47:42 -07:00
Luke Street f3c157ff06 WIP objdiff 3.0 refactor 2025-02-26 22:09:39 -07:00
angie 6d3c63ccd8 Use rabbitizer 2
Co-Authored-By: Luke Street <luke@street.dev>
2025-02-09 22:28:04 -07:00
Luke Street bbd8d9714f Adjust CI build features 2025-02-09 22:28:04 -07:00
Luke Street 3c66ac3d54 Fix run_make on Windows 2025-02-09 22:28:04 -07:00
Luke Street 561a9107e2 clippy & deny fixes 2025-02-09 22:28:04 -07:00
Luke Street e8de35b78e Make objdiff-core no_std + huge WASM rework 2025-02-09 22:28:02 -07:00
Luke Street d938988d43 Diff view refactor 2025-02-09 22:27:42 -07:00
LagoLunatic 3e6efb7736 Check relocation addends when diffing functions (#158)
* Check relocation addends when diffing functions

* Also highlight addend when reloc differs
2025-02-09 22:26:49 -07:00
Steven Casper 674c942d7d Implement context menu copy functionality for data values (#163)
* Implement context menu copy functionality for data values

* Clippy fixes
2025-02-09 22:24:52 -07:00
LagoLunatic 6b7dcabbed Fix added/removed bytes being visually misaligned in data diff view (#159) 2025-01-24 17:12:44 -07:00
101 changed files with 20224 additions and 9660 deletions
+24 -9
View File
@@ -10,7 +10,6 @@ on:
env:
BUILD_PROFILE: release-lto
CARGO_TARGET_DIR: target
CARGO_INCREMENTAL: 0
jobs:
@@ -33,9 +32,9 @@ jobs:
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo check
run: cargo check --all-features --all-targets
run: cargo check --features all
- name: Cargo clippy
run: cargo clippy --all-features --all-targets
run: cargo clippy --features all
fmt:
name: Format
@@ -71,7 +70,6 @@ jobs:
test:
name: Test
if: 'false' # No tests yet
strategy:
matrix:
platform: [ ubuntu-latest, windows-latest, macos-latest ]
@@ -90,7 +88,7 @@ jobs:
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Cargo test
run: cargo test --release --all-features
run: cargo test --release --features all
build-cli:
name: Build objdiff-cli
@@ -168,8 +166,8 @@ jobs:
with:
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
path: |
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
if-no-files-found: error
build-gui:
@@ -223,10 +221,27 @@ jobs:
with:
name: ${{ env.CARGO_BIN_NAME }}-${{ matrix.name }}
path: |
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
${{ env.CARGO_TARGET_DIR }}/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}
target/${{ matrix.target }}/${{ env.BUILD_PROFILE }}/${{ env.CARGO_BIN_NAME }}.exe
if-no-files-found: error
build-wasm:
name: Build objdiff-wasm
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@nightly
with:
components: rust-src
- name: Cache Rust workspace
uses: Swatinem/rust-cache@v2
- name: Install dependencies
run: npm -C objdiff-wasm install
- name: Build
run: npm -C objdiff-wasm run build
release:
name: Release
if: startsWith(github.ref, 'refs/tags/')
Generated
+1277 -594
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -3,6 +3,7 @@ members = [
"objdiff-cli",
"objdiff-core",
"objdiff-gui",
"objdiff-wasm",
]
resolver = "2"
@@ -13,9 +14,9 @@ strip = "debuginfo"
codegen-units = 1
[workspace.package]
version = "2.7.1"
version = "3.0.0-alpha.1"
authors = ["Luke Street <luke@street.dev>"]
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff"
rust-version = "1.81"
rust-version = "1.82"
+7 -3
View File
@@ -101,8 +101,7 @@ allow = [
"Zlib",
"0BSD",
"OFL-1.1",
"LicenseRef-UFL-1.0",
"OpenSSL",
"Ubuntu-font-1.0",
]
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
@@ -239,7 +238,12 @@ allow-git = []
[sources.allow-org]
# github.com organizations to allow git sources for
github = []
github = [
"CelestialAmber", # cwextab, rlwinmdec
"Decompollaborate", # rabbitizer
"enarx", # flagset
"encounter",
]
# gitlab.com organizations to allow git sources for
gitlab = []
# bitbucket.org organizations to allow git sources for
+1
View File
@@ -28,6 +28,7 @@ supports-color = "3.0"
time = { version = "0.3", features = ["formatting", "local-offset"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
typed-path = "0.10"
[target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1"
+130 -111
View File
@@ -1,8 +1,6 @@
use std::{
fs,
io::stdout,
mem,
path::{Path, PathBuf},
str::FromStr,
sync::{
atomic::{AtomicBool, Ordering},
@@ -27,19 +25,23 @@ use objdiff_core::{
watcher::{create_watcher, Watcher},
BuildConfig,
},
config::{build_globset, ProjectConfig, ProjectObject},
diff,
config::{
build_globset,
path::{check_path_buf, platform_path, platform_path_serde_option},
ProjectConfig, ProjectObject, ProjectObjectMetadata,
},
diff::{
ConfigEnum, ConfigPropertyId, ConfigPropertyKind, DiffObjConfig, MappingConfig, ObjDiff,
self, ConfigEnum, ConfigPropertyId, ConfigPropertyKind, DiffObjConfig, MappingConfig,
ObjectDiff,
},
jobs::{
objdiff::{start_build, ObjDiffConfig},
Job, JobQueue, JobResult,
},
obj,
obj::ObjInfo,
obj::{self, Object},
};
use ratatui::prelude::*;
use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
use crate::{
util::{
@@ -53,21 +55,21 @@ use crate::{
/// Diff two object files. (Interactive or one-shot mode)
#[argp(subcommand, name = "diff")]
pub struct Args {
#[argp(option, short = '1')]
#[argp(option, short = '1', from_str_fn(platform_path))]
/// Target object file
target: Option<PathBuf>,
#[argp(option, short = '2')]
target: Option<Utf8PlatformPathBuf>,
#[argp(option, short = '2', from_str_fn(platform_path))]
/// Base object file
base: Option<PathBuf>,
#[argp(option, short = 'p')]
base: Option<Utf8PlatformPathBuf>,
#[argp(option, short = 'p', from_str_fn(platform_path))]
/// Project directory
project: Option<PathBuf>,
project: Option<Utf8PlatformPathBuf>,
#[argp(option, short = 'u')]
/// Unit name within project
unit: Option<String>,
#[argp(option, short = 'o')]
#[argp(option, short = 'o', from_str_fn(platform_path))]
/// Output file (one-shot mode) ("-" for stdout)
output: Option<PathBuf>,
output: Option<Utf8PlatformPathBuf>,
#[argp(option)]
/// Output format (json, json-pretty, proto) (default: json)
format: Option<String>,
@@ -89,86 +91,61 @@ pub struct Args {
}
pub fn run(args: Args) -> Result<()> {
let (target_path, base_path, project_config) = match (
&args.target,
&args.base,
&args.project,
&args.unit,
) {
(Some(_), Some(_), None, None)
| (Some(_), None, None, None)
| (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None),
(None, None, p, u) => {
let project = match p {
Some(project) => project.clone(),
_ => std::env::current_dir().context("Failed to get the current directory")?,
};
let Some((project_config, project_config_info)) =
objdiff_core::config::try_project_config(&project)
else {
bail!("Project config not found in {}", &project.display())
};
let mut project_config = project_config.with_context(|| {
format!("Reading project config {}", project_config_info.path.display())
})?;
let object = {
let resolve_paths = |o: &mut ProjectObject| {
o.resolve_paths(
&project,
project_config.target_dir.as_deref(),
project_config.base_dir.as_deref(),
let (target_path, base_path, project_config) =
match (&args.target, &args.base, &args.project, &args.unit) {
(Some(_), Some(_), None, None)
| (Some(_), None, None, None)
| (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None),
(None, None, p, u) => {
let project = match p {
Some(project) => project.clone(),
_ => check_path_buf(
std::env::current_dir().context("Failed to get the current directory")?,
)
.context("Current directory is not valid UTF-8")?,
};
if let Some(u) = u {
let unit_path =
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
let Some(object) = project_config
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.find_map(|obj| {
if obj.name.as_deref() == Some(u) {
resolve_paths(obj);
return Some(obj);
}
let up = unit_path.as_deref()?;
resolve_paths(obj);
if [&obj.base_path, &obj.target_path]
.into_iter()
.filter_map(|p| p.as_ref().and_then(|p| p.canonicalize().ok()))
.any(|p| p == up)
{
return Some(obj);
}
None
})
else {
bail!("Unit not found: {}", u)
};
object
let Some((project_config, project_config_info)) =
objdiff_core::config::try_project_config(project.as_ref())
else {
bail!("Project config not found in {}", &project)
};
let project_config = project_config.with_context(|| {
format!("Reading project config {}", project_config_info.path.display())
})?;
let target_obj_dir = project_config
.target_dir
.as_ref()
.map(|p| project.join(p.with_platform_encoding()));
let base_obj_dir = project_config
.base_dir
.as_ref()
.map(|p| project.join(p.with_platform_encoding()));
let objects = project_config
.units
.iter()
.flatten()
.map(|o| {
ObjectConfig::new(
o,
&project,
target_obj_dir.as_deref(),
base_obj_dir.as_deref(),
)
})
.collect::<Vec<_>>();
let object = if let Some(u) = u {
objects
.iter()
.find(|obj| obj.name == *u)
.ok_or_else(|| anyhow!("Unit not found: {}", u))?
} else if let Some(symbol_name) = &args.symbol {
let mut idx = None;
let mut count = 0usize;
for (i, obj) in project_config
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.enumerate()
{
resolve_paths(obj);
for (i, obj) in objects.iter().enumerate() {
if obj
.target_path
.as_deref()
.map(|o| obj::read::has_function(o, symbol_name))
.map(|o| obj::read::has_function(o.as_ref(), symbol_name))
.transpose()?
.unwrap_or(false)
{
@@ -181,7 +158,7 @@ pub fn run(args: Args) -> Result<()> {
}
match (count, idx) {
(0, None) => bail!("Symbol not found: {}", symbol_name),
(1, Some(i)) => &mut project_config.units_mut()[i],
(1, Some(i)) => &objects[i],
(2.., Some(_)) => bail!(
"Multiple instances of {} were found, try specifying a unit",
symbol_name
@@ -190,14 +167,13 @@ pub fn run(args: Args) -> Result<()> {
}
} else {
bail!("Must specify one of: symbol, project and unit, target and base objects")
}
};
let target_path = object.target_path.clone();
let base_path = object.base_path.clone();
(target_path, base_path, Some(project_config))
}
_ => bail!("Either target and base or project and unit must be specified"),
};
};
let target_path = object.target_path.clone();
let base_path = object.base_path.clone();
(target_path, base_path, Some(project_config))
}
_ => bail!("Either target and base or project and unit must be specified"),
};
if let Some(output) = &args.output {
run_oneshot(&args, output, target_path.as_deref(), base_path.as_deref())
@@ -245,24 +221,24 @@ fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)>
fn run_oneshot(
args: &Args,
output: &Path,
target_path: Option<&Path>,
base_path: Option<&Path>,
output: &Utf8PlatformPath,
target_path: Option<&Utf8PlatformPath>,
base_path: Option<&Utf8PlatformPath>,
) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?;
let (diff_config, mapping_config) = build_config_from_args(args)?;
let target = target_path
.map(|p| {
obj::read::read(p, &diff_config).with_context(|| format!("Loading {}", p.display()))
obj::read::read(p.as_ref(), &diff_config).with_context(|| format!("Loading {}", p))
})
.transpose()?;
let base = base_path
.map(|p| {
obj::read::read(p, &diff_config).with_context(|| format!("Loading {}", p.display()))
obj::read::read(p.as_ref(), &diff_config).with_context(|| format!("Loading {}", p))
})
.transpose()?;
let result =
diff::diff_objs(&diff_config, &mapping_config, target.as_ref(), base.as_ref(), None)?;
diff::diff_objs(target.as_ref(), base.as_ref(), None, &diff_config, &mapping_config)?;
let left = target.as_ref().and_then(|o| result.left.as_ref().map(|d| (o, d)));
let right = base.as_ref().and_then(|o| result.right.as_ref().map(|d| (o, d)));
write_output(&DiffResult::new(left, right), Some(output), output_format)?;
@@ -272,13 +248,13 @@ fn run_oneshot(
pub struct AppState {
pub jobs: JobQueue,
pub waker: Arc<TermWaker>,
pub project_dir: Option<PathBuf>,
pub project_dir: Option<Utf8PlatformPathBuf>,
pub project_config: Option<ProjectConfig>,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub left_obj: Option<(ObjInfo, ObjDiff)>,
pub right_obj: Option<(ObjInfo, ObjDiff)>,
pub prev_obj: Option<(ObjInfo, ObjDiff)>,
pub target_path: Option<Utf8PlatformPathBuf>,
pub base_path: Option<Utf8PlatformPathBuf>,
pub left_obj: Option<(Object, ObjectDiff)>,
pub right_obj: Option<(Object, ObjectDiff)>,
pub prev_obj: Option<(Object, ObjectDiff)>,
pub reload_time: Option<time::OffsetDateTime>,
pub time_format: Vec<time::format_description::FormatItem<'static>>,
pub watcher: Option<Watcher>,
@@ -315,6 +291,49 @@ fn create_objdiff_config(state: &AppState) -> ObjDiffConfig {
}
}
/// The configuration for a single object file.
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
pub struct ObjectConfig {
pub name: String,
#[serde(default, with = "platform_path_serde_option")]
pub target_path: Option<Utf8PlatformPathBuf>,
#[serde(default, with = "platform_path_serde_option")]
pub base_path: Option<Utf8PlatformPathBuf>,
pub metadata: ProjectObjectMetadata,
pub complete: Option<bool>,
}
impl ObjectConfig {
pub fn new(
object: &ProjectObject,
project_dir: &Utf8PlatformPath,
target_obj_dir: Option<&Utf8PlatformPath>,
base_obj_dir: Option<&Utf8PlatformPath>,
) -> Self {
let target_path = if let (Some(target_obj_dir), Some(path), None) =
(target_obj_dir, &object.path, &object.target_path)
{
Some(target_obj_dir.join(path.with_platform_encoding()))
} else {
object.target_path.as_ref().map(|path| project_dir.join(path.with_platform_encoding()))
};
let base_path = if let (Some(base_obj_dir), Some(path), None) =
(base_obj_dir, &object.path, &object.base_path)
{
Some(base_obj_dir.join(path.with_platform_encoding()))
} else {
object.base_path.as_ref().map(|path| project_dir.join(path.with_platform_encoding()))
};
Self {
name: object.name().to_string(),
target_path,
base_path,
metadata: object.metadata.clone().unwrap_or_default(),
complete: object.complete(),
}
}
}
impl AppState {
fn reload(&mut self) -> Result<()> {
let config = create_objdiff_config(self);
@@ -355,8 +374,8 @@ impl Wake for TermWaker {
fn run_interactive(
args: Args,
target_path: Option<PathBuf>,
base_path: Option<PathBuf>,
target_path: Option<Utf8PlatformPathBuf>,
base_path: Option<Utf8PlatformPathBuf>,
project_config: Option<ProjectConfig>,
) -> Result<()> {
let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") };
@@ -384,7 +403,7 @@ fn run_interactive(
let watch_patterns = project_config.build_watch_patterns()?;
state.watcher = Some(create_watcher(
state.modified.clone(),
project_dir,
project_dir.as_ref(),
build_globset(&watch_patterns)?,
Waker::from(state.waker.clone()),
)?);
+75 -85
View File
@@ -1,10 +1,4 @@
use std::{
collections::HashSet,
fs::File,
io::Read,
path::{Path, PathBuf},
time::Instant,
};
use std::{collections::HashSet, fs::File, io::Read, time::Instant};
use anyhow::{bail, Context, Result};
use argp::FromArgs;
@@ -14,15 +8,19 @@ use objdiff_core::{
ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
REPORT_VERSION,
},
config::ProjectObject,
config::path::platform_path,
diff, obj,
obj::{ObjSectionKind, ObjSymbolFlags},
obj::{SectionKind, SymbolFlag},
};
use prost::Message;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use tracing::{info, warn};
use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
use crate::util::output::{write_output, OutputFormat};
use crate::{
cmd::diff::ObjectConfig,
util::output::{write_output, OutputFormat},
};
#[derive(FromArgs, PartialEq, Debug)]
/// Generate a progress report for a project.
@@ -43,12 +41,12 @@ pub enum SubCommand {
/// Generate a progress report for a project.
#[argp(subcommand, name = "generate")]
pub struct GenerateArgs {
#[argp(option, short = 'p')]
#[argp(option, short = 'p', from_str_fn(platform_path))]
/// Project directory
project: Option<PathBuf>,
#[argp(option, short = 'o')]
project: Option<Utf8PlatformPathBuf>,
#[argp(option, short = 'o', from_str_fn(platform_path))]
/// Output file
output: Option<PathBuf>,
output: Option<Utf8PlatformPathBuf>,
#[argp(switch, short = 'd')]
/// Deduplicate global and weak symbols (runs single-threaded)
deduplicate: bool,
@@ -61,15 +59,15 @@ pub struct GenerateArgs {
/// List any changes from a previous report.
#[argp(subcommand, name = "changes")]
pub struct ChangesArgs {
#[argp(positional)]
#[argp(positional, from_str_fn(platform_path))]
/// Previous report file
previous: PathBuf,
#[argp(positional)]
previous: Utf8PlatformPathBuf,
#[argp(positional, from_str_fn(platform_path))]
/// Current report file
current: PathBuf,
#[argp(option, short = 'o')]
current: Utf8PlatformPathBuf,
#[argp(option, short = 'o', from_str_fn(platform_path))]
/// Output file
output: Option<PathBuf>,
output: Option<Utf8PlatformPathBuf>,
#[argp(option, short = 'f')]
/// Output format (json, json-pretty, proto) (default: json)
format: Option<String>,
@@ -84,10 +82,10 @@ pub fn run(args: Args) -> Result<()> {
fn generate(args: GenerateArgs) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?;
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
info!("Loading project {}", project_dir.display());
let project_dir = args.project.as_deref().unwrap_or_else(|| Utf8PlatformPath::new("."));
info!("Loading project {}", project_dir);
let mut project = match objdiff_core::config::try_project_config(project_dir) {
let project = match objdiff_core::config::try_project_config(project_dir.as_ref()) {
Some((Ok(config), _)) => config,
Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err),
None => bail!("No project configuration found"),
@@ -98,37 +96,33 @@ fn generate(args: GenerateArgs) -> Result<()> {
if args.deduplicate { 1 } else { rayon::current_num_threads() }
);
let target_obj_dir =
project.target_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
let base_obj_dir =
project.base_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
let objects = project
.units
.iter()
.flatten()
.map(|o| {
ObjectConfig::new(o, project_dir, target_obj_dir.as_deref(), base_obj_dir.as_deref())
})
.collect::<Vec<_>>();
let start = Instant::now();
let mut units = vec![];
let mut existing_functions: HashSet<String> = HashSet::new();
if args.deduplicate {
// If deduplicating, we need to run single-threaded
for object in project.units.as_deref_mut().unwrap_or_default() {
if let Some(unit) = report_object(
object,
project_dir,
project.target_dir.as_deref(),
project.base_dir.as_deref(),
Some(&mut existing_functions),
)? {
for object in &objects {
if let Some(unit) = report_object(object, Some(&mut existing_functions))? {
units.push(unit);
}
}
} else {
let vec = project
.units
.as_deref_mut()
.unwrap_or_default()
.par_iter_mut()
.map(|object| {
report_object(
object,
project_dir,
project.target_dir.as_deref(),
project.base_dir.as_deref(),
None,
)
})
let vec = objects
.par_iter()
.map(|object| report_object(object, None))
.collect::<Result<Vec<Option<ReportUnit>>>>()?;
units = vec.into_iter().flatten().collect();
}
@@ -151,20 +145,16 @@ fn generate(args: GenerateArgs) -> Result<()> {
}
fn report_object(
object: &mut ProjectObject,
project_dir: &Path,
target_dir: Option<&Path>,
base_dir: Option<&Path>,
object: &ObjectConfig,
mut existing_functions: Option<&mut HashSet<String>>,
) -> Result<Option<ReportUnit>> {
object.resolve_paths(project_dir, target_dir, base_dir);
match (&object.target_path, &object.base_path) {
(None, Some(_)) if !object.complete().unwrap_or(false) => {
warn!("Skipping object without target: {}", object.name());
(None, Some(_)) if !object.complete.unwrap_or(false) => {
warn!("Skipping object without target: {}", object.name);
return Ok(None);
}
(None, None) => {
warn!("Skipping object without target or base: {}", object.name());
warn!("Skipping object without target or base: {}", object.name);
return Ok(None);
}
_ => {}
@@ -178,35 +168,31 @@ fn report_object(
.target_path
.as_ref()
.map(|p| {
obj::read::read(p, &diff_config)
.with_context(|| format!("Failed to open {}", p.display()))
obj::read::read(p.as_ref(), &diff_config)
.with_context(|| format!("Failed to open {}", p))
})
.transpose()?;
let base = object
.base_path
.as_ref()
.map(|p| {
obj::read::read(p, &diff_config)
.with_context(|| format!("Failed to open {}", p.display()))
obj::read::read(p.as_ref(), &diff_config)
.with_context(|| format!("Failed to open {}", p))
})
.transpose()?;
let result =
diff::diff_objs(&diff_config, &mapping_config, target.as_ref(), base.as_ref(), None)?;
diff::diff_objs(target.as_ref(), base.as_ref(), None, &diff_config, &mapping_config)?;
let metadata = ReportUnitMetadata {
complete: object.complete(),
complete: object.metadata.complete,
module_name: target
.as_ref()
.and_then(|o| o.split_meta.as_ref())
.and_then(|m| m.module_name.clone()),
module_id: target.as_ref().and_then(|o| o.split_meta.as_ref()).and_then(|m| m.module_id),
source_path: object.metadata.as_ref().and_then(|m| m.source_path.clone()),
progress_categories: object
.metadata
.as_ref()
.and_then(|m| m.progress_categories.clone())
.unwrap_or_default(),
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
source_path: object.metadata.source_path.as_ref().map(|p| p.to_string()),
progress_categories: object.metadata.progress_categories.clone().unwrap_or_default(),
auto_generated: object.metadata.auto_generated,
};
let mut measures = Measures { total_units: 1, ..Default::default() };
let mut sections = vec![];
@@ -214,11 +200,13 @@ fn report_object(
let obj = target.as_ref().or(base.as_ref()).unwrap();
let obj_diff = result.left.as_ref().or(result.right.as_ref()).unwrap();
for (section, section_diff) in obj.sections.iter().zip(&obj_diff.sections) {
for ((section_idx, section), section_diff) in
obj.sections.iter().enumerate().zip(&obj_diff.sections)
{
let section_match_percent = section_diff.match_percent.unwrap_or_else(|| {
// Support cases where we don't have a target object,
// assume complete means 100% match
if object.complete().unwrap_or(false) {
if object.complete.unwrap_or(false) {
100.0
} else {
0.0
@@ -235,23 +223,26 @@ fn report_object(
});
match section.kind {
ObjSectionKind::Data | ObjSectionKind::Bss => {
SectionKind::Data | SectionKind::Bss => {
measures.total_data += section.size;
if section_match_percent == 100.0 {
measures.matched_data += section.size;
}
continue;
}
ObjSectionKind::Code => (),
_ => {}
}
for (symbol, symbol_diff) in section.symbols.iter().zip(&section_diff.symbols) {
if symbol.size == 0 || symbol.flags.0.contains(ObjSymbolFlags::Hidden) {
for (symbol, symbol_diff) in obj.symbols.iter().zip(&obj_diff.symbols) {
if symbol.section != Some(section_idx)
|| symbol.size == 0
|| symbol.flags.contains(SymbolFlag::Hidden)
{
continue;
}
if let Some(existing_functions) = &mut existing_functions {
if (symbol.flags.0.contains(ObjSymbolFlags::Global)
|| symbol.flags.0.contains(ObjSymbolFlags::Weak))
if (symbol.flags.contains(SymbolFlag::Global)
|| symbol.flags.contains(SymbolFlag::Weak))
&& !existing_functions.insert(symbol.name.clone())
{
continue;
@@ -260,7 +251,7 @@ fn report_object(
let match_percent = symbol_diff.match_percent.unwrap_or_else(|| {
// Support cases where we don't have a target object,
// assume complete means 100% match
if object.complete().unwrap_or(false) {
if object.complete.unwrap_or(false) {
100.0
} else {
0.0
@@ -294,7 +285,7 @@ fn report_object(
measures.calc_fuzzy_match_percent();
measures.calc_matched_percent();
Ok(Some(ReportUnit {
name: object.name().to_string(),
name: object.name.clone(),
measures: Some(measures),
sections,
functions,
@@ -304,7 +295,7 @@ fn report_object(
fn changes(args: ChangesArgs) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?;
let (previous, current) = if args.previous == Path::new("-") && args.current == Path::new("-") {
let (previous, current) = if args.previous == "-" && args.current == "-" {
// Special case for comparing two reports from stdin
let mut data = vec![];
std::io::stdin().read_to_end(&mut data)?;
@@ -419,15 +410,14 @@ fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
.collect()
}
fn read_report(path: &Path) -> Result<Report> {
if path == Path::new("-") {
fn read_report(path: &Utf8PlatformPath) -> Result<Report> {
if path == Utf8PlatformPath::new("-") {
let mut data = vec![];
std::io::stdin().read_to_end(&mut data)?;
return Report::parse(&data).with_context(|| "Failed to load report from stdin");
}
let file = File::open(path).with_context(|| format!("Failed to open {}", path.display()))?;
let mmap = unsafe { memmap2::Mmap::map(&file) }
.with_context(|| format!("Failed to map {}", path.display()))?;
Report::parse(mmap.as_ref())
.with_context(|| format!("Failed to load report {}", path.display()))
let file = File::open(path).with_context(|| format!("Failed to open {}", path))?;
let mmap =
unsafe { memmap2::Mmap::map(&file) }.with_context(|| format!("Failed to map {}", path))?;
Report::parse(mmap.as_ref()).with_context(|| format!("Failed to load report {}", path))
}
+2
View File
@@ -1,3 +1,5 @@
#![allow(clippy::too_many_arguments)]
mod argp_version;
mod cmd;
mod util;
+6 -3
View File
@@ -34,9 +34,12 @@ impl OutputFormat {
}
}
pub fn write_output<T>(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()>
where T: serde::Serialize + prost::Message {
match output {
pub fn write_output<T, P>(input: &T, output: Option<P>, format: OutputFormat) -> Result<()>
where
T: serde::Serialize + prost::Message,
P: AsRef<Path>,
{
match output.as_ref().map(|p| p.as_ref()) {
Some(output) if output != Path::new("-") => {
info!("Writing to {}", output.display());
let file = File::options()
+88 -108
View File
@@ -1,11 +1,13 @@
use core::cmp::Ordering;
use anyhow::{bail, Result};
use crossterm::event::{Event, KeyCode, KeyEventKind, KeyModifiers, MouseButton, MouseEventKind};
use objdiff_core::{
diff::{
display::{display_diff, DiffText, HighlightKind},
FunctionRelocDiffs, ObjDiff, ObjInsDiffKind, ObjSymbolDiff,
display::{display_row, DiffText, DiffTextColor, HighlightKind},
DiffObjConfig, FunctionRelocDiffs, InstructionDiffKind, ObjectDiff, SymbolDiff,
},
obj::{ObjInfo, ObjSectionKind, ObjSymbol, SymbolRef},
obj::Object,
};
use ratatui::{
prelude::*,
@@ -28,9 +30,9 @@ pub struct FunctionDiffUi {
pub scroll_state_y: ScrollbarState,
pub per_page: usize,
pub num_rows: usize,
pub left_sym: Option<SymbolRef>,
pub right_sym: Option<SymbolRef>,
pub prev_sym: Option<SymbolRef>,
pub left_sym: Option<usize>,
pub right_sym: Option<usize>,
pub prev_sym: Option<usize>,
pub open_options: bool,
pub three_way: bool,
}
@@ -80,8 +82,8 @@ impl UiView for FunctionDiffUi {
f.render_widget(line_l, header_chunks[0]);
let mut line_r = Line::default();
if let Some(percent) =
get_symbol(state.right_obj.as_ref(), self.right_sym).and_then(|(_, d)| d.match_percent)
if let Some(percent) = get_symbol(state.right_obj.as_ref(), self.right_sym)
.and_then(|(_, _, d)| d.match_percent)
{
line_r.spans.push(Span::styled(
format!("{:.2}% ", percent),
@@ -106,13 +108,17 @@ impl UiView for FunctionDiffUi {
let mut left_text = None;
let mut left_highlight = None;
let mut max_width = 0;
if let Some((symbol, symbol_diff)) = get_symbol(state.left_obj.as_ref(), self.left_sym) {
if let Some((obj, symbol_idx, symbol_diff)) =
get_symbol(state.left_obj.as_ref(), self.left_sym)
{
let mut text = Text::default();
let rect = content_chunks[0].inner(Margin::new(0, 1));
left_highlight = self.print_sym(
&mut text,
symbol,
obj,
symbol_idx,
symbol_diff,
&state.diff_obj_config,
rect,
&self.left_highlight,
result,
@@ -125,13 +131,17 @@ impl UiView for FunctionDiffUi {
let mut right_text = None;
let mut right_highlight = None;
let mut margin_text = None;
if let Some((symbol, symbol_diff)) = get_symbol(state.right_obj.as_ref(), self.right_sym) {
if let Some((obj, symbol_idx, symbol_diff)) =
get_symbol(state.right_obj.as_ref(), self.right_sym)
{
let mut text = Text::default();
let rect = content_chunks[2].inner(Margin::new(0, 1));
right_highlight = self.print_sym(
&mut text,
symbol,
obj,
symbol_idx,
symbol_diff,
&state.diff_obj_config,
rect,
&self.right_highlight,
result,
@@ -150,14 +160,17 @@ impl UiView for FunctionDiffUi {
let mut prev_text = None;
let mut prev_margin_text = None;
if self.three_way {
if let Some((symbol, symbol_diff)) = get_symbol(state.prev_obj.as_ref(), self.prev_sym)
if let Some((obj, symbol_idx, symbol_diff)) =
get_symbol(state.prev_obj.as_ref(), self.prev_sym)
{
let mut text = Text::default();
let rect = content_chunks[4].inner(Margin::new(0, 1));
self.print_sym(
&mut text,
symbol,
obj,
symbol_idx,
symbol_diff,
&state.diff_obj_config,
rect,
&self.right_highlight,
result,
@@ -435,9 +448,11 @@ impl UiView for FunctionDiffUi {
get_symbol(state.left_obj.as_ref(), left_sym),
get_symbol(state.right_obj.as_ref(), right_sym),
) {
(Some((_l, ld)), Some((_r, rd))) => ld.instructions.len().max(rd.instructions.len()),
(Some((_l, ld)), None) => ld.instructions.len(),
(None, Some((_r, rd))) => rd.instructions.len(),
(Some((_l, _ls, ld)), Some((_r, _rs, rd))) => {
ld.instruction_rows.len().max(rd.instruction_rows.len())
}
(Some((_l, _ls, ld)), None) => ld.instruction_rows.len(),
(None, Some((_r, _rs, rd))) => rd.instruction_rows.len(),
(None, None) => bail!("Symbol not found: {}", self.symbol_name),
};
self.left_sym = left_sym;
@@ -480,113 +495,82 @@ impl FunctionDiffUi {
self.scroll_y += self.per_page / if half { 2 } else { 1 };
}
#[allow(clippy::too_many_arguments)]
fn print_sym(
&self,
out: &mut Text<'static>,
symbol: &ObjSymbol,
symbol_diff: &ObjSymbolDiff,
obj: &Object,
symbol_index: usize,
symbol_diff: &SymbolDiff,
diff_config: &DiffObjConfig,
rect: Rect,
highlight: &HighlightKind,
result: &EventResult,
only_changed: bool,
) -> Option<HighlightKind> {
let base_addr = symbol.address;
let mut new_highlight = None;
for (y, ins_diff) in symbol_diff
.instructions
for (y, ins_row) in symbol_diff
.instruction_rows
.iter()
.skip(self.scroll_y)
.take(rect.height as usize)
.enumerate()
{
if only_changed && ins_diff.kind == ObjInsDiffKind::None {
if only_changed && ins_row.kind == InstructionDiffKind::None {
out.lines.push(Line::default());
continue;
}
let mut sx = rect.x;
let sy = rect.y + y as u16;
let mut line = Line::default();
display_diff(ins_diff, base_addr, |text| -> Result<()> {
let label_text;
let mut base_color = match ins_diff.kind {
ObjInsDiffKind::None
| ObjInsDiffKind::OpMismatch
| ObjInsDiffKind::ArgMismatch => Color::Gray,
ObjInsDiffKind::Replace => Color::Cyan,
ObjInsDiffKind::Delete => Color::Red,
ObjInsDiffKind::Insert => Color::Green,
};
let mut pad_to = 0;
match text {
DiffText::Basic(text) => {
label_text = text.to_string();
}
DiffText::BasicColor(s, idx) => {
label_text = s.to_string();
base_color = COLOR_ROTATION[idx % COLOR_ROTATION.len()];
}
DiffText::Line(num) => {
label_text = format!("{num} ");
base_color = Color::DarkGray;
pad_to = 5;
}
DiffText::Address(addr) => {
label_text = format!("{:x}:", addr);
pad_to = 5;
}
DiffText::Opcode(mnemonic, _op) => {
label_text = mnemonic.to_string();
if ins_diff.kind == ObjInsDiffKind::OpMismatch {
base_color = Color::Blue;
}
pad_to = 8;
}
DiffText::Argument(arg, diff) => {
label_text = arg.to_string();
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::BranchDest(addr, diff) => {
label_text = format!("{addr:x}");
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::Symbol(sym, diff) => {
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
label_text = name.clone();
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
} else {
base_color = Color::White;
}
display_row(obj, symbol_index, ins_row, diff_config, |segment| {
let highlight_kind = HighlightKind::from(&segment.text);
let label_text = match segment.text {
DiffText::Basic(text) => text.to_string(),
DiffText::Line(num) => format!("{num} "),
DiffText::Address(addr) => format!("{:x}:", addr),
DiffText::Opcode(mnemonic, _op) => format!("{mnemonic} "),
DiffText::Argument(arg) => arg.to_string(),
DiffText::BranchDest(addr) => format!("{addr:x}"),
DiffText::Symbol(sym) => {
sym.demangled_name.as_ref().unwrap_or(&sym.name).clone()
}
DiffText::Addend(addend) => match addend.cmp(&0i64) {
Ordering::Greater => format!("+{:#x}", addend),
Ordering::Less => format!("-{:#x}", -addend),
_ => String::new(),
},
DiffText::Spacing(n) => {
line.spans.push(Span::raw(" ".repeat(n)));
line.spans.push(Span::raw(" ".repeat(n as usize)));
sx += n as u16;
return Ok(());
}
DiffText::Eol => {
return Ok(());
}
}
DiffText::Eol => return Ok(()),
};
let len = label_text.len();
let highlighted = *highlight == text;
let highlighted =
highlight_kind != HighlightKind::None && *highlight == highlight_kind;
if let Some((cx, cy)) = result.click_xy {
if cx >= sx && cx < sx + len as u16 && cy == sy {
new_highlight = Some(text.into());
new_highlight = Some(highlight_kind);
}
}
let mut style = Style::new().fg(base_color);
let mut style = Style::new().fg(match segment.color {
DiffTextColor::Normal => Color::Gray,
DiffTextColor::Dim => Color::DarkGray,
DiffTextColor::Bright => Color::White,
DiffTextColor::Replace => Color::Cyan,
DiffTextColor::Delete => Color::Red,
DiffTextColor::Insert => Color::Green,
DiffTextColor::Rotating(i) => COLOR_ROTATION[i as usize % COLOR_ROTATION.len()],
});
if highlighted {
style = style.bg(Color::DarkGray);
}
line.spans.push(Span::styled(label_text, style));
sx += len as u16;
if pad_to > len {
let pad = (pad_to - len) as u16;
if segment.pad_to as usize > len {
let pad = (segment.pad_to as usize - len) as u16;
line.spans.push(Span::raw(" ".repeat(pad as usize)));
sx += pad;
}
@@ -598,12 +582,13 @@ impl FunctionDiffUi {
new_highlight
}
fn print_margin(&self, out: &mut Text, symbol: &ObjSymbolDiff, rect: Rect) {
for ins_diff in symbol.instructions.iter().skip(self.scroll_y).take(rect.height as usize) {
if ins_diff.kind != ObjInsDiffKind::None {
out.lines.push(Line::raw(match ins_diff.kind {
ObjInsDiffKind::Delete => "<",
ObjInsDiffKind::Insert => ">",
fn print_margin(&self, out: &mut Text, symbol: &SymbolDiff, rect: Rect) {
for ins_row in symbol.instruction_rows.iter().skip(self.scroll_y).take(rect.height as usize)
{
if ins_row.kind != InstructionDiffKind::None {
out.lines.push(Line::raw(match ins_row.kind {
InstructionDiffKind::Delete => "<",
InstructionDiffKind::Insert => ">",
_ => "|",
}));
} else {
@@ -635,23 +620,18 @@ pub fn match_percent_color(match_percent: f32) -> Color {
#[inline]
fn get_symbol(
obj: Option<&(ObjInfo, ObjDiff)>,
sym: Option<SymbolRef>,
) -> Option<(&ObjSymbol, &ObjSymbolDiff)> {
obj: Option<&(Object, ObjectDiff)>,
sym: Option<usize>,
) -> Option<(&Object, usize, &SymbolDiff)> {
let (obj, diff) = obj?;
let sym = sym?;
Some((obj.section_symbol(sym).1, diff.symbol_diff(sym)))
Some((obj, sym, &diff.symbols[sym]))
}
fn find_function(obj: &ObjInfo, name: &str) -> Option<SymbolRef> {
for (section_idx, section) in obj.sections.iter().enumerate() {
if section.kind != ObjSectionKind::Code {
continue;
}
for (symbol_idx, symbol) in section.symbols.iter().enumerate() {
if symbol.name == name {
return Some(SymbolRef { section_idx, symbol_idx });
}
fn find_function(obj: &Object, name: &str) -> Option<usize> {
for (symbol_idx, symbol) in obj.symbols.iter().enumerate() {
if symbol.name == name {
return Some(symbol_idx);
}
}
None
+59 -65
View File
@@ -12,54 +12,42 @@ A local diffing tool for decompilation projects.
"""
documentation = "https://docs.rs/objdiff-core"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["std"]
all = [
# Features
"bindings",
"build",
"config",
"dwarf",
"serde",
# Architectures
"arm",
"arm64",
"mips",
"ppc",
"x86",
"arm",
"arm64",
]
# Implicit, used to check if any arch is enabled
any-arch = [
"config",
"dep:bimap",
"dep:byteorder",
"dep:flagset",
"dep:heck",
"dep:log",
"dep:memmap2",
"dep:num-traits",
"dep:prettyplease",
"dep:proc-macro2",
"dep:quote",
"dep:serde",
"dep:serde_json",
"dep:regex",
"dep:similar",
"dep:strum",
"dep:syn",
]
bindings = [
"dep:pbjson",
"dep:pbjson-build",
"dep:prost",
"dep:prost-build",
"dep:serde",
"dep:serde_json",
]
build = [
"dep:notify",
"dep:notify-debouncer-full",
"dep:path-slash",
"dep:reqwest",
"dep:self_update",
"dep:shell-escape",
@@ -68,15 +56,29 @@ build = [
"dep:winapi",
]
config = [
"dep:bimap",
"dep:filetime",
"dep:globset",
"dep:semver",
"dep:serde",
"dep:serde_json",
"dep:serde_yaml",
"dep:typed-path",
]
dwarf = ["dep:gimli"]
serde = [
"dep:pbjson",
"dep:pbjson-build",
"dep:serde",
"dep:serde_json",
]
std = [
"anyhow/std",
"flagset?/std",
"log?/std",
"num-traits?/std",
"object/std",
"prost?/std",
"serde?/std",
"typed-path?/std",
"dep:filetime",
"dep:memmap2",
]
mips = [
"any-arch",
"dep:rabbitizer",
@@ -86,6 +88,7 @@ ppc = [
"dep:cwdemangle",
"dep:cwextab",
"dep:ppc750cl",
"dep:rlwinmdec",
]
x86 = [
"any-arch",
@@ -105,78 +108,64 @@ arm64 = [
"dep:yaxpeax-arch",
"dep:yaxpeax-arm",
]
wasm = [
"any-arch",
"bindings",
"dep:console_error_panic_hook",
"dep:console_log",
"dep:log",
"dep:tsify-next",
"dep:wasm-bindgen",
]
[package.metadata.docs.rs]
features = ["all"]
[dependencies]
anyhow = "1.0"
bimap = { version = "0.6", features = ["serde"], optional = true }
byteorder = { version = "1.5", optional = true }
anyhow = { version = "1.0", default-features = false }
filetime = { version = "0.2", optional = true }
flagset = { version = "0.4", optional = true }
log = { version = "0.4", optional = true }
flagset = { version = "0.4", default-features = false, optional = true, git = "https://github.com/enarx/flagset.git", rev = "a1fe9369b3741e43fec45da1998e83b9d78966a2" }
itertools = { version = "0.14", default-features = false, features = ["use_alloc"] }
log = { version = "0.4", default-features = false, optional = true }
memmap2 = { version = "0.9", optional = true }
num-traits = { version = "0.2", optional = true }
object = { version = "0.36", features = ["read_core", "std", "elf", "pe"], default-features = false }
pbjson = { version = "0.7", optional = true }
prost = { version = "0.13", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
similar = { version = "2.6", default-features = false, optional = true }
strum = { version = "0.26", features = ["derive"], optional = true }
wasm-bindgen = { version = "0.2", optional = true }
tsify-next = { version = "0.5", default-features = false, features = ["js"], optional = true }
console_log = { version = "1.0", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }
num-traits = { version = "0.2", default-features = false, optional = true }
object = { version = "0.36", default-features = false, features = ["read_core", "elf", "pe"] }
pbjson = { version = "0.7", default-features = false, optional = true }
prost = { version = "0.13", default-features = false, features = ["prost-derive"], optional = true }
regex = { version = "1.11", default-features = false, features = [], optional = true }
serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }
similar = { version = "2.7", default-features = false, optional = true, git = "https://github.com/encounter/similar.git", branch = "no_std" }
typed-path = { version = "0.10", default-features = false, optional = true }
# config
globset = { version = "0.4", features = ["serde1"], optional = true }
semver = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }
globset = { version = "0.4", default-features = false, optional = true }
semver = { version = "1.0", default-features = false, optional = true }
serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true }
# dwarf
gimli = { version = "0.31", default-features = false, features = ["read-all"], optional = true }
gimli = { version = "0.31", default-features = false, features = ["read"], optional = true }
# ppc
cwdemangle = { version = "1.0", optional = true }
cwextab = { version = "1.0", optional = true }
cwextab = { version = "1.0", optional = true, git = "https://github.com/CelestialAmber/cwextab.git" }
ppc750cl = { version = "0.3", optional = true }
rlwinmdec = { version = "1.1", optional = true, git = "https://github.com/CelestialAmber/rlwinmdec.git" }
# mips
rabbitizer = { version = "1.12", optional = true }
rabbitizer = { git = "https://github.com/Decompollaborate/rabbitizer.git", branch = "🦀", default-features = false, features = ["all_extensions"], optional = true }
# x86
cpp_demangle = { version = "0.4", optional = true }
iced-x86 = { version = "1.21", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], optional = true }
msvc-demangler = { version = "0.10", optional = true }
cpp_demangle = { version = "0.4", default-features = false, features = ["alloc"], optional = true }
iced-x86 = { version = "1.21", default-features = false, features = ["decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums", "no_std"], optional = true }
msvc-demangler = { version = "0.11", optional = true }
# arm
unarm = { version = "1.6", optional = true }
arm-attr = { version = "0.1", optional = true }
unarm = { version = "1.7", optional = true }
arm-attr = { version = "0.2", optional = true }
# arm64
yaxpeax-arch = { version = "0.3", default-features = false, features = ["std"], optional = true }
yaxpeax-arm = { version = "0.3", default-features = false, features = ["std"], optional = true }
yaxpeax-arch = { version = "0.3", default-features = false, optional = true }
yaxpeax-arm = { version = "0.3", default-features = false, optional = true }
# build
notify = { version = "8.0.0", optional = true }
notify-debouncer-full = { version = "0.5.0", optional = true }
shell-escape = { version = "0.1", optional = true }
tempfile = { version = "3.15", optional = true }
tempfile = { version = "3.17", optional = true }
time = { version = "0.3", optional = true }
[target.'cfg(windows)'.dependencies]
path-slash = { version = "0.2", optional = true }
winapi = { version = "0.3", optional = true }
# For Linux static binaries, use rustls
@@ -196,6 +185,11 @@ prettyplease = { version = "0.2", optional = true }
proc-macro2 = { version = "1.0", optional = true }
prost-build = { version = "0.13", optional = true }
quote = { version = "1.0", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
serde_json = { version = "1.0", optional = true }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }
syn = { version = "2.0", optional = true }
[dev-dependencies]
# Enable all features for tests
objdiff-core = { path = ".", features = ["all"] }
insta = "1.42"
+13 -9
View File
@@ -1,11 +1,12 @@
#[cfg(feature = "any-arch")]
mod config_gen;
fn main() {
fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "bindings")]
compile_protos();
#[cfg(feature = "any-arch")]
config_gen::generate_diff_config();
Ok(())
}
#[cfg(feature = "bindings")]
@@ -18,7 +19,7 @@ fn compile_protos() {
.map(|m| m.modified().unwrap())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
let mut run_protoc = false;
let proto_files = vec![root.join("diff.proto"), root.join("report.proto")];
let proto_files = vec![root.join("report.proto")];
for proto_file in &proto_files {
println!("cargo:rerun-if-changed={}", proto_file.display());
let mtime = match std::fs::metadata(proto_file) {
@@ -54,11 +55,14 @@ fn compile_protos() {
}
}
let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set");
pbjson_build::Builder::new()
.register_descriptors(&descriptor_set)
.expect("Failed to register descriptors")
.preserve_proto_field_names()
.build(&[".objdiff"])
.expect("Failed to build pbjson");
#[cfg(feature = "serde")]
{
let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set");
pbjson_build::Builder::new()
.register_descriptors(&descriptor_set)
.expect("Failed to register descriptors")
.preserve_proto_field_names()
.build(&[".objdiff"])
.expect("Failed to build pbjson");
}
}
+9 -1
View File
@@ -39,6 +39,13 @@
"name": "Combine data sections",
"description": "Combines data sections with equal names."
},
{
"id": "combineTextSections",
"type": "boolean",
"default": false,
"name": "Combine text sections",
"description": "Combines all text sections into one."
},
{
"id": "arm.archVersion",
"type": "choice",
@@ -213,7 +220,8 @@
"properties": [
"functionRelocDiffs",
"spaceBetweenArgs",
"combineDataSections"
"combineDataSections",
"combineTextSections"
]
},
{
+18 -9
View File
@@ -100,7 +100,7 @@ pub fn generate_diff_config() {
}
let value = &item.value;
variants.extend(quote! {
#[serde(rename = #value, alias = #variant_name)]
#[cfg_attr(feature = "serde", serde(rename = #value, alias = #variant_name))]
#variant_ident,
});
full_variants.extend(quote! { #enum_ident::#variant_ident, });
@@ -134,8 +134,8 @@ pub fn generate_diff_config() {
});
}
enums.extend(quote! {
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum #enum_ident {
#variants
}
@@ -168,7 +168,7 @@ pub fn generate_diff_config() {
}
}
}
impl std::str::FromStr for #enum_ident {
impl core::str::FromStr for #enum_ident {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
#variant_from_str
@@ -244,7 +244,7 @@ pub fn generate_diff_config() {
let default = b.default;
if default {
property_fields.extend(quote! {
#[serde(default = "default_true")]
#[cfg_attr(feature = "serde", serde(default = "default_true"))]
});
}
property_fields.extend(quote! {
@@ -412,7 +412,7 @@ pub fn generate_diff_config() {
}
}
}
impl std::str::FromStr for ConfigPropertyId {
impl core::str::FromStr for ConfigPropertyId {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
#config_property_id_from_str
@@ -433,6 +433,7 @@ pub fn generate_diff_config() {
Choice(&'static str),
}
impl ConfigPropertyValue {
#[cfg(feature = "serde")]
pub fn to_json(&self) -> serde_json::Value {
match self {
ConfigPropertyValue::Boolean(value) => serde_json::Value::Bool(*value),
@@ -440,17 +441,25 @@ pub fn generate_diff_config() {
}
}
}
impl core::fmt::Display for ConfigPropertyValue {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ConfigPropertyValue::Boolean(value) => write!(f, "{}", value),
ConfigPropertyValue::Choice(value) => write!(f, "{}", value),
}
}
}
#[derive(Clone, Debug)]
pub enum ConfigPropertyKind {
Boolean,
Choice(&'static [ConfigEnumVariantInfo]),
}
#enums
#[cfg(feature = "serde")]
#[inline(always)]
fn default_true() -> bool { true }
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
#[serde(default)]
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize), serde(default))]
pub struct DiffObjConfig {
#property_fields
}
+7 -12
View File
@@ -87,11 +87,11 @@ message Relocation {
}
message RelocationTarget {
Symbol symbol = 1;
uint32 symbol_index = 1;
int64 addend = 2;
}
message InstructionDiff {
message InstructionDiffRow {
DiffKind diff_kind = 1;
optional Instruction instruction = 2;
optional InstructionBranchFrom branch_from = 3;
@@ -122,17 +122,12 @@ message InstructionBranchTo {
uint32 branch_index = 2;
}
message SymbolRef {
optional uint32 section_index = 1;
uint32 symbol_index = 2;
}
message SymbolDiff {
Symbol symbol = 1;
repeated InstructionDiff instructions = 2;
repeated InstructionDiffRow instruction_rows = 2;
optional float match_percent = 3;
// The symbol ref in the _other_ object that this symbol was diffed against
optional SymbolRef target = 5;
// The symbol index in the _other_ object that this symbol was diffed against
optional uint32 target_symbol = 5;
}
message DataDiff {
@@ -147,7 +142,7 @@ message SectionDiff {
SectionKind kind = 2;
uint64 size = 3;
uint64 address = 4;
repeated SymbolDiff symbols = 5;
reserved 5;
repeated DataDiff data = 6;
optional float match_percent = 7;
}
@@ -157,11 +152,11 @@ enum SectionKind {
SECTION_TEXT = 1;
SECTION_DATA = 2;
SECTION_BSS = 3;
SECTION_COMMON = 4;
}
message ObjectDiff {
repeated SectionDiff sections = 1;
repeated SymbolDiff symbols = 2;
}
message DiffResult {
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+199 -78
View File
@@ -1,19 +1,25 @@
use std::{borrow::Cow, collections::BTreeMap, ffi::CStr};
use alloc::{borrow::Cow, boxed::Box, format, string::String, vec::Vec};
use core::{ffi::CStr, fmt, fmt::Debug};
use anyhow::{bail, Result};
use byteorder::ByteOrder;
use object::{Architecture, File, Object, ObjectSymbol, Relocation, RelocationFlags, Symbol};
use object::Endian as _;
use crate::{
diff::DiffObjConfig,
obj::{ObjIns, ObjReloc, ObjSection},
diff::{
display::{ContextItem, HoverItem, InstructionPart},
DiffObjConfig,
},
obj::{
InstructionArg, Object, ParsedInstruction, RelocationFlags, ResolvedInstructionRef,
ScannedInstruction, SymbolFlagSet, SymbolKind,
},
util::ReallySigned,
};
#[cfg(feature = "arm")]
mod arm;
pub mod arm;
#[cfg(feature = "arm64")]
mod arm64;
pub mod arm64;
#[cfg(feature = "mips")]
pub mod mips;
#[cfg(feature = "ppc")]
@@ -27,18 +33,41 @@ pub enum DataType {
Int16,
Int32,
Int64,
Int128,
Float,
Double,
Bytes,
String,
}
impl fmt::Display for DataType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DataType::Int8 => write!(f, "Int8"),
DataType::Int16 => write!(f, "Int16"),
DataType::Int32 => write!(f, "Int32"),
DataType::Int64 => write!(f, "Int64"),
DataType::Float => write!(f, "Float"),
DataType::Double => write!(f, "Double"),
DataType::Bytes => write!(f, "Bytes"),
DataType::String => write!(f, "String"),
}
}
}
impl DataType {
pub fn display_bytes<Endian: ByteOrder>(&self, bytes: &[u8]) -> Option<String> {
pub fn display_labels(&self, endian: object::Endianness, bytes: &[u8]) -> Vec<String> {
let mut strs = Vec::new();
for literal in self.display_literals(endian, bytes) {
strs.push(format!("{}: {}", self, literal))
}
strs
}
pub fn display_literals(&self, endian: object::Endianness, bytes: &[u8]) -> Vec<String> {
let mut strs = Vec::new();
if self.required_len().is_some_and(|l| bytes.len() < l) {
log::warn!("Failed to display a symbol value for a symbol whose size is too small for instruction referencing it.");
return None;
return strs;
}
let mut bytes = bytes;
if self.required_len().is_some_and(|l| bytes.len() > l) {
@@ -56,58 +85,61 @@ impl DataType {
match self {
DataType::Int8 => {
let i = i8::from_ne_bytes(bytes.try_into().unwrap());
strs.push(format!("{:#x}", i));
if i < 0 {
format!("Int8: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int8: {:#x}", i)
strs.push(format!("{:#x}", ReallySigned(i)));
}
}
DataType::Int16 => {
let i = Endian::read_i16(bytes);
let i = endian.read_i16_bytes(bytes.try_into().unwrap());
strs.push(format!("{:#x}", i));
if i < 0 {
format!("Int16: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int16: {:#x}", i)
strs.push(format!("{:#x}", ReallySigned(i)));
}
}
DataType::Int32 => {
let i = Endian::read_i32(bytes);
let i = endian.read_i32_bytes(bytes.try_into().unwrap());
strs.push(format!("{:#x}", i));
if i < 0 {
format!("Int32: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int32: {:#x}", i)
strs.push(format!("{:#x}", ReallySigned(i)));
}
}
DataType::Int64 => {
let i = Endian::read_i64(bytes);
let i = endian.read_i64_bytes(bytes.try_into().unwrap());
strs.push(format!("{:#x}", i));
if i < 0 {
format!("Int64: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int64: {:#x}", i)
}
}
DataType::Int128 => {
let i = Endian::read_i128(bytes);
if i < 0 {
format!("Int128: {:#x} ({:#x})", i, ReallySigned(i))
} else {
format!("Int128: {:#x}", i)
strs.push(format!("{:#x}", ReallySigned(i)));
}
}
DataType::Float => {
format!("Float: {:?}f", Endian::read_f32(bytes))
let bytes: [u8; 4] = bytes.try_into().unwrap();
strs.push(format!("{:?}f", match endian {
object::Endianness::Little => f32::from_le_bytes(bytes),
object::Endianness::Big => f32::from_be_bytes(bytes),
}));
}
DataType::Double => {
format!("Double: {:?}", Endian::read_f64(bytes))
let bytes: [u8; 8] = bytes.try_into().unwrap();
strs.push(format!("{:?}f", match endian {
object::Endianness::Little => f64::from_le_bytes(bytes),
object::Endianness::Big => f64::from_be_bytes(bytes),
}));
}
DataType::Bytes => {
format!("Bytes: {:#?}", bytes)
strs.push(format!("{:#?}", bytes));
}
DataType::String => {
format!("String: {:?}", CStr::from_bytes_until_nul(bytes).ok()?)
if let Ok(cstr) = CStr::from_bytes_until_nul(bytes) {
strs.push(format!("{:?}", cstr));
}
}
}
.into()
strs
}
fn required_len(&self) -> Option<usize> {
@@ -116,7 +148,6 @@ impl DataType {
DataType::Int16 => Some(2),
DataType::Int32 => Some(4),
DataType::Int64 => Some(8),
DataType::Int128 => Some(16),
DataType::Float => Some(4),
DataType::Double => Some(8),
DataType::Bytes => None,
@@ -125,72 +156,162 @@ impl DataType {
}
}
pub trait ObjArch: Send + Sync {
fn process_code(
pub trait Arch: Send + Sync + Debug {
/// Generate a list of instructions references (offset, size, opcode) from the given code.
///
/// The opcode IDs are used to generate the initial diff. Implementations should do as little
/// parsing as possible here: just enough to identify the base instruction opcode, size, and
/// possible branch destination (for visual representation). As needed, instructions are parsed
/// via `process_instruction` to compare their arguments.
fn scan_instructions(
&self,
address: u64,
code: &[u8],
section_index: usize,
relocations: &[ObjReloc],
line_info: &BTreeMap<u64, u32>,
config: &DiffObjConfig,
) -> Result<ProcessCodeResult>;
diff_config: &DiffObjConfig,
) -> Result<Vec<ScannedInstruction>>;
/// Parse an instruction to gather its mnemonic and arguments for more detailed comparison.
///
/// This is called only when we need to compare the arguments of an instruction.
fn process_instruction(
&self,
resolved: ResolvedInstructionRef,
diff_config: &DiffObjConfig,
) -> Result<ParsedInstruction> {
let mut mnemonic = None;
let mut args = Vec::with_capacity(8);
self.display_instruction(resolved, diff_config, &mut |part| {
match part {
InstructionPart::Opcode(m, _) => mnemonic = Some(Cow::Owned(m.into_owned())),
InstructionPart::Arg(arg) => args.push(arg.into_static()),
_ => {}
}
Ok(())
})?;
// If the instruction has a relocation, but we didn't format it in the display, add it to
// the end of the arguments list.
if resolved.relocation.is_some() && !args.contains(&InstructionArg::Reloc) {
args.push(InstructionArg::Reloc);
}
Ok(ParsedInstruction {
ins_ref: resolved.ins_ref,
mnemonic: mnemonic.unwrap_or_default(),
args,
})
}
/// Format an instruction for display.
///
/// Implementations should call the callback for each part of the instruction: usually the
/// mnemonic and arguments, plus any separators and visual formatting.
fn display_instruction(
&self,
resolved: ResolvedInstructionRef,
diff_config: &DiffObjConfig,
cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
) -> Result<()>;
fn implcit_addend(
&self,
file: &File<'_>,
section: &ObjSection,
file: &object::File<'_>,
section: &object::Section,
address: u64,
reloc: &Relocation,
relocation: &object::Relocation,
flags: RelocationFlags,
) -> Result<i64>;
fn demangle(&self, _name: &str) -> Option<String> { None }
fn display_reloc(&self, flags: RelocationFlags) -> Cow<'static, str>;
fn reloc_name(&self, _flags: RelocationFlags) -> Option<&'static str> { None }
fn get_reloc_byte_size(&self, flags: RelocationFlags) -> usize;
fn data_reloc_size(&self, flags: RelocationFlags) -> usize;
fn symbol_address(&self, symbol: &Symbol) -> u64 { symbol.address() }
fn symbol_address(&self, address: u64, _kind: SymbolKind) -> u64 { address }
fn guess_data_type(&self, _instruction: &ObjIns) -> Option<DataType> { None }
fn display_data_type(&self, _ty: DataType, bytes: &[u8]) -> Option<String> {
Some(format!("Bytes: {:#x?}", bytes))
fn extra_symbol_flags(&self, _symbol: &object::Symbol) -> SymbolFlagSet {
SymbolFlagSet::default()
}
fn display_ins_data(&self, ins: &ObjIns) -> Option<String> {
let reloc = ins.reloc.as_ref()?;
if reloc.addend >= 0 && reloc.target.bytes.len() > reloc.addend as usize {
self.guess_data_type(ins).and_then(|ty| {
self.display_data_type(ty, &reloc.target.bytes[reloc.addend as usize..])
})
} else {
None
}
fn guess_data_type(&self, _resolved: ResolvedInstructionRef) -> Option<DataType> { None }
fn symbol_hover(&self, _obj: &Object, _symbol_index: usize) -> Vec<HoverItem> { Vec::new() }
fn symbol_context(&self, _obj: &Object, _symbol_index: usize) -> Vec<ContextItem> { Vec::new() }
fn instruction_hover(
&self,
_obj: &Object,
_resolved: ResolvedInstructionRef,
) -> Vec<HoverItem> {
Vec::new()
}
// Downcast methods
#[cfg(feature = "ppc")]
fn ppc(&self) -> Option<&ppc::ObjArchPpc> { None }
fn instruction_context(
&self,
_obj: &Object,
_resolved: ResolvedInstructionRef,
) -> Vec<ContextItem> {
Vec::new()
}
}
pub struct ProcessCodeResult {
pub ops: Vec<u16>,
pub insts: Vec<ObjIns>,
}
pub fn new_arch(object: &File) -> Result<Box<dyn ObjArch>> {
pub fn new_arch(object: &object::File) -> Result<Box<dyn Arch>> {
use object::Object as _;
Ok(match object.architecture() {
#[cfg(feature = "ppc")]
Architecture::PowerPc => Box::new(ppc::ObjArchPpc::new(object)?),
object::Architecture::PowerPc => Box::new(ppc::ArchPpc::new(object)?),
#[cfg(feature = "mips")]
Architecture::Mips => Box::new(mips::ObjArchMips::new(object)?),
object::Architecture::Mips => Box::new(mips::ArchMips::new(object)?),
#[cfg(feature = "x86")]
Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?),
object::Architecture::I386 | object::Architecture::X86_64 => {
Box::new(x86::ArchX86::new(object)?)
}
#[cfg(feature = "arm")]
Architecture::Arm => Box::new(arm::ObjArchArm::new(object)?),
object::Architecture::Arm => Box::new(arm::ArchArm::new(object)?),
#[cfg(feature = "arm64")]
Architecture::Aarch64 => Box::new(arm64::ObjArchArm64::new(object)?),
object::Architecture::Aarch64 => Box::new(arm64::ArchArm64::new(object)?),
arch => bail!("Unsupported architecture: {arch:?}"),
})
}
#[derive(Debug, Default)]
pub struct ArchDummy {}
impl ArchDummy {
pub fn new() -> Box<Self> { Box::new(Self {}) }
}
impl Arch for ArchDummy {
fn scan_instructions(
&self,
_address: u64,
_code: &[u8],
_section_index: usize,
_diff_config: &DiffObjConfig,
) -> Result<Vec<ScannedInstruction>> {
Ok(Vec::new())
}
fn display_instruction(
&self,
_resolved: ResolvedInstructionRef,
_diff_config: &DiffObjConfig,
_cb: &mut dyn FnMut(InstructionPart) -> Result<()>,
) -> Result<()> {
Ok(())
}
fn implcit_addend(
&self,
_file: &object::File<'_>,
_section: &object::Section,
_address: u64,
_relocation: &object::Relocation,
_flags: RelocationFlags,
) -> Result<i64> {
Ok(0)
}
fn data_reloc_size(&self, _flags: RelocationFlags) -> usize { 0 }
}

Some files were not shown because too many files have changed in this diff Show More