From e8de35b78e4b9c196d9b4c7aae5ff74447906e99 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 7 Feb 2025 00:10:49 -0700 Subject: [PATCH] Make objdiff-core no_std + huge WASM rework --- Cargo.lock | 320 +++++++++++++++--------- objdiff-cli/Cargo.toml | 1 + objdiff-cli/src/cmd/diff.rs | 230 +++++++++-------- objdiff-cli/src/cmd/report.rs | 137 +++++----- objdiff-cli/src/util/output.rs | 9 +- objdiff-core/Cargo.toml | 108 ++++---- objdiff-core/build.rs | 17 +- objdiff-core/config_gen.rs | 27 +- objdiff-core/src/arch/arm.rs | 20 +- objdiff-core/src/arch/arm64.rs | 10 +- objdiff-core/src/arch/mips.rs | 3 +- objdiff-core/src/arch/mod.rs | 3 +- objdiff-core/src/arch/ppc.rs | 18 +- objdiff-core/src/arch/x86.rs | 10 +- objdiff-core/src/bindings/diff.rs | 4 + objdiff-core/src/bindings/mod.rs | 2 - objdiff-core/src/bindings/report.rs | 55 ++-- objdiff-core/src/bindings/wasm.rs | 81 ------ objdiff-core/src/build/mod.rs | 11 +- objdiff-core/src/config/mod.rs | 226 ++++++++--------- objdiff-core/src/config/path.rs | 65 +++++ objdiff-core/src/diff/code.rs | 14 +- objdiff-core/src/diff/data.rs | 22 +- objdiff-core/src/diff/display.rs | 2 + objdiff-core/src/diff/mod.rs | 28 ++- objdiff-core/src/jobs/create_scratch.rs | 18 +- objdiff-core/src/jobs/objdiff.rs | 39 ++- objdiff-core/src/lib.rs | 5 + objdiff-core/src/obj/mod.rs | 9 +- objdiff-core/src/obj/read.rs | 83 +++--- objdiff-core/src/obj/split_meta.rs | 50 ++-- objdiff-core/src/util.rs | 31 ++- objdiff-core/src/wasm/api.rs | 99 ++++++++ objdiff-core/src/wasm/cabi_realloc.rs | 64 +++++ objdiff-core/src/wasm/mod.rs | 18 ++ objdiff-core/wit/objdiff.wit | 29 +++ objdiff-gui/Cargo.toml | 3 +- objdiff-gui/src/app.rs | 112 ++++++--- objdiff-gui/src/app_config.rs | 84 ++++--- objdiff-gui/src/config.rs | 66 +++-- objdiff-gui/src/jobs.rs | 2 +- objdiff-gui/src/views/config.rs | 80 +++--- objdiff-gui/src/views/file.rs | 15 +- objdiff-gui/src/views/symbol_diff.rs | 10 +- objdiff-wasm/package.json | 2 +- objdiff-wasm/src/display.ts | 107 ++++++++ objdiff-wasm/src/main.ts | 132 ++-------- objdiff-wasm/src/worker.ts | 27 +- objdiff-wasm/tsup.config.ts | 1 + 49 files changed, 1463 insertions(+), 1046 deletions(-) delete mode 100644 objdiff-core/src/bindings/wasm.rs create mode 100644 objdiff-core/src/config/path.rs create mode 100644 objdiff-core/src/wasm/api.rs create mode 100644 objdiff-core/src/wasm/cabi_realloc.rs create mode 100644 objdiff-core/src/wasm/mod.rs create mode 100644 objdiff-core/wit/objdiff.wit create mode 100644 objdiff-wasm/src/display.ts diff --git a/Cargo.lock b/Cargo.lock index b272296..9e2d3a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -152,11 +152,11 @@ dependencies = [ [[package]] name = "arm-attr" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d0cabd3a7d2dfa96ab3faa7b532a83c5e090061bf6d83197ca2bc91f5afac6c" +checksum = "5790583a9ebcc63c55f142a85d85fe63ec0e033c941fcf00a3605f81dada2e32" dependencies = [ - "thiserror 1.0.69", + "thiserror 2.0.11", ] [[package]] @@ -395,15 +395,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bimap" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" -dependencies = [ - "serde", -] - [[package]] name = "bit-set" version = "0.8.0" @@ -688,16 +679,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "console_log" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8aed40e4edbf4d3b4431ab260b63fdc40f5780a4766824329ea0f1eefe3c0f" -dependencies = [ - "log", - "web-sys", -] - [[package]] name = "const_format" version = "0.2.34" @@ -862,11 +843,10 @@ checksum = "c2e06f9bce634a3c898eb1e5cb949ff63133cbb218af93cc9b38b31d6f3ea285" [[package]] name = "cwextab" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003567b96ff9d8ac3275831650385891bca370092937be625157778b1e58f755" +version = "1.0.4" +source = "git+https://github.com/encounter/cwextab.git#15c344ac3302c32adbb8777c70f5ce739f432a6b" dependencies = [ - "thiserror 1.0.69", + "thiserror 2.0.11", ] [[package]] @@ -1336,12 +1316,6 @@ dependencies = [ "libc", ] -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - [[package]] name = "fastrand" version = "2.3.0" @@ -1632,10 +1606,6 @@ name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -dependencies = [ - "fallible-iterator", - "stable_deref_trait", -] [[package]] name = "gl_generator" @@ -2112,6 +2082,12 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + [[package]] name = "ident_case" version = "1.0.1" @@ -2159,6 +2135,7 @@ checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", + "serde", ] [[package]] @@ -2342,6 +2319,15 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" @@ -2539,11 +2525,12 @@ dependencies = [ [[package]] name = "msvc-demangler" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4c25a3bb7d880e8eceab4822f3141ad0700d20f025991c1f03bd3d00219a5fc" +checksum = "fbeff6bd154a309b2ada5639b2661ca6ae4599b34e8487dc276d2cd637da2d76" dependencies = [ "bitflags 2.8.0", + "itoa", ] [[package]] @@ -3000,6 +2987,7 @@ dependencies = [ "time", "tracing", "tracing-subscriber", + "typed-path", ] [[package]] @@ -3008,10 +2996,7 @@ version = "2.7.1" dependencies = [ "anyhow", "arm-attr", - "bimap", "byteorder", - "console_error_panic_hook", - "console_log", "cpp_demangle", "cwdemangle", "cwextab", @@ -3043,17 +3028,18 @@ dependencies = [ "semver", "serde", "serde_json", - "serde_yaml", "shell-escape", "similar", + "spin", "strum", "syn 2.0.96", + "talc", "tempfile", "time", - "tsify-next", + "typed-path", "unarm", - "wasm-bindgen", "winapi", + "wit-bindgen", "yaxpeax-arch", "yaxpeax-arm", ] @@ -3095,6 +3081,7 @@ dependencies = [ "time", "tracing-subscriber", "tracing-wasm", + "typed-path", "wgpu", "winapi", ] @@ -4097,17 +4084,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-wasm-bindgen" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" -dependencies = [ - "js-sys", - "serde", - "wasm-bindgen", -] - [[package]] name = "serde_derive" version = "1.0.217" @@ -4119,17 +4095,6 @@ dependencies = [ "syn 2.0.96", ] -[[package]] -name = "serde_derive_internals" -version = "0.29.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - [[package]] name = "serde_json" version = "1.0.135" @@ -4174,19 +4139,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -4246,9 +4198,11 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "similar" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" +version = "2.7.0" +source = "git+https://github.com/encounter/similar.git?branch=no_std#20f3537abfefa4cfb8c4b981c8aab99c7b53130d" +dependencies = [ + "hashbrown", +] [[package]] name = "slab" @@ -4329,11 +4283,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spdx" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b69356da67e2fc1f542c71ea7e654a361a79c938e4424392ecf4fa065d2193" +dependencies = [ + "smallvec", +] + [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] [[package]] name = "spirv" @@ -4441,6 +4407,15 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "talc" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fcad3be1cfe36eb7d716a04791eba36a197da9d9b6ea1e28e64ac569da3701d" +dependencies = [ + "lock_api", +] + [[package]] name = "tap" version = "1.0.1" @@ -4786,30 +4761,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "tsify-next" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4a645dca4ee0800f5ab60ce166deba2db6a0315de795a2691e138a3d55d756" -dependencies = [ - "serde", - "serde-wasm-bindgen", - "tsify-next-macros", - "wasm-bindgen", -] - -[[package]] -name = "tsify-next-macros" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d5c06f8a51d759bb58129e30b2631739e7e1e4579fad1f30ac09a6c88e488a6" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.96", -] - [[package]] name = "ttf-parser" version = "0.25.1" @@ -4825,6 +4776,12 @@ dependencies = [ "rustc-hash 1.1.0", ] +[[package]] +name = "typed-path" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41713888c5ccfd99979fcd1afd47b71652e331b3d4a0e19d30769e80fec76cce" + [[package]] name = "uds_windows" version = "1.1.0" @@ -4838,9 +4795,9 @@ dependencies = [ [[package]] name = "unarm" -version = "1.6.7" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34c5159ddf1e715c3144fd31571bd2d9b63ac70ce1df51d7145ade4532d0c78" +checksum = "dff0b9c752e29548c4bf614faa49a5f2d222c6333f9f70e3eb5cd84387f6a333" [[package]] name = "unicase" @@ -4889,12 +4846,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - [[package]] name = "untrusted" version = "0.9.0" @@ -5065,6 +5016,45 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.224.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7249cf8cb0c6b9cb42bce90c0a5feb276fbf963fa385ff3d818ab3d90818ed6" +dependencies = [ + "leb128", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.224.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79d13d93febc749413cb6f327e4fdba8c84e4d03bd69fcc4a220c66f113c8de1" +dependencies = [ + "anyhow", + "indexmap", + "serde", + "serde_derive", + "serde_json", + "spdx", + "url", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.224.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65881a664fdd43646b647bb27bf186ab09c05bf56779d40aed4c6dce47d423f5" +dependencies = [ + "bitflags 2.8.0", + "hashbrown", + "indexmap", + "semver", +] + [[package]] name = "wayland-backend" version = "0.3.7" @@ -5750,6 +5740,104 @@ dependencies = [ "winapi", ] +[[package]] +name = "wit-bindgen" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b550e454e4cce8984398539a94a0226511e1f295b14afdc8f08b4e2e2ff9de3a" +dependencies = [ + "wit-bindgen-rt", + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e2f98d49960a416074c5d72889f810ed3032a32ffef5e4760094426fefbfe8" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6f8d372a2d4a1227f2556e051cc24b2a5f15768d53451c84ff91e2527139e3" +dependencies = [ + "bitflags 2.8.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc49091f84e4f2ace078bbc86082b57e667b9e789baece4b1184e0963382b6e" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.96", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3545a699dc9d72298b2064ce71b771fc10fc6b757d29306b1e54a4283a75abba" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.96", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.224.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad555ab4f4e676474df746d937823c7279c2d6dd36c3e97a61db893d4ef64ee5" +dependencies = [ + "anyhow", + "bitflags 2.8.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.224.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23e2925a7365d2c6709ae17bdbb5777ffd8154fd70906b413fc01b75f0dba59e" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + [[package]] name = "write16" version = "1.0.0" @@ -5855,9 +5943,9 @@ dependencies = [ [[package]] name = "yaxpeax-arm" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1c6a2af41f88546a08df3bc77aadf7263884d6dffdac5b32dea7dc2df23f241" +checksum = "1db82aac85bc577d19b6255bf54ad97241c436eeb997ba159f399adacc5fb69e" dependencies = [ "bitvec", "yaxpeax-arch", diff --git a/objdiff-cli/Cargo.toml b/objdiff-cli/Cargo.toml index 498ac88..268d1e9 100644 --- a/objdiff-cli/Cargo.toml +++ b/objdiff-cli/Cargo.toml @@ -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" diff --git a/objdiff-cli/src/cmd/diff.rs b/objdiff-cli/src/cmd/diff.rs index 129d4d2..fd80a66 100644 --- a/objdiff-cli/src/cmd/diff.rs +++ b/objdiff-cli/src/cmd/diff.rs @@ -1,8 +1,6 @@ use std::{ - fs, io::stdout, mem, - path::{Path, PathBuf}, str::FromStr, sync::{ atomic::{AtomicBool, Ordering}, @@ -27,7 +25,11 @@ use objdiff_core::{ watcher::{create_watcher, Watcher}, BuildConfig, }, - config::{build_globset, ProjectConfig, ProjectObject}, + config::{ + build_globset, + path::{check_path_buf, platform_path, platform_path_serde_option}, + ProjectConfig, ProjectObject, ProjectObjectMetadata, + }, diff, diff::{ ConfigEnum, ConfigPropertyId, ConfigPropertyKind, DiffObjConfig, MappingConfig, ObjDiff, @@ -40,6 +42,7 @@ use objdiff_core::{ obj::ObjInfo, }; use ratatui::prelude::*; +use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf}; use crate::{ util::{ @@ -53,21 +56,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, - #[argp(option, short = '2')] + target: Option, + #[argp(option, short = '2', from_str_fn(platform_path))] /// Base object file - base: Option, - #[argp(option, short = 'p')] + base: Option, + #[argp(option, short = 'p', from_str_fn(platform_path))] /// Project directory - project: Option, + project: Option, #[argp(option, short = 'u')] /// Unit name within project unit: Option, - #[argp(option, short = 'o')] + #[argp(option, short = 'o', from_str_fn(platform_path))] /// Output file (one-shot mode) ("-" for stdout) - output: Option, + output: Option, #[argp(option)] /// Output format (json, json-pretty, proto) (default: json) format: Option, @@ -89,86 +92,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::>(); + 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 +159,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 +168,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,20 +222,20 @@ 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 = @@ -272,10 +249,10 @@ fn run_oneshot( pub struct AppState { pub jobs: JobQueue, pub waker: Arc, - pub project_dir: Option, + pub project_dir: Option, pub project_config: Option, - pub target_path: Option, - pub base_path: Option, + pub target_path: Option, + pub base_path: Option, pub left_obj: Option<(ObjInfo, ObjDiff)>, pub right_obj: Option<(ObjInfo, ObjDiff)>, pub prev_obj: Option<(ObjInfo, ObjDiff)>, @@ -315,6 +292,53 @@ 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, + #[serde(default, with = "platform_path_serde_option")] + pub base_path: Option, + pub metadata: ProjectObjectMetadata, + pub complete: Option, +} + +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 if let Some(path) = &object.target_path { + Some(project_dir.join(path.with_platform_encoding())) + } else { + None + }; + 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 if let Some(path) = &object.base_path { + Some(project_dir.join(path.with_platform_encoding())) + } else { + None + }; + 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 +379,8 @@ impl Wake for TermWaker { fn run_interactive( args: Args, - target_path: Option, - base_path: Option, + target_path: Option, + base_path: Option, project_config: Option, ) -> Result<()> { let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") }; @@ -384,7 +408,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()), )?); diff --git a/objdiff-cli/src/cmd/report.rs b/objdiff-cli/src/cmd/report.rs index ab74c11..17eb51b 100644 --- a/objdiff-cli/src/cmd/report.rs +++ b/objdiff-cli/src/cmd/report.rs @@ -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}, }; 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, - #[argp(option, short = 'o')] + project: Option, + #[argp(option, short = 'o', from_str_fn(platform_path))] /// Output file - output: Option, + output: Option, #[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, + output: Option, #[argp(option, short = 'f')] /// Output format (json, json-pretty, proto) (default: json) format: Option, @@ -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::>(); + let start = Instant::now(); let mut units = vec![]; let mut existing_functions: HashSet = 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::>>>()?; 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>, ) -> Result> { - 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)?; 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![]; @@ -218,7 +204,7 @@ fn report_object( 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 @@ -260,7 +246,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 +280,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 +290,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 +405,14 @@ fn process_new_items(items: &[ReportItem]) -> Vec { .collect() } -fn read_report(path: &Path) -> Result { - if path == Path::new("-") { +fn read_report(path: &Utf8PlatformPath) -> Result { + 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)) } diff --git a/objdiff-cli/src/util/output.rs b/objdiff-cli/src/util/output.rs index 8651aee..c08eee9 100644 --- a/objdiff-cli/src/util/output.rs +++ b/objdiff-cli/src/util/output.rs @@ -34,9 +34,12 @@ impl OutputFormat { } } -pub fn write_output(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()> -where T: serde::Serialize + prost::Message { - match output { +pub fn write_output(input: &T, output: Option

, format: OutputFormat) -> Result<()> +where + T: serde::Serialize + prost::Message, + P: AsRef, +{ + match output.as_ref().map(|p| p.as_ref()) { Some(output) if output != Path::new("-") => { info!("Writing to {}", output.display()); let file = File::options() diff --git a/objdiff-core/Cargo.toml b/objdiff-core/Cargo.toml index d9b42e8..64a9ff0 100644 --- a/objdiff-core/Cargo.toml +++ b/objdiff-core/Cargo.toml @@ -16,12 +16,14 @@ documentation = "https://docs.rs/objdiff-core" crate-type = ["cdylib", "rlib"] [features] +default = ["std"] all = [ # Features "bindings", "build", "config", "dwarf", + "serde", # Architectures "mips", "ppc", @@ -31,30 +33,21 @@ all = [ ] # Implicit, used to check if any arch is enabled any-arch = [ - "config", - "dep:bimap", "dep:byteorder", "dep:flagset", "dep:heck", "dep:log", - "dep:memmap2", "dep:num-traits", "dep:prettyplease", "dep:proc-macro2", "dep:quote", - "dep:serde", - "dep:serde_json", "dep:similar", "dep:strum", "dep:syn", ] bindings = [ - "dep:pbjson", - "dep:pbjson-build", "dep:prost", "dep:prost-build", - "dep:serde", - "dep:serde_json", ] build = [ "dep:notify", @@ -68,15 +61,31 @@ 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", + "byteorder?/std", + "flagset?/std", + "log?/std", + "num-traits?/std", + "object/std", + "prost?/std", + "serde?/std", + "strum?/std", + "typed-path?/std", + "dep:filetime", + "dep:memmap2", +] mips = [ "any-arch", "dep:rabbitizer", @@ -108,65 +117,66 @@ arm64 = [ wasm = [ "any-arch", "bindings", - "dep:console_error_panic_hook", - "dep:console_log", "dep:log", - "dep:tsify-next", - "dep:wasm-bindgen", + "dep:talc", + "dep:spin", + "dep:wit-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 } +byteorder = { version = "1.5", default-features = false, optional = true } 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 } +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 } +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" } +strum = { version = "0.26", default-features = false, features = ["derive"], optional = true } +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/encounter/cwextab.git" } ppc750cl = { version = "0.3", optional = true } # mips rabbitizer = { version = "1.12", 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 } + +# wasm +#console_error_panic_hook = { version = "0.1", optional = true } +#console_log = { version = "1.0", optional = true } +talc = { version = "4.4", optional = true } +spin = { version = "0.9", optional = true } +wit-bindgen = { version = "0.38", default-features = false, features = ["macros"], optional = true } # build notify = { version = "8.0.0", optional = true } @@ -196,6 +206,6 @@ 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 } diff --git a/objdiff-core/build.rs b/objdiff-core/build.rs index 521f203..0b307b4 100644 --- a/objdiff-core/build.rs +++ b/objdiff-core/build.rs @@ -54,11 +54,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"); + } } diff --git a/objdiff-core/config_gen.rs b/objdiff-core/config_gen.rs index 9467594..c904a78 100644 --- a/objdiff-core/config_gen.rs +++ b/objdiff-core/config_gen.rs @@ -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 { #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 { #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 } diff --git a/objdiff-core/src/arch/arm.rs b/objdiff-core/src/arch/arm.rs index 84a21cc..c7f5698 100644 --- a/objdiff-core/src/arch/arm.rs +++ b/objdiff-core/src/arch/arm.rs @@ -1,14 +1,18 @@ -use std::{ +use alloc::{ borrow::Cow, - collections::{BTreeMap, HashMap}, + collections::BTreeMap, + format, + string::{String, ToString}, + vec, + vec::Vec, }; use anyhow::{bail, Result}; use arm_attr::{enums::CpuArch, tag::Tag, BuildAttrs}; use object::{ elf::{self, SHT_ARM_ATTRIBUTES}, - Endian, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, SectionIndex, - SectionKind, Symbol, SymbolKind, + Endian, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, SectionKind, + Symbol, SymbolKind, }; use unarm::{ args::{Argument, OffsetImm, OffsetReg, Register}, @@ -24,7 +28,7 @@ use crate::{ pub struct ObjArchArm { /// Maps section index, to list of disasm modes (arm, thumb or data) sorted by address - disasm_modes: HashMap>, + disasm_modes: BTreeMap>, detected_version: Option, endianness: object::Endianness, } @@ -78,7 +82,7 @@ impl ObjArchArm { Ok(None) } - fn elf_get_mapping_symbols(file: &File) -> HashMap> { + fn elf_get_mapping_symbols(file: &File) -> BTreeMap> { file.sections() .filter(|s| s.kind() == SectionKind::Text) .map(|s| { @@ -89,7 +93,7 @@ impl ObjArchArm { .filter_map(|s| DisasmMode::from_symbol(&s)) .collect(); mapping_symbols.sort_unstable_by_key(|x| x.address); - (s.index(), mapping_symbols) + (s.index().0, mapping_symbols) }) .collect() } @@ -121,7 +125,7 @@ impl ObjArch for ObjArchArm { let fallback_mappings = [DisasmMode { address: start_addr, mapping: ParseMode::Arm }]; let mapping_symbols = self .disasm_modes - .get(&SectionIndex(section_index)) + .get(§ion_index) .map(|x| x.as_slice()) .unwrap_or(&fallback_mappings); let first_mapping_idx = mapping_symbols diff --git a/objdiff-core/src/arch/arm64.rs b/objdiff-core/src/arch/arm64.rs index ec225f3..971cc45 100644 --- a/objdiff-core/src/arch/arm64.rs +++ b/objdiff-core/src/arch/arm64.rs @@ -1,4 +1,12 @@ -use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap}; +use alloc::{ + borrow::Cow, + collections::BTreeMap, + format, + string::{String, ToString}, + vec, + vec::Vec, +}; +use core::cmp::Ordering; use anyhow::{bail, Result}; use object::{elf, File, Relocation, RelocationFlags}; diff --git a/objdiff-core/src/arch/mips.rs b/objdiff-core/src/arch/mips.rs index 2b89b23..c2fb1b0 100644 --- a/objdiff-core/src/arch/mips.rs +++ b/objdiff-core/src/arch/mips.rs @@ -1,4 +1,5 @@ -use std::{borrow::Cow, collections::BTreeMap, sync::Mutex}; +use alloc::{borrow::Cow, collections::BTreeMap, format, vec::Vec}; +use std::sync::Mutex; use anyhow::{anyhow, bail, Result}; use object::{ diff --git a/objdiff-core/src/arch/mod.rs b/objdiff-core/src/arch/mod.rs index 515ee3f..ba06dd4 100644 --- a/objdiff-core/src/arch/mod.rs +++ b/objdiff-core/src/arch/mod.rs @@ -1,4 +1,5 @@ -use std::{borrow::Cow, collections::BTreeMap, ffi::CStr}; +use alloc::{borrow::Cow, boxed::Box, collections::BTreeMap, format, string::String, vec::Vec}; +use core::ffi::CStr; use anyhow::{bail, Result}; use byteorder::ByteOrder; diff --git a/objdiff-core/src/arch/ppc.rs b/objdiff-core/src/arch/ppc.rs index caa09c0..c1d37e0 100644 --- a/objdiff-core/src/arch/ppc.rs +++ b/objdiff-core/src/arch/ppc.rs @@ -1,6 +1,10 @@ -use std::{ +use alloc::{ borrow::Cow, - collections::{BTreeMap, HashMap, HashSet}, + collections::{BTreeMap, BTreeSet}, + format, + string::{String, ToString}, + vec, + vec::Vec, }; use anyhow::{bail, ensure, Result}; @@ -479,7 +483,7 @@ fn get_offset_and_addr_gpr_for_possible_pool_reference( // Remove the relocation we're keeping track of in a particular register when an instruction reuses // that register to hold some other value, unrelated to pool relocation addresses. -fn clear_overwritten_gprs(ins: Ins, gpr_pool_relocs: &mut HashMap) { +fn clear_overwritten_gprs(ins: Ins, gpr_pool_relocs: &mut BTreeMap) { let mut def_args = Arguments::default(); ins.parse_defs(&mut def_args); for arg in def_args { @@ -576,11 +580,11 @@ fn generate_fake_pool_reloc_for_addr_mapping( func_address: u64, code: &[u8], relocations: &[ObjReloc], -) -> HashMap { - let mut visited_ins_addrs = HashSet::new(); - let mut pool_reloc_for_addr = HashMap::new(); +) -> BTreeMap { + let mut visited_ins_addrs = BTreeSet::new(); + let mut pool_reloc_for_addr = BTreeMap::new(); let mut ins_iters_with_gpr_state = - vec![(InsIter::new(code, func_address as u32), HashMap::new())]; + vec![(InsIter::new(code, func_address as u32), BTreeMap::new())]; let mut gpr_state_at_bctr = BTreeMap::new(); while let Some((ins_iter, mut gpr_pool_relocs)) = ins_iters_with_gpr_state.pop() { for (cur_addr, ins) in ins_iter { diff --git a/objdiff-core/src/arch/x86.rs b/objdiff-core/src/arch/x86.rs index b292993..b94e73a 100644 --- a/objdiff-core/src/arch/x86.rs +++ b/objdiff-core/src/arch/x86.rs @@ -1,4 +1,12 @@ -use std::{borrow::Cow, collections::BTreeMap}; +use alloc::{ + borrow::Cow, + boxed::Box, + collections::BTreeMap, + format, + string::{String, ToString}, + vec, + vec::Vec, +}; use anyhow::{anyhow, bail, ensure, Result}; use iced_x86::{ diff --git a/objdiff-core/src/bindings/diff.rs b/objdiff-core/src/bindings/diff.rs index e856b92..f702dbe 100644 --- a/objdiff-core/src/bindings/diff.rs +++ b/objdiff-core/src/bindings/diff.rs @@ -1,4 +1,7 @@ #![allow(clippy::needless_lifetimes)] // Generated serde code + +use alloc::string::ToString; + use crate::{ diff::{ ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, @@ -13,6 +16,7 @@ use crate::{ // Protobuf diff types include!(concat!(env!("OUT_DIR"), "/objdiff.diff.rs")); +#[cfg(feature = "serde")] include!(concat!(env!("OUT_DIR"), "/objdiff.diff.serde.rs")); impl DiffResult { diff --git a/objdiff-core/src/bindings/mod.rs b/objdiff-core/src/bindings/mod.rs index 44e979e..4b67b72 100644 --- a/objdiff-core/src/bindings/mod.rs +++ b/objdiff-core/src/bindings/mod.rs @@ -1,5 +1,3 @@ #[cfg(feature = "any-arch")] pub mod diff; pub mod report; -#[cfg(feature = "wasm")] -pub mod wasm; diff --git a/objdiff-core/src/bindings/report.rs b/objdiff-core/src/bindings/report.rs index 275487b..938bc45 100644 --- a/objdiff-core/src/bindings/report.rs +++ b/objdiff-core/src/bindings/report.rs @@ -1,12 +1,18 @@ #![allow(clippy::needless_lifetimes)] // Generated serde code -use std::ops::AddAssign; + +use alloc::{ + string::{String, ToString}, + vec, + vec::Vec, +}; +use core::ops::AddAssign; use anyhow::{bail, Result}; use prost::Message; -use serde_json::error::Category; // Protobuf report types include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs")); +#[cfg(feature = "serde")] include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs")); pub const REPORT_VERSION: u32 = 2; @@ -15,23 +21,30 @@ impl Report { /// Attempts to parse the report as binary protobuf or JSON. pub fn parse(data: &[u8]) -> Result { if data.is_empty() { - bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof)); + bail!("Empty data"); } let report = if data[0] == b'{' { // Load as JSON - Self::from_json(data)? + #[cfg(feature = "serde")] + { + Self::from_json(data)? + } + #[cfg(not(feature = "serde"))] + bail!("JSON report parsing requires the `serde` feature") } else { // Load as binary protobuf - Self::decode(data)? + Self::decode(data).map_err(|e| anyhow::Error::msg(e.to_string()))? }; Ok(report) } + #[cfg(feature = "serde")] /// Attempts to parse the report as JSON, migrating from the legacy report format if necessary. fn from_json(bytes: &[u8]) -> Result { match serde_json::from_slice::(bytes) { Ok(report) => Ok(report), Err(e) => { + use serde_json::error::Category; match e.classify() { Category::Io | Category::Eof | Category::Syntax => Err(e), Category::Data => { @@ -304,7 +317,8 @@ impl FromIterator for Measures { } // Older JSON report types -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] struct LegacyReport { fuzzy_match_percent: f32, total_code: u64, @@ -341,7 +355,8 @@ impl From for Report { } } -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] struct LegacyReportUnit { name: String, fuzzy_match_percent: f32, @@ -351,11 +366,11 @@ struct LegacyReportUnit { matched_data: u64, total_functions: u32, matched_functions: u32, - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] complete: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] module_name: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] module_id: Option, sections: Vec, functions: Vec, @@ -389,16 +404,20 @@ impl From for ReportUnit { } } -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] struct LegacyReportItem { name: String, - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] demangled_name: Option, - #[serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "serialize_hex", - deserialize_with = "deserialize_hex" + #[cfg_attr( + feature = "serde", + serde( + default, + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_hex", + deserialize_with = "deserialize_hex" + ) )] address: Option, size: u64, @@ -419,6 +438,7 @@ impl From for ReportItem { } } +#[cfg(feature = "serde")] fn serialize_hex(x: &Option, s: S) -> Result where S: serde::Serializer { if let Some(x) = x { @@ -428,6 +448,7 @@ where S: serde::Serializer { } } +#[cfg(feature = "serde")] fn deserialize_hex<'de, D>(d: D) -> Result, D::Error> where D: serde::Deserializer<'de> { use serde::Deserialize; diff --git a/objdiff-core/src/bindings/wasm.rs b/objdiff-core/src/bindings/wasm.rs deleted file mode 100644 index 37cb884..0000000 --- a/objdiff-core/src/bindings/wasm.rs +++ /dev/null @@ -1,81 +0,0 @@ -use prost::Message; -use wasm_bindgen::prelude::*; - -use crate::{bindings::diff::DiffResult, diff, obj}; - -fn parse_object( - data: Option>, - config: &diff::DiffObjConfig, -) -> Result, JsError> { - data.as_ref().map(|data| obj::read::parse(data, config)).transpose().to_js() -} - -fn parse_and_run_diff( - left: Option>, - right: Option>, - diff_config: diff::DiffObjConfig, - mapping_config: diff::MappingConfig, -) -> Result { - let target = parse_object(left, &diff_config)?; - let base = parse_object(right, &diff_config)?; - run_diff(target.as_ref(), base.as_ref(), diff_config, mapping_config) -} - -fn run_diff( - left: Option<&obj::ObjInfo>, - right: Option<&obj::ObjInfo>, - diff_config: diff::DiffObjConfig, - mapping_config: diff::MappingConfig, -) -> Result { - log::debug!("Running diff with config: {:?}", diff_config); - let result = diff::diff_objs(&diff_config, &mapping_config, left, right, None).to_js()?; - let left = left.and_then(|o| result.left.as_ref().map(|d| (o, d))); - let right = right.and_then(|o| result.right.as_ref().map(|d| (o, d))); - Ok(DiffResult::new(left, right)) -} - -// #[wasm_bindgen] -// pub fn run_diff_json( -// left: Option>, -// right: Option>, -// config: diff::DiffObjConfig, -// ) -> Result { -// let out = run_diff_opt_box(left, right, config)?; -// serde_json::to_string(&out).map_err(|e| JsError::new(&e.to_string())) -// } - -#[wasm_bindgen] -pub fn run_diff_proto( - left: Option>, - right: Option>, - diff_config: diff::DiffObjConfig, - mapping_config: diff::MappingConfig, -) -> Result, JsError> { - let out = parse_and_run_diff(left, right, diff_config, mapping_config)?; - Ok(out.encode_to_vec().into_boxed_slice()) -} - -#[wasm_bindgen(start)] -fn start() -> Result<(), JsError> { - console_error_panic_hook::set_once(); - #[cfg(debug_assertions)] - console_log::init_with_level(log::Level::Debug).to_js()?; - #[cfg(not(debug_assertions))] - console_log::init_with_level(log::Level::Info).to_js()?; - Ok(()) -} - -#[inline] -fn to_js_error(e: impl std::fmt::Display) -> JsError { JsError::new(&e.to_string()) } - -trait ToJsResult { - type Output; - - fn to_js(self) -> Result; -} - -impl ToJsResult for Result { - type Output = T; - - fn to_js(self) -> Result { self.map_err(to_js_error) } -} diff --git a/objdiff-core/src/build/mod.rs b/objdiff-core/src/build/mod.rs index d8bae78..3532c8a 100644 --- a/objdiff-core/src/build/mod.rs +++ b/objdiff-core/src/build/mod.rs @@ -1,9 +1,8 @@ pub mod watcher; -use std::{ - path::{Path, PathBuf}, - process::Command, -}; +use std::process::Command; + +use typed_path::Utf8PlatformPathBuf; pub struct BuildStatus { pub success: bool, @@ -25,14 +24,14 @@ impl Default for BuildStatus { #[derive(Debug, Clone)] pub struct BuildConfig { - pub project_dir: Option, + pub project_dir: Option, pub custom_make: Option, pub custom_args: Option>, #[allow(unused)] pub selected_wsl_distro: Option, } -pub fn run_make(config: &BuildConfig, arg: &Path) -> BuildStatus { +pub fn run_make(config: &BuildConfig, arg: &str) -> BuildStatus { let Some(cwd) = &config.project_dir else { return BuildStatus { success: false, diff --git a/objdiff-core/src/config/mod.rs b/objdiff-core/src/config/mod.rs index 3b39ab0..272f35b 100644 --- a/objdiff-core/src/config/mod.rs +++ b/objdiff-core/src/config/mod.rs @@ -1,37 +1,47 @@ -use std::{ +pub mod path; + +use alloc::{ collections::BTreeMap, - fs, - fs::File, - io::{BufReader, BufWriter, Read}, - path::{Path, PathBuf}, + string::{String, ToString}, + vec::Vec, }; use anyhow::{anyhow, Context, Result}; -use filetime::FileTime; use globset::{Glob, GlobSet, GlobSetBuilder}; +use path::unix_path_serde_option; +use typed_path::Utf8UnixPathBuf; -#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] +#[derive(Default, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))] pub struct ProjectConfig { - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub min_version: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub custom_make: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub custom_args: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub target_dir: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub base_dir: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "serde", + serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none") + )] + pub target_dir: Option, + #[cfg_attr( + feature = "serde", + serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none") + )] + pub base_dir: Option, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub build_base: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub build_target: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub watch_patterns: Option>, - #[serde(default, alias = "objects", skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "serde", + serde(alias = "objects", skip_serializing_if = "Option::is_none") + )] pub units: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub progress_categories: Option>, } @@ -39,11 +49,6 @@ impl ProjectConfig { #[inline] pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() } - #[inline] - pub fn units_mut(&mut self) -> &mut Vec { - self.units.get_or_insert_with(Vec::new) - } - #[inline] pub fn progress_categories(&self) -> &[ProjectProgressCategory] { self.progress_categories.as_deref().unwrap_or_default() @@ -66,55 +71,62 @@ impl ProjectConfig { } } -#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] +#[derive(Default, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))] pub struct ProjectObject { - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub name: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub path: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub target_path: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub base_path: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "serde", + serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none") + )] + pub path: Option, + #[cfg_attr( + feature = "serde", + serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none") + )] + pub target_path: Option, + #[cfg_attr( + feature = "serde", + serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none") + )] + pub base_path: Option, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[deprecated(note = "Use metadata.reverse_fn_order")] pub reverse_fn_order: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] #[deprecated(note = "Use metadata.complete")] pub complete: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub scratch: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub metadata: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub symbol_mappings: Option, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub symbol_mappings: Option>, } -#[cfg_attr(feature = "wasm", tsify_next::declare)] -pub type SymbolMappings = BTreeMap; - -#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] +#[derive(Default, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))] pub struct ProjectObjectMetadata { - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub complete: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub reverse_fn_order: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_path: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "serde", + serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none") + )] + pub source_path: Option, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub progress_categories: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub auto_generated: Option, } -#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] +#[derive(Default, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))] pub struct ProjectProgressCategory { - #[serde(default)] pub id: String, - #[serde(default)] pub name: String, } @@ -123,33 +135,12 @@ impl ProjectObject { if let Some(name) = &self.name { name } else if let Some(path) = &self.path { - path.to_str().unwrap_or("[invalid path]") + path.as_str() } else { "[unknown]" } } - pub fn resolve_paths( - &mut self, - project_dir: &Path, - target_obj_dir: Option<&Path>, - base_obj_dir: Option<&Path>, - ) { - if let (Some(target_obj_dir), Some(path), None) = - (target_obj_dir, &self.path, &self.target_path) - { - self.target_path = Some(target_obj_dir.join(path)); - } else if let Some(path) = &self.target_path { - self.target_path = Some(project_dir.join(path)); - } - if let (Some(base_obj_dir), Some(path), None) = (base_obj_dir, &self.path, &self.base_path) - { - self.base_path = Some(base_obj_dir.join(path)); - } else if let Some(path) = &self.base_path { - self.base_path = Some(project_dir.join(path)); - } - } - pub fn complete(&self) -> Option { #[expect(deprecated)] self.metadata.as_ref().and_then(|m| m.complete).or(self.complete) @@ -164,25 +155,36 @@ impl ProjectObject { self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false) } - pub fn source_path(&self) -> Option<&String> { + pub fn source_path(&self) -> Option<&Utf8UnixPathBuf> { self.metadata.as_ref().and_then(|m| m.source_path.as_ref()) } + + pub fn progress_categories(&self) -> &[String] { + self.metadata.as_ref().and_then(|m| m.progress_categories.as_deref()).unwrap_or_default() + } + + pub fn auto_generated(&self) -> Option { + self.metadata.as_ref().and_then(|m| m.auto_generated) + } } -#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] +#[derive(Default, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize), serde(default))] pub struct ScratchConfig { - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub platform: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub compiler: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub c_flags: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub ctx_path: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "serde", + serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none") + )] + pub ctx_path: Option, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub build_ctx: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub preset_id: Option, } @@ -197,16 +199,20 @@ pub fn default_watch_patterns() -> Vec { DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() } +#[cfg(feature = "std")] #[derive(Clone, Eq, PartialEq)] pub struct ProjectConfigInfo { - pub path: PathBuf, - pub timestamp: Option, + pub path: std::path::PathBuf, + pub timestamp: Option, } -pub fn try_project_config(dir: &Path) -> Option<(Result, ProjectConfigInfo)> { +#[cfg(feature = "std")] +pub fn try_project_config( + dir: &std::path::Path, +) -> Option<(Result, ProjectConfigInfo)> { for filename in CONFIG_FILENAMES.iter() { let config_path = dir.join(filename); - let Ok(file) = File::open(&config_path) else { + let Ok(file) = std::fs::File::open(&config_path) else { continue; }; let metadata = file.metadata(); @@ -214,12 +220,9 @@ pub fn try_project_config(dir: &Path) -> Option<(Result, ProjectC if !metadata.is_file() { continue; } - let ts = FileTime::from_last_modification_time(&metadata); - let mut reader = BufReader::new(file); - let mut result = match filename.contains("json") { - true => read_json_config(&mut reader), - false => read_yml_config(&mut reader), - }; + let ts = filetime::FileTime::from_last_modification_time(&metadata); + let mut reader = std::io::BufReader::new(file); + let mut result = read_json_config(&mut reader); if let Ok(config) = &result { // Validate min_version if present if let Err(e) = validate_min_version(config) { @@ -232,40 +235,42 @@ pub fn try_project_config(dir: &Path) -> Option<(Result, ProjectC None } +#[cfg(feature = "std")] pub fn save_project_config( config: &ProjectConfig, info: &ProjectConfigInfo, ) -> Result { if let Some(last_ts) = info.timestamp { // Check if the file has changed since we last read it - if let Ok(metadata) = fs::metadata(&info.path) { - let ts = FileTime::from_last_modification_time(&metadata); + if let Ok(metadata) = std::fs::metadata(&info.path) { + let ts = filetime::FileTime::from_last_modification_time(&metadata); if ts != last_ts { return Err(anyhow!("Config file has changed since last read")); } } } - let mut writer = - BufWriter::new(File::create(&info.path).context("Failed to create config file")?); + let mut writer = std::io::BufWriter::new( + std::fs::File::create(&info.path).context("Failed to create config file")?, + ); let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json"); match ext { "json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"), - "yml" | "yaml" => { - serde_yaml::to_writer(&mut writer, config).context("Failed to write YAML") - } _ => Err(anyhow!("Unknown config file extension: {ext}")), }?; let file = writer.into_inner().context("Failed to flush file")?; let metadata = file.metadata().context("Failed to get file metadata")?; - let ts = FileTime::from_last_modification_time(&metadata); + let ts = filetime::FileTime::from_last_modification_time(&metadata); Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) }) } fn validate_min_version(config: &ProjectConfig) -> Result<()> { let Some(min_version) = &config.min_version else { return Ok(()) }; let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) + .map_err(|e| anyhow::Error::msg(e.to_string())) .context("Failed to parse package version")?; - let min_version = semver::Version::parse(min_version).context("Failed to parse min_version")?; + let min_version = semver::Version::parse(min_version) + .map_err(|e| anyhow::Error::msg(e.to_string())) + .context("Failed to parse min_version")?; if version >= min_version { Ok(()) } else { @@ -273,15 +278,12 @@ fn validate_min_version(config: &ProjectConfig) -> Result<()> { } } -fn read_yml_config(reader: &mut R) -> Result { - Ok(serde_yaml::from_reader(reader)?) -} - -fn read_json_config(reader: &mut R) -> Result { +#[cfg(feature = "std")] +fn read_json_config(reader: &mut R) -> Result { Ok(serde_json::from_reader(reader)?) } -pub fn build_globset(vec: &[Glob]) -> std::result::Result { +pub fn build_globset(vec: &[Glob]) -> Result { let mut builder = GlobSetBuilder::new(); for glob in vec { builder.add(glob.clone()); diff --git a/objdiff-core/src/config/path.rs b/objdiff-core/src/config/path.rs new file mode 100644 index 0000000..d94d977 --- /dev/null +++ b/objdiff-core/src/config/path.rs @@ -0,0 +1,65 @@ +// For argp::FromArgs +#[cfg(feature = "std")] +pub fn platform_path(value: &str) -> Result { + Ok(typed_path::Utf8PlatformPathBuf::from(value)) +} + +/// Checks if the path is valid UTF-8 and returns it as a [`Utf8PlatformPath`]. +#[cfg(feature = "std")] +pub fn check_path( + path: &std::path::Path, +) -> Result<&typed_path::Utf8PlatformPath, core::str::Utf8Error> { + typed_path::Utf8PlatformPath::from_bytes_path(typed_path::PlatformPath::new( + path.as_os_str().as_encoded_bytes(), + )) +} + +/// Checks if the path is valid UTF-8 and returns it as a [`Utf8NativePathBuf`]. +#[cfg(feature = "std")] +pub fn check_path_buf( + path: std::path::PathBuf, +) -> Result { + typed_path::Utf8PlatformPathBuf::from_bytes_path_buf(typed_path::PlatformPathBuf::from( + path.into_os_string().into_encoded_bytes(), + )) +} + +#[cfg(feature = "serde")] +pub mod unix_path_serde_option { + use serde::{Deserialize, Deserializer, Serializer}; + use typed_path::Utf8UnixPathBuf; + + pub fn serialize(path: &Option, s: S) -> Result + where S: Serializer { + if let Some(path) = path { + s.serialize_some(path.as_str()) + } else { + s.serialize_none() + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where D: Deserializer<'de> { + Ok(Option::::deserialize(deserializer)?.map(Utf8UnixPathBuf::from)) + } +} + +#[cfg(all(feature = "serde", feature = "std"))] +pub mod platform_path_serde_option { + use serde::{Deserialize, Deserializer, Serializer}; + use typed_path::Utf8PlatformPathBuf; + + pub fn serialize(path: &Option, s: S) -> Result + where S: Serializer { + if let Some(path) = path { + s.serialize_some(path.as_str()) + } else { + s.serialize_none() + } + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where D: Deserializer<'de> { + Ok(Option::::deserialize(deserializer)?.map(Utf8PlatformPathBuf::from)) + } +} diff --git a/objdiff-core/src/diff/code.rs b/objdiff-core/src/diff/code.rs index 9dc0fbc..b52d0d5 100644 --- a/objdiff-core/src/diff/code.rs +++ b/objdiff-core/src/diff/code.rs @@ -1,7 +1,12 @@ -use std::{cmp::max, collections::BTreeMap}; +use alloc::{ + collections::BTreeMap, + string::{String, ToString}, + vec, + vec::Vec, +}; use anyhow::{anyhow, Result}; -use similar::{capture_diff_slices_deadline, Algorithm}; +use similar::{capture_diff_slices, Algorithm}; use super::FunctionRelocDiffs; use crate::{ @@ -118,8 +123,7 @@ fn diff_instructions( left_code: &ProcessCodeResult, right_code: &ProcessCodeResult, ) -> Result<()> { - let ops = - capture_diff_slices_deadline(Algorithm::Patience, &left_code.ops, &right_code.ops, None); + let ops = capture_diff_slices(Algorithm::Patience, &left_code.ops, &right_code.ops); if ops.is_empty() { left_diff.extend( left_code @@ -138,7 +142,7 @@ fn diff_instructions( for op in ops { let (_tag, left_range, right_range) = op.as_tag_tuple(); - let len = max(left_range.len(), right_range.len()); + let len = left_range.len().max(right_range.len()); left_diff.extend( left_code.insts[left_range.clone()] .iter() diff --git a/objdiff-core/src/diff/data.rs b/objdiff-core/src/diff/data.rs index 954fc55..f53f727 100644 --- a/objdiff-core/src/diff/data.rs +++ b/objdiff-core/src/diff/data.rs @@ -1,10 +1,8 @@ -use std::{ - cmp::{max, min, Ordering}, - ops::Range, -}; +use alloc::{vec, vec::Vec}; +use core::{cmp::Ordering, ops::Range}; use anyhow::{anyhow, Result}; -use similar::{capture_diff_slices_deadline, get_diff_ratio, Algorithm}; +use similar::{capture_diff_slices, get_diff_ratio, Algorithm}; use super::code::{address_eq, section_name_eq}; use crate::{ @@ -142,7 +140,7 @@ pub fn diff_data_section( right.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0).min(right.size); let left_data = &left.data[..left_max as usize]; let right_data = &right.data[..right_max as usize]; - let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None); + let ops = capture_diff_slices(Algorithm::Patience, left_data, right_data); let match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0; let mut left_diff = Vec::::new(); @@ -151,27 +149,27 @@ pub fn diff_data_section( let (tag, left_range, right_range) = op.as_tag_tuple(); let left_len = left_range.len(); let right_len = right_range.len(); - let mut len = max(left_len, right_len); + let mut len = left_len.max(right_len); let kind = match tag { similar::DiffTag::Equal => ObjDataDiffKind::None, similar::DiffTag::Delete => ObjDataDiffKind::Delete, similar::DiffTag::Insert => ObjDataDiffKind::Insert, similar::DiffTag::Replace => { // Ensure replacements are equal length - len = min(left_len, right_len); + len = left_len.min(right_len); ObjDataDiffKind::Replace } }; let left_data = &left.data[left_range]; let right_data = &right.data[right_range]; left_diff.push(ObjDataDiff { - data: left_data[..min(len, left_data.len())].to_vec(), + data: left_data[..len.min(left_data.len())].to_vec(), kind, len, ..Default::default() }); right_diff.push(ObjDataDiff { - data: right_data[..min(len, right_data.len())].to_vec(), + data: right_data[..len.min(right_data.len())].to_vec(), kind, len, ..Default::default() @@ -283,7 +281,7 @@ pub fn diff_data_symbol( right_range, ); - let ops = capture_diff_slices_deadline(Algorithm::Patience, left_data, right_data, None); + let ops = capture_diff_slices(Algorithm::Patience, left_data, right_data); let bytes_match_ratio = get_diff_ratio(&ops, left_data.len(), right_data.len()); let mut match_ratio = bytes_match_ratio; @@ -375,7 +373,7 @@ pub fn diff_bss_section( ) -> Result<(ObjSectionDiff, ObjSectionDiff)> { let left_sizes = left.symbols.iter().map(|s| (s.section_address, s.size)).collect::>(); let right_sizes = right.symbols.iter().map(|s| (s.section_address, s.size)).collect::>(); - let ops = capture_diff_slices_deadline(Algorithm::Patience, &left_sizes, &right_sizes, None); + let ops = capture_diff_slices(Algorithm::Patience, &left_sizes, &right_sizes); let mut match_percent = get_diff_ratio(&ops, left_sizes.len(), right_sizes.len()) * 100.0; // Use the highest match percent between two options: diff --git a/objdiff-core/src/diff/display.rs b/objdiff-core/src/diff/display.rs index 29fccdb..559ef69 100644 --- a/objdiff-core/src/diff/display.rs +++ b/objdiff-core/src/diff/display.rs @@ -1,3 +1,5 @@ +use alloc::string::{String, ToString}; + use crate::{ diff::{ObjInsArgDiff, ObjInsDiff}, obj::{ObjInsArg, ObjInsArgValue, ObjReloc, ObjSymbol}, diff --git a/objdiff-core/src/diff/mod.rs b/objdiff-core/src/diff/mod.rs index 49611e5..1f86547 100644 --- a/objdiff-core/src/diff/mod.rs +++ b/objdiff-core/src/diff/mod.rs @@ -1,9 +1,14 @@ -use std::{collections::HashSet, ops::Range}; +use alloc::{ + collections::{BTreeMap, BTreeSet}, + string::String, + vec, + vec::Vec, +}; +use core::ops::Range; use anyhow::Result; use crate::{ - config::SymbolMappings, diff::{ code::{diff_code, no_diff_code, process_code_symbol}, data::{ @@ -473,12 +478,11 @@ struct SectionMatch { section_kind: ObjSectionKind, } -#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)] -#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] -#[serde(default)] +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))] pub struct MappingConfig { /// Manual symbol mappings - pub mappings: SymbolMappings, + pub mappings: BTreeMap, /// The right object symbol name that we're selecting a left symbol for pub selecting_left: Option, /// The left object symbol name that we're selecting a right symbol for @@ -500,8 +504,8 @@ fn apply_symbol_mappings( left: &ObjInfo, right: &ObjInfo, mapping_config: &MappingConfig, - left_used: &mut HashSet, - right_used: &mut HashSet, + left_used: &mut BTreeSet, + right_used: &mut BTreeSet, matches: &mut Vec, ) -> Result<()> { // If we're selecting a symbol to use as a comparison, mark it as used @@ -563,8 +567,8 @@ fn matching_symbols( mappings: &MappingConfig, ) -> Result> { let mut matches = Vec::new(); - let mut left_used = HashSet::new(); - let mut right_used = HashSet::new(); + let mut left_used = BTreeSet::new(); + let mut right_used = BTreeSet::new(); if let Some(left) = left { if let Some(right) = right { apply_symbol_mappings( @@ -645,7 +649,7 @@ fn matching_symbols( fn unmatched_symbols<'section, 'used>( section: &'section ObjSection, section_idx: usize, - used: Option<&'used HashSet>, + used: Option<&'used BTreeSet>, ) -> impl Iterator + 'used where 'section: 'used, @@ -660,7 +664,7 @@ fn find_symbol( obj: Option<&ObjInfo>, in_symbol: &ObjSymbol, in_section: &ObjSection, - used: Option<&HashSet>, + used: Option<&BTreeSet>, ) -> Option { let obj = obj?; // Try to find an exact name match diff --git a/objdiff-core/src/jobs/create_scratch.rs b/objdiff-core/src/jobs/create_scratch.rs index 3d74f19..ab427ee 100644 --- a/objdiff-core/src/jobs/create_scratch.rs +++ b/objdiff-core/src/jobs/create_scratch.rs @@ -1,6 +1,7 @@ -use std::{fs, path::PathBuf, sync::mpsc::Receiver, task::Waker}; +use std::{fs, sync::mpsc::Receiver, task::Waker}; use anyhow::{anyhow, bail, Context, Result}; +use typed_path::{Utf8PlatformPathBuf, Utf8UnixPathBuf}; use crate::{ build::{run_make, BuildConfig, BuildStatus}, @@ -10,7 +11,7 @@ use crate::{ #[derive(Debug, Clone)] pub struct CreateScratchConfig { pub build_config: BuildConfig, - pub context_path: Option, + pub context_path: Option, pub build_context: bool, // Scratch fields @@ -18,7 +19,7 @@ pub struct CreateScratchConfig { pub platform: String, pub compiler_flags: String, pub function_name: String, - pub target_obj: PathBuf, + pub target_obj: Utf8PlatformPathBuf, pub preset_id: Option, } @@ -47,26 +48,25 @@ fn run_create_scratch( if let Some(context_path) = &config.context_path { if config.build_context { update_status(status, "Building context".to_string(), 0, 2, &cancel)?; - match run_make(&config.build_config, context_path) { + match run_make(&config.build_config, context_path.as_ref()) { BuildStatus { success: true, .. } => {} BuildStatus { success: false, stdout, stderr, .. } => { bail!("Failed to build context:\n{stdout}\n{stderr}") } } } - let context_path = project_dir.join(context_path); + let context_path = project_dir.join(context_path.with_platform_encoding()); context = Some( fs::read_to_string(&context_path) - .map_err(|e| anyhow!("Failed to read {}: {}", context_path.display(), e))?, + .map_err(|e| anyhow!("Failed to read {}: {}", context_path, e))?, ); } update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?; let diff_flags = [format!("--disassemble={}", config.function_name)]; let diff_flags = serde_json::to_string(&diff_flags)?; - let obj_path = project_dir.join(&config.target_obj); - let file = reqwest::blocking::multipart::Part::file(&obj_path) - .with_context(|| format!("Failed to open {}", obj_path.display()))?; + let file = reqwest::blocking::multipart::Part::file(&config.target_obj) + .with_context(|| format!("Failed to open {}", config.target_obj))?; let mut form = reqwest::blocking::multipart::Form::new() .text("compiler", config.compiler.clone()) .text("platform", config.platform.clone()) diff --git a/objdiff-core/src/jobs/objdiff.rs b/objdiff-core/src/jobs/objdiff.rs index 36ca981..c3f95c0 100644 --- a/objdiff-core/src/jobs/objdiff.rs +++ b/objdiff-core/src/jobs/objdiff.rs @@ -1,7 +1,8 @@ -use std::{path::PathBuf, sync::mpsc::Receiver, task::Waker}; +use std::{sync::mpsc::Receiver, task::Waker}; use anyhow::{anyhow, Error, Result}; use time::OffsetDateTime; +use typed_path::Utf8PlatformPathBuf; use crate::{ build::{run_make, BuildConfig, BuildStatus}, @@ -14,8 +15,8 @@ pub struct ObjDiffConfig { pub build_config: BuildConfig, pub build_base: bool, pub build_target: bool, - pub target_path: Option, - pub base_path: Option, + pub target_path: Option, + pub base_path: Option, pub diff_obj_config: DiffObjConfig, pub mapping_config: MappingConfig, } @@ -43,20 +44,12 @@ fn run_build( .ok_or_else(|| Error::msg("Missing project dir"))?; if let Some(target_path) = &config.target_path { target_path_rel = Some(target_path.strip_prefix(project_dir).map_err(|_| { - anyhow!( - "Target path '{}' doesn't begin with '{}'", - target_path.display(), - project_dir.display() - ) + anyhow!("Target path '{}' doesn't begin with '{}'", target_path, project_dir) })?); } if let Some(base_path) = &config.base_path { base_path_rel = Some(base_path.strip_prefix(project_dir).map_err(|_| { - anyhow!( - "Base path '{}' doesn't begin with '{}'", - base_path.display(), - project_dir.display() - ) + anyhow!("Base path '{}' doesn't begin with '{}'", base_path, project_dir) })?); }; } @@ -80,13 +73,13 @@ fn run_build( Some(target_path_rel) if config.build_target => { update_status( context, - format!("Building target {}", target_path_rel.display()), + format!("Building target {}", target_path_rel), step_idx, total, &cancel, )?; step_idx += 1; - run_make(&config.build_config, target_path_rel) + run_make(&config.build_config, target_path_rel.as_ref()) } _ => BuildStatus::default(), }; @@ -95,13 +88,13 @@ fn run_build( Some(base_path_rel) if config.build_base => { update_status( context, - format!("Building base {}", base_path_rel.display()), + format!("Building base {}", base_path_rel), step_idx, total, &cancel, )?; step_idx += 1; - run_make(&config.build_config, base_path_rel) + run_make(&config.build_config, base_path_rel.as_ref()) } _ => BuildStatus::default(), }; @@ -112,18 +105,18 @@ fn run_build( Some(target_path) if first_status.success => { update_status( context, - format!("Loading target {}", target_path.display()), + format!("Loading target {}", target_path), step_idx, total, &cancel, )?; step_idx += 1; - match read::read(target_path, &config.diff_obj_config) { + match read::read(target_path.as_ref(), &config.diff_obj_config) { Ok(obj) => Some(obj), Err(e) => { first_status = BuildStatus { success: false, - stdout: format!("Loading object '{}'", target_path.display()), + stdout: format!("Loading object '{}'", target_path), stderr: format!("{:#}", e), ..Default::default() }; @@ -142,18 +135,18 @@ fn run_build( Some(base_path) if second_status.success => { update_status( context, - format!("Loading base {}", base_path.display()), + format!("Loading base {}", base_path), step_idx, total, &cancel, )?; step_idx += 1; - match read::read(base_path, &config.diff_obj_config) { + match read::read(base_path.as_ref(), &config.diff_obj_config) { Ok(obj) => Some(obj), Err(e) => { second_status = BuildStatus { success: false, - stdout: format!("Loading object '{}'", base_path.display()), + stdout: format!("Loading object '{}'", base_path), stderr: format!("{:#}", e), ..Default::default() }; diff --git a/objdiff-core/src/lib.rs b/objdiff-core/src/lib.rs index 8663ba9..263f17c 100644 --- a/objdiff-core/src/lib.rs +++ b/objdiff-core/src/lib.rs @@ -1,3 +1,6 @@ +#![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; + #[cfg(feature = "any-arch")] pub mod arch; #[cfg(feature = "bindings")] @@ -14,3 +17,5 @@ pub mod jobs; pub mod obj; #[cfg(feature = "any-arch")] pub mod util; +#[cfg(feature = "wasm")] +pub mod wasm; diff --git a/objdiff-core/src/obj/mod.rs b/objdiff-core/src/obj/mod.rs index a060ad6..21b213e 100644 --- a/objdiff-core/src/obj/mod.rs +++ b/objdiff-core/src/obj/mod.rs @@ -1,9 +1,9 @@ pub mod read; pub mod split_meta; -use std::{borrow::Cow, collections::BTreeMap, fmt, path::PathBuf}; +use alloc::{borrow::Cow, boxed::Box, collections::BTreeMap, string::String, vec::Vec}; +use core::fmt; -use filetime::FileTime; use flagset::{flags, FlagSet}; use object::RelocationFlags; use split_meta::SplitMeta; @@ -152,8 +152,9 @@ pub struct ObjSymbol { pub struct ObjInfo { pub arch: Box, - pub path: Option, - pub timestamp: Option, + pub path: Option, + #[cfg(feature = "std")] + pub timestamp: Option, pub sections: Vec, /// Common BSS symbols pub common: Vec, diff --git a/objdiff-core/src/obj/read.rs b/objdiff-core/src/obj/read.rs index ee0347e..7550108 100644 --- a/objdiff-core/src/obj/read.rs +++ b/objdiff-core/src/obj/read.rs @@ -1,13 +1,12 @@ -use std::{ - collections::{HashMap, HashSet}, - fs, - io::Cursor, - mem::size_of, - path::Path, +use alloc::{ + collections::{BTreeMap, BTreeSet}, + format, + string::{String, ToString}, + vec, + vec::Vec, }; use anyhow::{anyhow, bail, ensure, Context, Result}; -use filetime::FileTime; use flagset::Flags; use object::{ endian::LittleEndian as LE, @@ -160,7 +159,7 @@ fn symbols_by_section( section: &ObjSection, section_symbols: &[Symbol<'_, '_>], split_meta: Option<&SplitMeta>, - name_counts: &mut HashMap, + name_counts: &mut BTreeMap, ) -> Result> { let mut result = Vec::::new(); for symbol in section_symbols { @@ -377,33 +376,37 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8]) // DWARF 1.1 if let Some(section) = obj_file.section_by_name(".line") { let data = section.uncompressed_data()?; - let mut reader = Cursor::new(data.as_ref()); + let mut reader: &[u8] = data.as_ref(); let mut text_sections = obj_file.sections().filter(|s| s.kind() == SectionKind::Text); - while reader.position() < data.len() as u64 { + while !reader.is_empty() { let text_section_index = text_sections .next() .ok_or_else(|| anyhow!("Next text section not found for line info"))? .index() .0; - let start = reader.position(); - let size = read_u32(obj_file, &mut reader)?; - let base_address = read_u32(obj_file, &mut reader)? as u64; + + let mut section_data = &reader[..]; + let size = read_u32(obj_file, &mut section_data)? as usize; + if size > reader.len() { + bail!("Line info size {size} exceeds remaining size {}", reader.len()); + } + (section_data, reader) = reader.split_at(size); + + let base_address = read_u32(obj_file, &mut section_data)? as u64; let Some(out_section) = sections.iter_mut().find(|s| s.orig_index == text_section_index) else { // Skip line info for sections we filtered out - reader.set_position(start + size as u64); continue; }; - let end = start + size as u64; - while reader.position() < end { - let line_number = read_u32(obj_file, &mut reader)?; - let statement_pos = read_u16(obj_file, &mut reader)?; + while !section_data.is_empty() { + let line_number = read_u32(obj_file, &mut section_data)?; + let statement_pos = read_u16(obj_file, &mut section_data)?; if statement_pos != 0xFFFF { log::warn!("Unhandled statement pos {}", statement_pos); } - let address_delta = read_u32(obj_file, &mut reader)? as u64; + let address_delta = read_u32(obj_file, &mut section_data)? as u64; out_section.line_info.insert(base_address + address_delta, line_number); log::debug!("Line: {:#x} -> {}", base_address + address_delta, line_number); } @@ -413,22 +416,24 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8]) // DWARF 2+ #[cfg(feature = "dwarf")] { + fn gimli_error(e: gimli::Error) -> anyhow::Error { anyhow::anyhow!("DWARF error: {e:?}") } let dwarf_cow = gimli::DwarfSections::load(|id| { Ok::<_, gimli::Error>( obj_file .section_by_name(id.name()) .and_then(|section| section.uncompressed_data().ok()) - .unwrap_or(std::borrow::Cow::Borrowed(&[][..])), + .unwrap_or(alloc::borrow::Cow::Borrowed(&[][..])), ) - })?; + }) + .map_err(gimli_error)?; let endian = match obj_file.endianness() { object::Endianness::Little => gimli::RunTimeEndian::Little, object::Endianness::Big => gimli::RunTimeEndian::Big, }; let dwarf = dwarf_cow.borrow(|section| gimli::EndianSlice::new(section, endian)); let mut iter = dwarf.units(); - if let Some(header) = iter.next()? { - let unit = dwarf.unit(header)?; + if let Some(header) = iter.next().map_err(gimli_error)? { + let unit = dwarf.unit(header).map_err(gimli_error)?; if let Some(program) = unit.line_program.clone() { let mut text_sections = obj_file.sections().filter(|s| s.kind() == SectionKind::Text); @@ -438,7 +443,7 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8]) .map(|s| &mut s.line_info); let mut rows = program.rows(); - while let Some((_header, row)) = rows.next_row()? { + while let Some((_header, row)) = rows.next_row().map_err(gimli_error)? { if let (Some(line), Some(lines)) = (row.line(), &mut lines) { lines.insert(row.address(), line.get() as u32); } @@ -453,7 +458,7 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8]) } } } - if iter.next()?.is_some() { + if iter.next().map_err(gimli_error)?.is_some() { log::warn!("Multiple units found in DWARF data, only processing the first"); } } @@ -638,7 +643,7 @@ fn combine_sections(section: ObjSection, combine: ObjSection) -> Result) -> Result<()> { - let names_to_combine: HashSet<_> = sections + let names_to_combine: BTreeSet<_> = sections .iter() .filter(|s| s.kind == ObjSectionKind::Data) .map(|s| s.name.clone()) @@ -677,14 +682,15 @@ fn combine_data_sections(sections: &mut Vec) -> Result<()> { Ok(()) } -pub fn read(obj_path: &Path, config: &DiffObjConfig) -> Result { +#[cfg(feature = "std")] +pub fn read(obj_path: &std::path::Path, config: &DiffObjConfig) -> Result { let (data, timestamp) = { - let file = fs::File::open(obj_path)?; - let timestamp = FileTime::from_last_modification_time(&file.metadata()?); + let file = std::fs::File::open(obj_path)?; + let timestamp = filetime::FileTime::from_last_modification_time(&file.metadata()?); (unsafe { memmap2::Mmap::map(&file) }?, timestamp) }; let mut obj = parse(&data, config)?; - obj.path = Some(obj_path.to_owned()); + obj.path = Some(obj_path.to_string_lossy().into_owned()); obj.timestamp = Some(timestamp); Ok(obj) } @@ -710,7 +716,7 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result { } let mut sections = filter_sections(&obj_file, split_meta.as_ref())?; - let mut section_name_counts: HashMap = HashMap::new(); + let mut section_name_counts: BTreeMap = BTreeMap::new(); for section in &mut sections { section.symbols = symbols_by_section( arch.as_ref(), @@ -733,12 +739,21 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result { } line_info(&obj_file, &mut sections, data)?; let common = common_symbols(arch.as_ref(), &obj_file, split_meta.as_ref())?; - Ok(ObjInfo { arch, path: None, timestamp: None, sections, common, split_meta }) + Ok(ObjInfo { + arch, + path: None, + #[cfg(feature = "std")] + timestamp: None, + sections, + common, + split_meta, + }) } -pub fn has_function(obj_path: &Path, symbol_name: &str) -> Result { +#[cfg(feature = "std")] +pub fn has_function(obj_path: &std::path::Path, symbol_name: &str) -> Result { let data = { - let file = fs::File::open(obj_path)?; + let file = std::fs::File::open(obj_path)?; unsafe { memmap2::Mmap::map(&file) }? }; Ok(File::parse(&*data)? diff --git a/objdiff-core/src/obj/split_meta.rs b/objdiff-core/src/obj/split_meta.rs index be056b5..ac2010d 100644 --- a/objdiff-core/src/obj/split_meta.rs +++ b/objdiff-core/src/obj/split_meta.rs @@ -1,5 +1,6 @@ -use std::{io, io::Write}; +use alloc::{string::String, vec, vec::Vec}; +use anyhow::{anyhow, Result}; use object::{elf::SHT_NOTE, Endian, ObjectSection}; pub const SPLITMETA_SECTION: &str = ".note.split"; @@ -27,10 +28,10 @@ const NT_SPLIT_MODULE_ID: u32 = u32::from_be_bytes(*b"MODI"); const NT_SPLIT_VIRTUAL_ADDRESSES: u32 = u32::from_be_bytes(*b"VIRT"); impl SplitMeta { - pub fn from_section(section: object::Section, e: E, is_64: bool) -> io::Result + pub fn from_section(section: object::Section, e: E, is_64: bool) -> Result where E: Endian { let mut result = SplitMeta::default(); - let data = section.uncompressed_data().map_err(object_io_error)?; + let data = section.uncompressed_data().map_err(object_error)?; let mut iter = NoteIterator::new(data.as_ref(), section.align(), e, is_64)?; while let Some(note) = iter.next(e)? { if note.name != ELF_NOTE_SPLIT { @@ -39,19 +40,18 @@ impl SplitMeta { match note.n_type { NT_SPLIT_GENERATOR => { let string = String::from_utf8(note.desc.to_vec()) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + .map_err(|e| anyhow::Error::from(e))?; result.generator = Some(string); } NT_SPLIT_MODULE_NAME => { let string = String::from_utf8(note.desc.to_vec()) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + .map_err(|e| anyhow::Error::from(e))?; result.module_name = Some(string); } NT_SPLIT_MODULE_ID => { - result.module_id = - Some(e.read_u32_bytes(note.desc.try_into().map_err(|_| { - io::Error::new(io::ErrorKind::InvalidData, "Invalid module ID size") - })?)); + result.module_id = Some(e.read_u32_bytes( + note.desc.try_into().map_err(|_| anyhow!("Invalid module ID size"))?, + )); } NT_SPLIT_VIRTUAL_ADDRESSES => { let vec = if is_64 { @@ -79,10 +79,11 @@ impl SplitMeta { Ok(result) } - pub fn to_writer(&self, writer: &mut W, e: E, is_64: bool) -> io::Result<()> + #[cfg(feature = "std")] + pub fn to_writer(&self, writer: &mut W, e: E, is_64: bool) -> std::io::Result<()> where E: Endian, - W: Write + ?Sized, + W: std::io::Write + ?Sized, { if let Some(generator) = &self.generator { write_note_header(writer, e, NT_SPLIT_GENERATOR, generator.len())?; @@ -137,10 +138,9 @@ impl SplitMeta { } } -/// Convert an object::read::Error to an io::Error. -fn object_io_error(err: object::read::Error) -> io::Error { - io::Error::new(io::ErrorKind::InvalidData, err) -} +/// Convert an object::read::Error to a String. +#[inline] +fn object_error(err: object::read::Error) -> anyhow::Error { anyhow::Error::new(err) } /// An ELF note entry. struct Note<'data> { @@ -161,27 +161,27 @@ where E: Endian impl<'data, E> NoteIterator<'data, E> where E: Endian { - fn new(data: &'data [u8], align: u64, e: E, is_64: bool) -> io::Result { + fn new(data: &'data [u8], align: u64, e: E, is_64: bool) -> Result { Ok(if is_64 { NoteIterator::B64( - object::read::elf::NoteIterator::new(e, align, data).map_err(object_io_error)?, + object::read::elf::NoteIterator::new(e, align, data).map_err(object_error)?, ) } else { NoteIterator::B32( object::read::elf::NoteIterator::new(e, align as u32, data) - .map_err(object_io_error)?, + .map_err(object_error)?, ) }) } - fn next(&mut self, e: E) -> io::Result>> { + fn next(&mut self, e: E) -> Result>> { match self { - NoteIterator::B32(iter) => Ok(iter.next().map_err(object_io_error)?.map(|note| Note { + NoteIterator::B32(iter) => Ok(iter.next().map_err(object_error)?.map(|note| Note { n_type: note.n_type(e), name: note.name(), desc: note.desc(), })), - NoteIterator::B64(iter) => Ok(iter.next().map_err(object_io_error)?.map(|note| Note { + NoteIterator::B64(iter) => Ok(iter.next().map_err(object_error)?.map(|note| Note { n_type: note.n_type(e), name: note.name(), desc: note.desc(), @@ -192,7 +192,8 @@ where E: Endian fn align_size_to_4(size: usize) -> usize { (size + 3) & !3 } -fn align_data_to_4(writer: &mut W, len: usize) -> io::Result<()> { +#[cfg(feature = "std")] +fn align_data_to_4(writer: &mut W, len: usize) -> std::io::Result<()> { const ALIGN_BYTES: &[u8] = &[0; 4]; if len % 4 != 0 { writer.write_all(&ALIGN_BYTES[..4 - len % 4])?; @@ -208,10 +209,11 @@ fn align_data_to_4(writer: &mut W, len: usize) -> io::Result< // Desc | variable size, padded to a 4 byte boundary const NOTE_HEADER_SIZE: usize = 12 + ((ELF_NOTE_SPLIT.len() + 4) & !3); -fn write_note_header(writer: &mut W, e: E, kind: u32, desc_len: usize) -> io::Result<()> +#[cfg(feature = "std")] +fn write_note_header(writer: &mut W, e: E, kind: u32, desc_len: usize) -> std::io::Result<()> where E: Endian, - W: Write + ?Sized, + W: std::io::Write + ?Sized, { writer.write_all(&e.write_u32_bytes(ELF_NOTE_SPLIT.len() as u32 + 1))?; // Name Size writer.write_all(&e.write_u32_bytes(desc_len as u32))?; // Desc Size diff --git a/objdiff-core/src/util.rs b/objdiff-core/src/util.rs index e46d136..b8a32b3 100644 --- a/objdiff-core/src/util.rs +++ b/objdiff-core/src/util.rs @@ -1,18 +1,15 @@ -use std::{ - fmt::{LowerHex, UpperHex}, - io::Read, -}; +use alloc::format; +use core::fmt; use anyhow::Result; -use byteorder::{NativeEndian, ReadBytesExt}; use num_traits::PrimInt; use object::{Endian, Object}; // https://stackoverflow.com/questions/44711012/how-do-i-format-a-signed-integer-to-a-sign-aware-hexadecimal-representation pub struct ReallySigned(pub(crate) N); -impl LowerHex for ReallySigned { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { +impl fmt::LowerHex for ReallySigned { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let num = self.0.to_i64().unwrap(); let prefix = if f.alternate() { "0x" } else { "" }; let bare_hex = format!("{:x}", num.abs()); @@ -20,8 +17,8 @@ impl LowerHex for ReallySigned { } } -impl UpperHex for ReallySigned { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { +impl fmt::UpperHex for ReallySigned { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let num = self.0.to_i64().unwrap(); let prefix = if f.alternate() { "0x" } else { "" }; let bare_hex = format!("{:X}", num.abs()); @@ -29,10 +26,18 @@ impl UpperHex for ReallySigned { } } -pub fn read_u32(obj_file: &object::File, reader: &mut R) -> Result { - Ok(obj_file.endianness().read_u32(reader.read_u32::()?)) +pub fn read_u32(obj_file: &object::File, reader: &mut &[u8]) -> Result { + if reader.len() < 4 { + return Err(anyhow::anyhow!("Not enough bytes to read u32")); + } + let value = u32::from_ne_bytes(reader[..4].try_into()?); + Ok(obj_file.endianness().read_u32(value)) } -pub fn read_u16(obj_file: &object::File, reader: &mut R) -> Result { - Ok(obj_file.endianness().read_u16(reader.read_u16::()?)) +pub fn read_u16(obj_file: &object::File, reader: &mut &[u8]) -> Result { + if reader.len() < 2 { + return Err(anyhow::anyhow!("Not enough bytes to read u16")); + } + let value = u16::from_ne_bytes(reader[..2].try_into()?); + Ok(obj_file.endianness().read_u16(value)) } diff --git a/objdiff-core/src/wasm/api.rs b/objdiff-core/src/wasm/api.rs new file mode 100644 index 0000000..a8b5209 --- /dev/null +++ b/objdiff-core/src/wasm/api.rs @@ -0,0 +1,99 @@ +use alloc::{ + format, + str::FromStr, + string::{String, ToString}, + vec::Vec, +}; +use core::cell::RefCell; + +use prost::Message; + +use crate::{bindings::diff::DiffResult, diff, obj}; + +wit_bindgen::generate!({ + world: "api", +}); + +use exports::objdiff::core::diff::{ + DiffConfigBorrow, Guest as GuestTypes, GuestDiffConfig, GuestObject, Object, ObjectBorrow, +}; + +struct Component; + +impl Guest for Component { + fn init() -> Result<(), String> { + // console_error_panic_hook::set_once(); + // #[cfg(debug_assertions)] + // console_log::init_with_level(log::Level::Debug).map_err(|e| e.to_string())?; + // #[cfg(not(debug_assertions))] + // console_log::init_with_level(log::Level::Info).map_err(|e| e.to_string())?; + Ok(()) + } + + fn version() -> String { env!("CARGO_PKG_VERSION").to_string() } +} + +#[repr(transparent)] +struct ResourceDiffConfig(RefCell); + +impl GuestTypes for Component { + type DiffConfig = ResourceDiffConfig; + type Object = obj::ObjInfo; + + fn run_diff( + left: Option, + right: Option, + diff_config: DiffConfigBorrow, + ) -> Result, String> { + let diff_config = diff_config.get::().0.borrow(); + let result = run_diff_internal( + left.as_ref().map(|o| o.get()), + right.as_ref().map(|o| o.get()), + &diff_config, + &diff::MappingConfig::default(), + ) + .map_err(|e| e.to_string())?; + Ok(result.encode_to_vec()) + } +} + +impl GuestDiffConfig for ResourceDiffConfig { + fn new() -> Self { Self(RefCell::new(diff::DiffObjConfig::default())) } + + fn set_property(&self, key: String, value: String) -> Result<(), String> { + let id = diff::ConfigPropertyId::from_str(&key) + .map_err(|_| format!("Invalid property key {:?}", key))?; + self.0 + .borrow_mut() + .set_property_value_str(id, &value) + .map_err(|_| format!("Invalid property value {:?}", value)) + } + + fn get_property(&self, key: String) -> Result { + let id = diff::ConfigPropertyId::from_str(&key) + .map_err(|_| format!("Invalid property key {:?}", key))?; + Ok(self.0.borrow().get_property_value(id).to_string()) + } +} + +impl GuestObject for obj::ObjInfo { + fn parse(data: Vec, diff_config: DiffConfigBorrow) -> Result { + let diff_config = diff_config.get::().0.borrow(); + obj::read::parse(&data, &diff_config).map(|o| Object::new(o)).map_err(|e| e.to_string()) + } +} + +fn run_diff_internal( + left: Option<&obj::ObjInfo>, + right: Option<&obj::ObjInfo>, + diff_config: &diff::DiffObjConfig, + mapping_config: &diff::MappingConfig, +) -> anyhow::Result { + log::debug!("Running diff with config: {:?}", diff_config); + let result = diff::diff_objs(diff_config, mapping_config, left, right, None)?; + let left = left.and_then(|o| result.left.as_ref().map(|d| (o, d))); + let right = right.and_then(|o| result.right.as_ref().map(|d| (o, d))); + Ok(DiffResult::new(left, right)) +} + +export!(Component); diff --git a/objdiff-core/src/wasm/cabi_realloc.rs b/objdiff-core/src/wasm/cabi_realloc.rs new file mode 100644 index 0000000..40107ae --- /dev/null +++ b/objdiff-core/src/wasm/cabi_realloc.rs @@ -0,0 +1,64 @@ +//! This module contains a canonical definition of the `cabi_realloc` function +//! for the component model. +//! +//! The component model's canonical ABI for representing datatypes in memory +//! makes use of this function when transferring lists and strings, for example. +//! This function behaves like C's `realloc` but also takes alignment into +//! account. +//! +//! Components are notably not required to export this function, but nearly +//! all components end up doing so currently. This definition in the standard +//! library removes the need for all compilations to define this themselves. +//! +//! More information about the canonical ABI can be found at +//! +//! +//! Note that the name of this function is not standardized in the canonical ABI +//! at this time. Instead it's a convention of the "componentization process" +//! where a core wasm module is converted to a component to use this name. +//! Additionally this is not the only possible definition of this function, so +//! this is defined as a "weak" symbol. This means that other definitions are +//! allowed to overwrite it if they are present in a compilation. + +use alloc::{alloc, Layout}; +use core::ptr; + +#[used] +static FORCE_CODEGEN_OF_CABI_REALLOC: unsafe extern "C" fn( + *mut u8, + usize, + usize, + usize, +) -> *mut u8 = cabi_realloc; + +#[no_mangle] +pub unsafe extern "C" fn cabi_realloc( + old_ptr: *mut u8, + old_len: usize, + align: usize, + new_len: usize, +) -> *mut u8 { + let layout; + let ptr = if old_len == 0 { + if new_len == 0 { + return ptr::without_provenance_mut(align); + } + layout = Layout::from_size_align_unchecked(new_len, align); + alloc::alloc(layout) + } else { + debug_assert_ne!(new_len, 0, "non-zero old_len requires non-zero new_len!"); + layout = Layout::from_size_align_unchecked(old_len, align); + alloc::realloc(old_ptr, layout, new_len) + }; + if ptr.is_null() { + // Print a nice message in debug mode, but in release mode don't + // pull in so many dependencies related to printing so just emit an + // `unreachable` instruction. + if cfg!(debug_assertions) { + alloc::handle_alloc_error(layout); + } else { + core::unreachable!("allocation failed") + } + } + ptr +} diff --git a/objdiff-core/src/wasm/mod.rs b/objdiff-core/src/wasm/mod.rs new file mode 100644 index 0000000..894901f --- /dev/null +++ b/objdiff-core/src/wasm/mod.rs @@ -0,0 +1,18 @@ +mod api; + +#[cfg(not(feature = "std"))] +mod cabi_realloc; + +#[cfg(not(feature = "std"))] +static mut ARENA: [u8; 10000] = [0; 10000]; + +#[cfg(not(feature = "std"))] +#[global_allocator] +static ALLOCATOR: talc::Talck, talc::ClaimOnOom> = talc::Talc::new(unsafe { + talc::ClaimOnOom::new(talc::Span::from_array(core::ptr::addr_of!(ARENA) as *mut [u8; 10000])) +}) +.lock(); + +#[cfg(not(feature = "std"))] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} } diff --git a/objdiff-core/wit/objdiff.wit b/objdiff-core/wit/objdiff.wit new file mode 100644 index 0000000..73007b2 --- /dev/null +++ b/objdiff-core/wit/objdiff.wit @@ -0,0 +1,29 @@ +package objdiff:core; + +interface diff { + resource diff-config { + constructor(); + set-property: func(id: string, value: string) -> result<_, string>; + get-property: func(id: string) -> result; + } + + resource object { + parse: static func( + data: list, + config: borrow, + ) -> result; + } + + run-diff: func( + left: option>, + right: option>, + config: borrow, + ) -> result, string>; +} + +world api { + export diff; + + export init: func() -> result<_, string>; + export version: func() -> string; +} diff --git a/objdiff-gui/Cargo.toml b/objdiff-gui/Cargo.toml index 1e6f773..9c44842 100644 --- a/objdiff-gui/Cargo.toml +++ b/objdiff-gui/Cargo.toml @@ -29,7 +29,7 @@ bytes = "1.9" cfg-if = "1.0" const_format = "0.2" cwdemangle = "1.0" -cwextab = "1.0" +cwextab = { version = "1.0", git = "https://github.com/encounter/cwextab.git" } dirs = "5.0" egui = "0.30" egui_extras = "0.30" @@ -51,6 +51,7 @@ serde_json = "1.0" shell-escape = "0.1" strum = { version = "0.26", features = ["derive"] } time = { version = "0.3", features = ["formatting", "local-offset"] } +typed-path = "0.10" # Keep version in sync with egui [dependencies.eframe] diff --git a/objdiff-gui/src/app.rs b/objdiff-gui/src/app.rs index ddc395e..06a0d30 100644 --- a/objdiff-gui/src/app.rs +++ b/objdiff-gui/src/app.rs @@ -1,4 +1,5 @@ use std::{ + collections::BTreeMap, default::Default, fs, path::{Path, PathBuf}, @@ -15,13 +16,15 @@ use globset::Glob; use objdiff_core::{ build::watcher::{create_watcher, Watcher}, config::{ - build_globset, default_watch_patterns, save_project_config, ProjectConfig, - ProjectConfigInfo, ProjectObject, ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS, + build_globset, default_watch_patterns, path::platform_path_serde_option, + save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, + DEFAULT_WATCH_PATTERNS, }, diff::DiffObjConfig, jobs::{Job, JobQueue, JobResult}, }; use time::UtcOffset; +use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf}; use crate::{ app_config::{deserialize_config, AppConfigVersion}, @@ -90,26 +93,57 @@ impl Default for ViewState { #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct ObjectConfig { pub name: String, - pub target_path: Option, - pub base_path: Option, + #[serde(default, with = "platform_path_serde_option")] + pub target_path: Option, + #[serde(default, with = "platform_path_serde_option")] + pub base_path: Option, pub reverse_fn_order: Option, pub complete: Option, - pub scratch: Option, - pub source_path: Option, #[serde(default)] - pub symbol_mappings: SymbolMappings, + pub hidden: bool, + pub scratch: Option, + #[serde(default, with = "platform_path_serde_option")] + pub source_path: Option, + #[serde(default)] + pub symbol_mappings: BTreeMap, } -impl From<&ProjectObject> for ObjectConfig { - fn from(object: &ProjectObject) -> Self { +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 if let Some(path) = &object.target_path { + Some(project_dir.join(path.with_platform_encoding())) + } else { + None + }; + 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 if let Some(path) = &object.base_path { + Some(project_dir.join(path.with_platform_encoding())) + } else { + None + }; + let source_path = + object.source_path().map(|s| project_dir.join(s.with_platform_encoding())); Self { name: object.name().to_string(), - target_path: object.target_path.clone(), - base_path: object.base_path.clone(), + target_path, + base_path, reverse_fn_order: object.reverse_fn_order(), complete: object.complete(), + hidden: object.hidden(), scratch: object.scratch.clone(), - source_path: object.source_path().cloned(), + source_path, symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(), } } @@ -120,7 +154,7 @@ fn bool_true() -> bool { true } pub struct AppState { pub config: AppConfig, - pub objects: Vec, + pub objects: Vec, pub object_nodes: Vec, pub watcher_change: bool, pub config_change: bool, @@ -170,12 +204,12 @@ pub struct AppConfig { pub custom_args: Option>, #[serde(default)] pub selected_wsl_distro: Option, - #[serde(default)] - pub project_dir: Option, - #[serde(default)] - pub target_obj_dir: Option, - #[serde(default)] - pub base_obj_dir: Option, + #[serde(default, with = "platform_path_serde_option")] + pub project_dir: Option, + #[serde(default, with = "platform_path_serde_option")] + pub target_obj_dir: Option, + #[serde(default, with = "platform_path_serde_option")] + pub base_obj_dir: Option, #[serde(default)] pub selected_obj: Option, #[serde(default = "bool_true")] @@ -189,7 +223,7 @@ pub struct AppConfig { #[serde(default = "default_watch_patterns")] pub watch_patterns: Vec, #[serde(default)] - pub recent_projects: Vec, + pub recent_projects: Vec, #[serde(default)] pub diff_obj_config: DiffObjConfig, } @@ -217,12 +251,12 @@ impl Default for AppConfig { } impl AppState { - pub fn set_project_dir(&mut self, path: PathBuf) { + pub fn set_project_dir(&mut self, path: Utf8PlatformPathBuf) { self.config.recent_projects.retain(|p| p != &path); if self.config.recent_projects.len() > 9 { self.config.recent_projects.truncate(9); } - self.config.recent_projects.insert(0, path.clone()); + self.config.recent_projects.insert(0, path.to_string()); self.config.project_dir = Some(path); self.config.target_obj_dir = None; self.config.base_obj_dir = None; @@ -240,7 +274,7 @@ impl AppState { self.selecting_right = None; } - pub fn set_target_obj_dir(&mut self, path: PathBuf) { + pub fn set_target_obj_dir(&mut self, path: Utf8PlatformPathBuf) { self.config.target_obj_dir = Some(path); self.config.selected_obj = None; self.obj_change = true; @@ -249,7 +283,7 @@ impl AppState { self.selecting_right = None; } - pub fn set_base_obj_dir(&mut self, path: PathBuf) { + pub fn set_base_obj_dir(&mut self, path: Utf8PlatformPathBuf) { self.config.base_obj_dir = Some(path); self.config.selected_obj = None; self.obj_change = true; @@ -360,14 +394,8 @@ impl AppState { Some(object.symbol_mappings.clone()) }; } - if let Some(existing) = - self.objects.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name)) - { - existing.symbol_mappings = if object.symbol_mappings.is_empty() { - None - } else { - Some(object.symbol_mappings.clone()) - }; + if let Some(existing) = self.objects.iter_mut().find(|u| u.name == object.name) { + existing.symbol_mappings = object.symbol_mappings.clone(); } } // Save the updated project config @@ -530,8 +558,13 @@ impl App { match build_globset(&state.config.watch_patterns) .map_err(anyhow::Error::new) .and_then(|globset| { - create_watcher(self.modified.clone(), project_dir, globset, egui_waker(ctx)) - .map_err(anyhow::Error::new) + create_watcher( + self.modified.clone(), + project_dir.as_ref(), + globset, + egui_waker(ctx), + ) + .map_err(anyhow::Error::new) }) { Ok(watcher) => self.watcher = Some(watcher), Err(e) => log::error!("Failed to create watcher: {e}"), @@ -672,8 +705,11 @@ impl eframe::App for App { }; ui.separator(); for path in recent_projects { - if ui.button(format!("{}", path.display())).clicked() { - state.write().unwrap().set_project_dir(path); + if ui.button(&path).clicked() { + state + .write() + .unwrap() + .set_project_dir(Utf8PlatformPathBuf::from(path)); ui.close_menu(); } } @@ -776,8 +812,8 @@ impl eframe::App for App { } #[inline] -fn file_modified(path: &Path, last_ts: FileTime) -> bool { - if let Ok(metadata) = fs::metadata(path) { +fn file_modified>(path: P, last_ts: FileTime) -> bool { + if let Ok(metadata) = fs::metadata(path.as_ref()) { FileTime::from_last_modification_time(&metadata) != last_ts } else { false diff --git a/objdiff-gui/src/app_config.rs b/objdiff-gui/src/app_config.rs index 19d28dc..0f26876 100644 --- a/objdiff-gui/src/app_config.rs +++ b/objdiff-gui/src/app_config.rs @@ -1,14 +1,15 @@ -use std::path::PathBuf; +use std::collections::BTreeMap; use eframe::Storage; use globset::Glob; use objdiff_core::{ - config::{ScratchConfig, SymbolMappings}, + config::ScratchConfig, diff::{ ArmArchVersion, ArmR9Usage, DiffObjConfig, FunctionRelocDiffs, MipsAbi, MipsInstrCategory, X86Formatter, }, }; +use typed_path::{Utf8PlatformPathBuf, Utf8UnixPathBuf}; use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY}; @@ -62,7 +63,7 @@ pub struct ScratchConfigV2 { #[serde(default)] pub c_flags: Option, #[serde(default)] - pub ctx_path: Option, + pub ctx_path: Option, #[serde(default)] pub build_ctx: Option, #[serde(default)] @@ -75,7 +76,7 @@ impl ScratchConfigV2 { platform: self.platform, compiler: self.compiler, c_flags: self.c_flags, - ctx_path: self.ctx_path, + ctx_path: self.ctx_path.map(Utf8UnixPathBuf::from), build_ctx: self.build_ctx, preset_id: self.preset_id, } @@ -85,26 +86,27 @@ impl ScratchConfigV2 { #[derive(serde::Deserialize, serde::Serialize)] pub struct ObjectConfigV2 { pub name: String, - pub target_path: Option, - pub base_path: Option, + pub target_path: Option, + pub base_path: Option, pub reverse_fn_order: Option, pub complete: Option, pub scratch: Option, pub source_path: Option, #[serde(default)] - pub symbol_mappings: SymbolMappings, + pub symbol_mappings: BTreeMap, } impl ObjectConfigV2 { fn into_config(self) -> ObjectConfig { ObjectConfig { name: self.name, - target_path: self.target_path, - base_path: self.base_path, + target_path: self.target_path.map(Utf8PlatformPathBuf::from), + base_path: self.base_path.map(Utf8PlatformPathBuf::from), reverse_fn_order: self.reverse_fn_order, complete: self.complete, + hidden: false, scratch: self.scratch.map(|scratch| scratch.into_config()), - source_path: self.source_path, + source_path: None, symbol_mappings: self.symbol_mappings, } } @@ -120,11 +122,11 @@ pub struct AppConfigV2 { #[serde(default)] pub selected_wsl_distro: Option, #[serde(default)] - pub project_dir: Option, + pub project_dir: Option, #[serde(default)] - pub target_obj_dir: Option, + pub target_obj_dir: Option, #[serde(default)] - pub base_obj_dir: Option, + pub base_obj_dir: Option, #[serde(default)] pub selected_obj: Option, #[serde(default = "bool_true")] @@ -138,7 +140,7 @@ pub struct AppConfigV2 { #[serde(default)] pub watch_patterns: Vec, #[serde(default)] - pub recent_projects: Vec, + pub recent_projects: Vec, #[serde(default)] pub diff_obj_config: DiffObjConfigV1, } @@ -150,9 +152,9 @@ impl AppConfigV2 { custom_make: self.custom_make, custom_args: self.custom_args, selected_wsl_distro: self.selected_wsl_distro, - project_dir: self.project_dir, - target_obj_dir: self.target_obj_dir, - base_obj_dir: self.base_obj_dir, + project_dir: self.project_dir.map(Utf8PlatformPathBuf::from), + target_obj_dir: self.target_obj_dir.map(Utf8PlatformPathBuf::from), + base_obj_dir: self.base_obj_dir.map(Utf8PlatformPathBuf::from), selected_obj: self.selected_obj.map(|obj| obj.into_config()), build_base: self.build_base, build_target: self.build_target, @@ -175,7 +177,7 @@ pub struct ScratchConfigV1 { #[serde(default)] pub c_flags: Option, #[serde(default)] - pub ctx_path: Option, + pub ctx_path: Option, #[serde(default)] pub build_ctx: bool, } @@ -186,7 +188,7 @@ impl ScratchConfigV1 { platform: self.platform, compiler: self.compiler, c_flags: self.c_flags, - ctx_path: self.ctx_path, + ctx_path: self.ctx_path.map(Utf8UnixPathBuf::from), build_ctx: self.build_ctx.then_some(true), preset_id: None, } @@ -196,8 +198,8 @@ impl ScratchConfigV1 { #[derive(serde::Deserialize, serde::Serialize)] pub struct ObjectConfigV1 { pub name: String, - pub target_path: Option, - pub base_path: Option, + pub target_path: Option, + pub base_path: Option, pub reverse_fn_order: Option, pub complete: Option, pub scratch: Option, @@ -208,12 +210,12 @@ impl ObjectConfigV1 { fn into_config(self) -> ObjectConfig { ObjectConfig { name: self.name, - target_path: self.target_path, - base_path: self.base_path, + target_path: self.target_path.map(Utf8PlatformPathBuf::from), + base_path: self.base_path.map(Utf8PlatformPathBuf::from), reverse_fn_order: self.reverse_fn_order, complete: self.complete, scratch: self.scratch.map(|scratch| scratch.into_config()), - source_path: self.source_path, + source_path: None, ..Default::default() } } @@ -298,11 +300,11 @@ pub struct AppConfigV1 { #[serde(default)] pub selected_wsl_distro: Option, #[serde(default)] - pub project_dir: Option, + pub project_dir: Option, #[serde(default)] - pub target_obj_dir: Option, + pub target_obj_dir: Option, #[serde(default)] - pub base_obj_dir: Option, + pub base_obj_dir: Option, #[serde(default)] pub selected_obj: Option, #[serde(default = "bool_true")] @@ -316,7 +318,7 @@ pub struct AppConfigV1 { #[serde(default)] pub watch_patterns: Vec, #[serde(default)] - pub recent_projects: Vec, + pub recent_projects: Vec, #[serde(default)] pub diff_obj_config: DiffObjConfigV1, } @@ -328,9 +330,9 @@ impl AppConfigV1 { custom_make: self.custom_make, custom_args: self.custom_args, selected_wsl_distro: self.selected_wsl_distro, - project_dir: self.project_dir, - target_obj_dir: self.target_obj_dir, - base_obj_dir: self.base_obj_dir, + project_dir: self.project_dir.map(Utf8PlatformPathBuf::from), + target_obj_dir: self.target_obj_dir.map(Utf8PlatformPathBuf::from), + base_obj_dir: self.base_obj_dir.map(Utf8PlatformPathBuf::from), selected_obj: self.selected_obj.map(|obj| obj.into_config()), build_base: self.build_base, build_target: self.build_target, @@ -347,8 +349,8 @@ impl AppConfigV1 { #[derive(serde::Deserialize, serde::Serialize)] pub struct ObjectConfigV0 { pub name: String, - pub target_path: PathBuf, - pub base_path: PathBuf, + pub target_path: String, + pub base_path: String, pub reverse_fn_order: Option, } @@ -356,8 +358,8 @@ impl ObjectConfigV0 { fn into_config(self) -> ObjectConfig { ObjectConfig { name: self.name, - target_path: Some(self.target_path), - base_path: Some(self.base_path), + target_path: Some(Utf8PlatformPathBuf::from(self.target_path)), + base_path: Some(Utf8PlatformPathBuf::from(self.base_path)), reverse_fn_order: self.reverse_fn_order, ..Default::default() } @@ -368,9 +370,9 @@ impl ObjectConfigV0 { pub struct AppConfigV0 { pub custom_make: Option, pub selected_wsl_distro: Option, - pub project_dir: Option, - pub target_obj_dir: Option, - pub base_obj_dir: Option, + pub project_dir: Option, + pub target_obj_dir: Option, + pub base_obj_dir: Option, pub selected_obj: Option, pub build_target: bool, pub auto_update_check: bool, @@ -383,9 +385,9 @@ impl AppConfigV0 { AppConfig { custom_make: self.custom_make, selected_wsl_distro: self.selected_wsl_distro, - project_dir: self.project_dir, - target_obj_dir: self.target_obj_dir, - base_obj_dir: self.base_obj_dir, + project_dir: self.project_dir.map(Utf8PlatformPathBuf::from), + target_obj_dir: self.target_obj_dir.map(Utf8PlatformPathBuf::from), + base_obj_dir: self.base_obj_dir.map(Utf8PlatformPathBuf::from), selected_obj: self.selected_obj.map(|obj| obj.into_config()), build_target: self.build_target, auto_update_check: self.auto_update_check, diff --git a/objdiff-gui/src/config.rs b/objdiff-gui/src/config.rs index 86d14e0..1ddda64 100644 --- a/objdiff-gui/src/config.rs +++ b/objdiff-gui/src/config.rs @@ -1,8 +1,7 @@ -use std::path::{Component, Path}; - use anyhow::Result; use globset::Glob; -use objdiff_core::config::{try_project_config, ProjectObject, DEFAULT_WATCH_PATTERNS}; +use objdiff_core::config::{try_project_config, DEFAULT_WATCH_PATTERNS}; +use typed_path::{Utf8UnixComponent, Utf8UnixPath}; use crate::app::{AppState, ObjectConfig}; @@ -47,32 +46,19 @@ fn find_dir<'a>( unreachable!(); } -fn build_nodes( - units: &mut [ProjectObject], - project_dir: &Path, - target_obj_dir: Option<&Path>, - base_obj_dir: Option<&Path>, -) -> Vec { +fn build_nodes(units: &mut [ObjectConfig]) -> Vec { let mut nodes = vec![]; for (idx, unit) in units.iter_mut().enumerate() { - unit.resolve_paths(project_dir, target_obj_dir, base_obj_dir); let mut out_nodes = &mut nodes; - let path = if let Some(name) = &unit.name { - Path::new(name) - } else if let Some(path) = &unit.path { - path - } else { - continue; - }; + let path = Utf8UnixPath::new(&unit.name); if let Some(parent) = path.parent() { for component in parent.components() { - if let Component::Normal(name) = component { - let name = name.to_str().unwrap(); + if let Utf8UnixComponent::Normal(name) = component { out_nodes = find_dir(name, out_nodes); } } } - let filename = path.file_name().unwrap().to_str().unwrap().to_string(); + let filename = path.file_name().unwrap().to_string(); out_nodes.push(ProjectObjectNode::Unit(filename, idx)); } // Within the top-level module directories, join paths. Leave the @@ -90,13 +76,18 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> { let Some(project_dir) = &state.config.project_dir else { return Ok(()); }; - if let Some((result, info)) = try_project_config(project_dir) { + if let Some((result, info)) = try_project_config(project_dir.as_ref()) { let project_config = result?; state.config.custom_make = project_config.custom_make.clone(); state.config.custom_args = project_config.custom_args.clone(); - state.config.target_obj_dir = - project_config.target_dir.as_deref().map(|p| project_dir.join(p)); - state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p)); + state.config.target_obj_dir = project_config + .target_dir + .as_deref() + .map(|p| project_dir.join(p.with_platform_encoding())); + state.config.base_obj_dir = project_config + .base_dir + .as_deref() + .map(|p| project_dir.join(p.with_platform_encoding())); state.config.build_base = project_config.build_base.unwrap_or(true); state.config.build_target = project_config.build_target.unwrap_or(false); if let Some(watch_patterns) = &project_config.watch_patterns { @@ -109,21 +100,28 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> { DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(); } state.watcher_change = true; - state.objects = project_config.units.clone().unwrap_or_default(); - state.object_nodes = build_nodes( - &mut state.objects, - project_dir, - state.config.target_obj_dir.as_deref(), - state.config.base_obj_dir.as_deref(), - ); + state.objects = project_config + .units + .as_deref() + .unwrap_or_default() + .iter() + .map(|o| { + ObjectConfig::new( + o, + project_dir, + state.config.target_obj_dir.as_deref(), + state.config.base_obj_dir.as_deref(), + ) + }) + .collect::>(); + state.object_nodes = build_nodes(&mut state.objects); state.current_project_config = Some(project_config); state.project_config_info = Some(info); // Reload selected object if let Some(selected_obj) = &state.config.selected_obj { - if let Some(obj) = state.objects.iter().find(|o| o.name() == selected_obj.name) { - let config = ObjectConfig::from(obj); - state.set_selected_obj(config); + if let Some(obj) = state.objects.iter().find(|o| o.name == selected_obj.name) { + state.set_selected_obj(obj.clone()); } else { state.clear_selected_obj(); } diff --git a/objdiff-gui/src/jobs.rs b/objdiff-gui/src/jobs.rs index 0f9168b..f9dcd38 100644 --- a/objdiff-gui/src/jobs.rs +++ b/objdiff-gui/src/jobs.rs @@ -73,7 +73,7 @@ fn create_scratch_config( platform: scratch_config.platform.clone().unwrap_or_default(), compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(), function_name, - target_obj: target_path.to_path_buf(), + target_obj: target_path.clone(), preset_id: scratch_config.preset_id, }) } diff --git a/objdiff-gui/src/views/config.rs b/objdiff-gui/src/views/config.rs index abcf473..aa0761d 100644 --- a/objdiff-gui/src/views/config.rs +++ b/objdiff-gui/src/views/config.rs @@ -1,9 +1,6 @@ #[cfg(all(windows, feature = "wsl"))] use std::string::FromUtf16Error; -use std::{ - mem::take, - path::{Path, PathBuf, MAIN_SEPARATOR}, -}; +use std::{mem::take, path::MAIN_SEPARATOR}; #[cfg(all(windows, feature = "wsl"))] use anyhow::{Context, Result}; @@ -13,13 +10,14 @@ use egui::{ }; use globset::Glob; use objdiff_core::{ - config::{ProjectObject, DEFAULT_WATCH_PATTERNS}, + config::{path::check_path_buf, DEFAULT_WATCH_PATTERNS}, diff::{ ConfigEnum, ConfigEnumVariantInfo, ConfigPropertyId, ConfigPropertyKind, ConfigPropertyValue, CONFIG_GROUPS, }, jobs::{check_update::CheckUpdateResult, Job, JobQueue, JobResult}, }; +use typed_path::Utf8PlatformPathBuf; use crate::{ app::{AppConfig, AppState, AppStateRef, ObjectConfig}, @@ -89,7 +87,7 @@ impl ConfigViewState { if let Ok(obj_path) = path.strip_prefix(base_dir) { let target_path = target_dir.join(obj_path); guard.set_selected_obj(ObjectConfig { - name: obj_path.display().to_string(), + name: obj_path.to_string(), target_path: Some(target_path), base_path: Some(path), ..Default::default() @@ -97,7 +95,7 @@ impl ConfigViewState { } else if let Ok(obj_path) = path.strip_prefix(target_dir) { let base_path = base_dir.join(obj_path); guard.set_selected_obj(ObjectConfig { - name: obj_path.display().to_string(), + name: obj_path.to_string(), target_path: Some(path), base_path: Some(base_path), ..Default::default() @@ -169,10 +167,7 @@ pub fn config_ui( ) { let mut state_guard = state.write().unwrap(); let AppState { - config: - AppConfig { - project_dir, target_obj_dir, base_obj_dir, selected_obj, auto_update_check, .. - }, + config: AppConfig { target_obj_dir, base_obj_dir, selected_obj, auto_update_check, .. }, objects, object_nodes, .. @@ -223,9 +218,9 @@ pub fn config_ui( } }); - let selected_index = selected_obj.as_ref().and_then(|selected_obj| { - objects.iter().position(|obj| obj.name.as_ref() == Some(&selected_obj.name)) - }); + let selected_index = selected_obj + .as_ref() + .and_then(|selected_obj| objects.iter().position(|obj| obj.name == selected_obj.name)); let mut new_selected_index = selected_index; if objects.is_empty() { if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { @@ -324,22 +319,14 @@ pub fn config_ui( config_state.show_hidden, ) }) { - display_node( - ui, - &mut new_selected_index, - project_dir.as_deref(), - objects, - &node, - appearance, - node_open, - ); + display_node(ui, &mut new_selected_index, objects, &node, appearance, node_open); } }); } if new_selected_index != selected_index { if let Some(idx) = new_selected_index { // Will set obj_changed, which will trigger a rebuild - let config = ObjectConfig::from(&objects[idx]); + let config = objects[idx].clone(); state_guard.set_selected_obj(config); } } @@ -353,9 +340,8 @@ pub fn config_ui( fn display_unit( ui: &mut egui::Ui, selected_obj: &mut Option, - project_dir: Option<&Path>, name: &str, - units: &[ProjectObject], + units: &[ObjectConfig], index: usize, appearance: &Appearance, ) { @@ -363,7 +349,7 @@ fn display_unit( let selected = *selected_obj == Some(index); let color = if selected { appearance.emphasized_text_color - } else if let Some(complete) = object.complete() { + } else if let Some(complete) = object.complete { if complete { appearance.insert_color } else { @@ -382,26 +368,22 @@ fn display_unit( .color(color), ) .ui(ui); - if get_source_path(project_dir, object).is_some() { - response.context_menu(|ui| object_context_ui(ui, object, project_dir)); + if object.source_path.is_some() { + response.context_menu(|ui| object_context_ui(ui, object)); } if response.clicked() { *selected_obj = Some(index); } } -fn get_source_path(project_dir: Option<&Path>, object: &ProjectObject) -> Option { - project_dir.and_then(|dir| object.source_path().map(|path| dir.join(path))) -} - -fn object_context_ui(ui: &mut egui::Ui, object: &ProjectObject, project_dir: Option<&Path>) { - if let Some(source_path) = get_source_path(project_dir, object) { +fn object_context_ui(ui: &mut egui::Ui, object: &ObjectConfig) { + if let Some(source_path) = &object.source_path { if ui .button("Open source file") .on_hover_text("Open the source file in the default editor") .clicked() { - log::info!("Opening file {}", source_path.display()); + log::info!("Opening file {}", source_path); if let Err(e) = open::that_detached(&source_path) { log::error!("Failed to open source file: {e}"); } @@ -422,15 +404,14 @@ enum NodeOpen { fn display_node( ui: &mut egui::Ui, selected_obj: &mut Option, - project_dir: Option<&Path>, - units: &[ProjectObject], + units: &[ObjectConfig], node: &ProjectObjectNode, appearance: &Appearance, node_open: NodeOpen, ) { match node { ProjectObjectNode::Unit(name, idx) => { - display_unit(ui, selected_obj, project_dir, name, units, *idx, appearance); + display_unit(ui, selected_obj, name, units, *idx, appearance); } ProjectObjectNode::Dir(name, children) => { let contains_obj = selected_obj.map(|idx| contains_node(node, idx)); @@ -456,7 +437,7 @@ fn display_node( .open(open) .show(ui, |ui| { for node in children { - display_node(ui, selected_obj, project_dir, units, node, appearance, node_open); + display_node(ui, selected_obj, units, node, appearance, node_open); } }); } @@ -473,7 +454,7 @@ fn contains_node(node: &ProjectObjectNode, selected_obj: usize) -> bool { } fn filter_node( - units: &[ProjectObject], + units: &[ObjectConfig], node: &ProjectObjectNode, search: &str, filter_diffable: bool, @@ -485,8 +466,8 @@ fn filter_node( let unit = &units[*idx]; if (search.is_empty() || name.to_ascii_lowercase().contains(search)) && (!filter_diffable || (unit.base_path.is_some() && unit.target_path.is_some())) - && (!filter_incomplete || matches!(unit.complete(), None | Some(false))) - && (show_hidden || !unit.hidden()) + && (!filter_incomplete || matches!(unit.complete, None | Some(false))) + && (show_hidden || !unit.hidden) { Some(node.clone()) } else { @@ -524,13 +505,16 @@ fn subheading(ui: &mut egui::Ui, text: &str, appearance: &Appearance) { ); } -fn format_path(path: &Option, appearance: &Appearance) -> RichText { +fn format_path(path: &Option, appearance: &Appearance) -> RichText { let mut color = appearance.replace_color; let text = if let Some(dir) = path { - if let Some(rel) = dirs::home_dir().and_then(|home| dir.strip_prefix(&home).ok()) { - format!("~{}{}", MAIN_SEPARATOR, rel.display()) + if let Some(rel) = dirs::home_dir() + .and_then(|home| check_path_buf(home).ok()) + .and_then(|home| dir.strip_prefix(&home).ok()) + { + format!("~{}{}", MAIN_SEPARATOR, rel) } else { - format!("{}", dir.display()) + dir.to_string() } } else { color = appearance.delete_color; @@ -544,7 +528,7 @@ pub const CONFIG_DISABLED_TEXT: &str = fn pick_folder_ui( ui: &mut egui::Ui, - dir: &Option, + dir: &Option, label: &str, tooltip: impl FnOnce(&mut egui::Ui), appearance: &Appearance, diff --git a/objdiff-gui/src/views/file.rs b/objdiff-gui/src/views/file.rs index 37d3de6..1ecca42 100644 --- a/objdiff-gui/src/views/file.rs +++ b/objdiff-gui/src/views/file.rs @@ -1,16 +1,18 @@ use std::{future::Future, path::PathBuf, pin::Pin, thread::JoinHandle}; +use objdiff_core::config::path::check_path_buf; use pollster::FutureExt; use rfd::FileHandle; +use typed_path::Utf8PlatformPathBuf; #[derive(Default)] pub enum FileDialogResult { #[default] None, - ProjectDir(PathBuf), - TargetDir(PathBuf), - BaseDir(PathBuf), - Object(PathBuf), + ProjectDir(Utf8PlatformPathBuf), + TargetDir(Utf8PlatformPathBuf), + BaseDir(Utf8PlatformPathBuf), + Object(Utf8PlatformPathBuf), } #[derive(Default)] @@ -22,7 +24,7 @@ impl FileDialogState { pub fn queue(&mut self, init: InitCb, result_cb: ResultCb) where InitCb: FnOnce() -> Pin> + Send>>, - ResultCb: FnOnce(PathBuf) -> FileDialogResult + Send + 'static, + ResultCb: FnOnce(Utf8PlatformPathBuf) -> FileDialogResult + Send + 'static, { if self.thread.is_some() { return; @@ -30,7 +32,8 @@ impl FileDialogState { let future = init(); self.thread = Some(std::thread::spawn(move || { if let Some(handle) = future.block_on() { - result_cb(PathBuf::from(handle)) + let path = PathBuf::from(handle); + check_path_buf(path).map(result_cb).unwrap_or(FileDialogResult::None) } else { FileDialogResult::None } diff --git a/objdiff-gui/src/views/symbol_diff.rs b/objdiff-gui/src/views/symbol_diff.rs index 23e95aa..ff6c5ff 100644 --- a/objdiff-gui/src/views/symbol_diff.rs +++ b/objdiff-gui/src/views/symbol_diff.rs @@ -280,12 +280,10 @@ impl DiffViewState { let Ok(state) = state.read() else { return; }; - if let (Some(project_dir), Some(source_path)) = ( - &state.config.project_dir, - state.config.selected_obj.as_ref().and_then(|obj| obj.source_path.as_ref()), - ) { - let source_path = project_dir.join(source_path); - log::info!("Opening file {}", source_path.display()); + if let Some(source_path) = + state.config.selected_obj.as_ref().and_then(|obj| obj.source_path.as_ref()) + { + log::info!("Opening file {}", source_path); open::that_detached(source_path).unwrap_or_else(|err| { log::error!("Failed to open source file: {err}"); }); diff --git a/objdiff-wasm/package.json b/objdiff-wasm/package.json index d6a6f27..3d466cf 100644 --- a/objdiff-wasm/package.json +++ b/objdiff-wasm/package.json @@ -1,6 +1,6 @@ { "name": "objdiff-wasm", - "version": "2.6.0", + "version": "2.7.1", "description": "A local diffing tool for decompilation projects.", "author": { "name": "Luke Street", diff --git a/objdiff-wasm/src/display.ts b/objdiff-wasm/src/display.ts new file mode 100644 index 0000000..b93051a --- /dev/null +++ b/objdiff-wasm/src/display.ts @@ -0,0 +1,107 @@ +import {ArgumentValue, InstructionDiff, RelocationTarget} from "../gen/diff_pb"; + +export type DiffText = + DiffTextBasic + | DiffTextBasicColor + | DiffTextAddress + | DiffTextLine + | DiffTextOpcode + | DiffTextArgument + | DiffTextSymbol + | DiffTextBranchDest + | DiffTextSpacing; + +type DiffTextBase = { + diff_index?: number, +}; +export type DiffTextBasic = DiffTextBase & { + type: 'basic', + text: string, +}; +export type DiffTextBasicColor = DiffTextBase & { + type: 'basic_color', + text: string, + index: number, +}; +export type DiffTextAddress = DiffTextBase & { + type: 'address', + address: bigint, +}; +export type DiffTextLine = DiffTextBase & { + type: 'line', + line_number: number, +}; +export type DiffTextOpcode = DiffTextBase & { + type: 'opcode', + mnemonic: string, + opcode: number, +}; +export type DiffTextArgument = DiffTextBase & { + type: 'argument', + value: ArgumentValue, +}; +export type DiffTextSymbol = DiffTextBase & { + type: 'symbol', + target: RelocationTarget, +}; +export type DiffTextBranchDest = DiffTextBase & { + type: 'branch_dest', + address: bigint, +}; +export type DiffTextSpacing = DiffTextBase & { + type: 'spacing', + count: number, +}; + +// Native JavaScript implementation of objdiff_core::diff::display::display_diff +export function displayDiff(diff: InstructionDiff, baseAddr: bigint, cb: (text: DiffText) => void) { + const ins = diff.instruction; + if (!ins) { + return; + } + if (ins.line_number != null) { + cb({type: 'line', line_number: ins.line_number}); + } + cb({type: 'address', address: ins.address - baseAddr}); + if (diff.branch_from) { + cb({type: 'basic_color', text: ' ~> ', index: diff.branch_from.branch_index}); + } else { + cb({type: 'spacing', count: 4}); + } + cb({type: 'opcode', mnemonic: ins.mnemonic, opcode: ins.opcode}); + let arg_diff_idx = 0; // non-PlainText argument index + for (let i = 0; i < ins.arguments.length; i++) { + if (i === 0) { + cb({type: 'spacing', count: 1}); + } + const arg = ins.arguments[i].value; + let diff_index: number | undefined; + if (arg.oneofKind !== 'plain_text') { + diff_index = diff.arg_diff[arg_diff_idx]?.diff_index; + arg_diff_idx++; + } + switch (arg.oneofKind) { + case "plain_text": + cb({type: 'basic', text: arg.plain_text, diff_index}); + break; + case "argument": + cb({type: 'argument', value: arg.argument, diff_index}); + break; + case "relocation": { + const reloc = ins.relocation!; + cb({type: 'symbol', target: reloc.target!, diff_index}); + break; + } + case "branch_dest": + if (arg.branch_dest < baseAddr) { + cb({type: 'basic', text: '', diff_index}); + } else { + cb({type: 'branch_dest', address: arg.branch_dest - baseAddr, diff_index}); + } + break; + } + } + if (diff.branch_to) { + cb({type: 'basic_color', text: ' ~> ', index: diff.branch_to.branch_index}); + } +} diff --git a/objdiff-wasm/src/main.ts b/objdiff-wasm/src/main.ts index 1704778..f7cd3bb 100644 --- a/objdiff-wasm/src/main.ts +++ b/objdiff-wasm/src/main.ts @@ -1,20 +1,20 @@ -import {ArgumentValue, DiffResult, InstructionDiff, RelocationTarget} from "../gen/diff_pb"; +import {DiffResult} from "../gen/diff_pb"; import type { - ArmArchVersion, - ArmR9Usage, - DiffObjConfig, - MipsAbi, - MipsInstrCategory, - X86Formatter + ConfigProperty, + MappingConfig, + SymbolMappings, } from '../pkg'; import {AnyHandlerData, InMessage, OutMessage} from './worker'; // Export wasm types -export {ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter, DiffObjConfig}; +export {ConfigProperty, MappingConfig, SymbolMappings}; // Export protobuf types export * from '../gen/diff_pb'; +// Export display types +export * from './display'; + interface PromiseCallbacks { start: number; resolve: (value: T | PromiseLike) => void; @@ -111,12 +111,18 @@ async function defer(message: AnyHandlerData, worker?: Worker): Promise { return promise; } -export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, diff_config?: DiffObjConfig): Promise { +export async function runDiff( + left: Uint8Array | null | undefined, + right: Uint8Array | null | undefined, + properties?: ConfigProperty[], + mappingConfig?: MappingConfig, +): Promise { const data = await defer({ type: 'run_diff_proto', left, right, - diff_config + properties, + mappingConfig, }); const parseStart = performance.now(); const result = DiffResult.fromBinary(data, {readUnknownField: false}); @@ -124,109 +130,3 @@ export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | console.debug(`Parsing message took ${end - parseStart}ms`); return result; } - -export type DiffText = - DiffTextBasic - | DiffTextBasicColor - | DiffTextAddress - | DiffTextLine - | DiffTextOpcode - | DiffTextArgument - | DiffTextSymbol - | DiffTextBranchDest - | DiffTextSpacing; - -type DiffTextBase = { - diff_index?: number, -}; -export type DiffTextBasic = DiffTextBase & { - type: 'basic', - text: string, -}; -export type DiffTextBasicColor = DiffTextBase & { - type: 'basic_color', - text: string, - index: number, -}; -export type DiffTextAddress = DiffTextBase & { - type: 'address', - address: bigint, -}; -export type DiffTextLine = DiffTextBase & { - type: 'line', - line_number: number, -}; -export type DiffTextOpcode = DiffTextBase & { - type: 'opcode', - mnemonic: string, - opcode: number, -}; -export type DiffTextArgument = DiffTextBase & { - type: 'argument', - value: ArgumentValue, -}; -export type DiffTextSymbol = DiffTextBase & { - type: 'symbol', - target: RelocationTarget, -}; -export type DiffTextBranchDest = DiffTextBase & { - type: 'branch_dest', - address: bigint, -}; -export type DiffTextSpacing = DiffTextBase & { - type: 'spacing', - count: number, -}; - -// Native JavaScript implementation of objdiff_core::diff::display::display_diff -export function displayDiff(diff: InstructionDiff, baseAddr: bigint, cb: (text: DiffText) => void) { - const ins = diff.instruction; - if (!ins) { - return; - } - if (ins.line_number != null) { - cb({type: 'line', line_number: ins.line_number}); - } - cb({type: 'address', address: ins.address - baseAddr}); - if (diff.branch_from) { - cb({type: 'basic_color', text: ' ~> ', index: diff.branch_from.branch_index}); - } else { - cb({type: 'spacing', count: 4}); - } - cb({type: 'opcode', mnemonic: ins.mnemonic, opcode: ins.opcode}); - let arg_diff_idx = 0; // non-PlainText argument index - for (let i = 0; i < ins.arguments.length; i++) { - if (i === 0) { - cb({type: 'spacing', count: 1}); - } - const arg = ins.arguments[i].value; - let diff_index: number | undefined; - if (arg.oneofKind !== 'plain_text') { - diff_index = diff.arg_diff[arg_diff_idx]?.diff_index; - arg_diff_idx++; - } - switch (arg.oneofKind) { - case "plain_text": - cb({type: 'basic', text: arg.plain_text, diff_index}); - break; - case "argument": - cb({type: 'argument', value: arg.argument, diff_index}); - break; - case "relocation": { - const reloc = ins.relocation!; - cb({type: 'symbol', target: reloc.target!, diff_index}); - break; - } - case "branch_dest": - if (arg.branch_dest < baseAddr) { - cb({type: 'basic', text: '', diff_index}); - } else { - cb({type: 'branch_dest', address: arg.branch_dest - baseAddr, diff_index}); - } - break; - } - } - if (diff.branch_to) { - cb({type: 'basic_color', text: ' ~> ', index: diff.branch_to.branch_index}); - } -} diff --git a/objdiff-wasm/src/worker.ts b/objdiff-wasm/src/worker.ts index 0ac9374..c4c036b 100644 --- a/objdiff-wasm/src/worker.ts +++ b/objdiff-wasm/src/worker.ts @@ -2,7 +2,6 @@ import wasmInit, * as exports from '../pkg'; const handlers = { init: init, - // run_diff_json: run_diff_json, run_diff_proto: run_diff_proto, } as const; type ExtractData = T extends (arg: infer U) => Promise ? U : never; @@ -29,24 +28,16 @@ async function initIfNeeded() { return wasmReady; } -// async function run_diff_json({left, right, config}: { -// left: Uint8Array | undefined, -// right: Uint8Array | undefined, -// config?: exports.DiffObjConfig, -// }): Promise { -// config = config || exports.default_diff_obj_config(); -// return exports.run_diff_json(left, right, cfg); -// } - -async function run_diff_proto({left, right, diff_config, mapping_config}: { - left: Uint8Array | undefined, - right: Uint8Array | undefined, - diff_config?: exports.DiffObjConfig, - mapping_config?: exports.MappingConfig, +async function run_diff_proto({left, right, properties, mappingConfig}: { + left: Uint8Array | null | undefined, + right: Uint8Array | null | undefined, + properties?: exports.ConfigProperty[], + mappingConfig?: exports.MappingConfig, }): Promise { - diff_config = diff_config || {}; - mapping_config = mapping_config || {}; - return exports.run_diff_proto(left, right, diff_config, mapping_config); + const diffConfig = exports.config_from_properties(properties || []); + const leftObj = left ? exports.parse_object(left, diffConfig) : null; + const rightObj = right ? exports.parse_object(right, diffConfig) : null; + return exports.run_diff(leftObj, rightObj, diffConfig, mappingConfig || {}); } export type AnyHandlerData = HandlerData[keyof HandlerData]; diff --git a/objdiff-wasm/tsup.config.ts b/objdiff-wasm/tsup.config.ts index f2a5246..7c89361 100644 --- a/objdiff-wasm/tsup.config.ts +++ b/objdiff-wasm/tsup.config.ts @@ -28,6 +28,7 @@ export default defineConfig([ // https://github.com/egoist/tsup/issues/278 async onSuccess() { await fs.copyFile('pkg/objdiff_core_bg.wasm', 'dist/objdiff_core_bg.wasm'); + await fs.copyFile('../objdiff-core/config-schema.json', 'dist/config-schema.json'); } } ]);