Make objdiff-core no_std + huge WASM rework

This commit is contained in:
Luke Street 2025-02-07 00:10:49 -07:00
parent d938988d43
commit e8de35b78e
49 changed files with 1463 additions and 1046 deletions

320
Cargo.lock generated
View File

@ -152,11 +152,11 @@ dependencies = [
[[package]] [[package]]
name = "arm-attr" name = "arm-attr"
version = "0.1.1" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d0cabd3a7d2dfa96ab3faa7b532a83c5e090061bf6d83197ca2bc91f5afac6c" checksum = "5790583a9ebcc63c55f142a85d85fe63ec0e033c941fcf00a3605f81dada2e32"
dependencies = [ dependencies = [
"thiserror 1.0.69", "thiserror 2.0.11",
] ]
[[package]] [[package]]
@ -395,15 +395,6 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bimap"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.8.0" version = "0.8.0"
@ -688,16 +679,6 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "const_format" name = "const_format"
version = "0.2.34" version = "0.2.34"
@ -862,11 +843,10 @@ checksum = "c2e06f9bce634a3c898eb1e5cb949ff63133cbb218af93cc9b38b31d6f3ea285"
[[package]] [[package]]
name = "cwextab" name = "cwextab"
version = "1.0.3" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/encounter/cwextab.git#15c344ac3302c32adbb8777c70f5ce739f432a6b"
checksum = "003567b96ff9d8ac3275831650385891bca370092937be625157778b1e58f755"
dependencies = [ dependencies = [
"thiserror 1.0.69", "thiserror 2.0.11",
] ]
[[package]] [[package]]
@ -1336,12 +1316,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]] [[package]]
name = "fastrand" name = "fastrand"
version = "2.3.0" version = "2.3.0"
@ -1632,10 +1606,6 @@ name = "gimli"
version = "0.31.1" version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
dependencies = [
"fallible-iterator",
"stable_deref_trait",
]
[[package]] [[package]]
name = "gl_generator" name = "gl_generator"
@ -2112,6 +2082,12 @@ dependencies = [
"syn 2.0.96", "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]] [[package]]
name = "ident_case" name = "ident_case"
version = "1.0.1" version = "1.0.1"
@ -2159,6 +2135,7 @@ checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
"serde",
] ]
[[package]] [[package]]
@ -2342,6 +2319,15 @@ name = "lazy_static"
version = "1.5.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]]
name = "leb128"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67"
[[package]] [[package]]
name = "libc" name = "libc"
@ -2539,11 +2525,12 @@ dependencies = [
[[package]] [[package]]
name = "msvc-demangler" name = "msvc-demangler"
version = "0.10.1" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4c25a3bb7d880e8eceab4822f3141ad0700d20f025991c1f03bd3d00219a5fc" checksum = "fbeff6bd154a309b2ada5639b2661ca6ae4599b34e8487dc276d2cd637da2d76"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"itoa",
] ]
[[package]] [[package]]
@ -3000,6 +2987,7 @@ dependencies = [
"time", "time",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"typed-path",
] ]
[[package]] [[package]]
@ -3008,10 +2996,7 @@ version = "2.7.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arm-attr", "arm-attr",
"bimap",
"byteorder", "byteorder",
"console_error_panic_hook",
"console_log",
"cpp_demangle", "cpp_demangle",
"cwdemangle", "cwdemangle",
"cwextab", "cwextab",
@ -3043,17 +3028,18 @@ dependencies = [
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml",
"shell-escape", "shell-escape",
"similar", "similar",
"spin",
"strum", "strum",
"syn 2.0.96", "syn 2.0.96",
"talc",
"tempfile", "tempfile",
"time", "time",
"tsify-next", "typed-path",
"unarm", "unarm",
"wasm-bindgen",
"winapi", "winapi",
"wit-bindgen",
"yaxpeax-arch", "yaxpeax-arch",
"yaxpeax-arm", "yaxpeax-arm",
] ]
@ -3095,6 +3081,7 @@ dependencies = [
"time", "time",
"tracing-subscriber", "tracing-subscriber",
"tracing-wasm", "tracing-wasm",
"typed-path",
"wgpu", "wgpu",
"winapi", "winapi",
] ]
@ -4097,17 +4084,6 @@ dependencies = [
"serde_derive", "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]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.217" version = "1.0.217"
@ -4119,17 +4095,6 @@ dependencies = [
"syn 2.0.96", "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]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.135" version = "1.0.135"
@ -4174,19 +4139,6 @@ dependencies = [
"serde", "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]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.7" version = "0.1.7"
@ -4246,9 +4198,11 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]] [[package]]
name = "similar" name = "similar"
version = "2.6.0" version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/encounter/similar.git?branch=no_std#20f3537abfefa4cfb8c4b981c8aab99c7b53130d"
checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" dependencies = [
"hashbrown",
]
[[package]] [[package]]
name = "slab" name = "slab"
@ -4329,11 +4283,23 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]] [[package]]
name = "spirv" name = "spirv"
@ -4441,6 +4407,15 @@ dependencies = [
"syn 2.0.96", "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]] [[package]]
name = "tap" name = "tap"
version = "1.0.1" version = "1.0.1"
@ -4786,30 +4761,6 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 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]] [[package]]
name = "ttf-parser" name = "ttf-parser"
version = "0.25.1" version = "0.25.1"
@ -4825,6 +4776,12 @@ dependencies = [
"rustc-hash 1.1.0", "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]] [[package]]
name = "uds_windows" name = "uds_windows"
version = "1.1.0" version = "1.1.0"
@ -4838,9 +4795,9 @@ dependencies = [
[[package]] [[package]]
name = "unarm" name = "unarm"
version = "1.6.7" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a34c5159ddf1e715c3144fd31571bd2d9b63ac70ce1df51d7145ade4532d0c78" checksum = "dff0b9c752e29548c4bf614faa49a5f2d222c6333f9f70e3eb5cd84387f6a333"
[[package]] [[package]]
name = "unicase" name = "unicase"
@ -4889,12 +4846,6 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
@ -5065,6 +5016,45 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "wayland-backend" name = "wayland-backend"
version = "0.3.7" version = "0.3.7"
@ -5750,6 +5740,104 @@ dependencies = [
"winapi", "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]] [[package]]
name = "write16" name = "write16"
version = "1.0.0" version = "1.0.0"
@ -5855,9 +5943,9 @@ dependencies = [
[[package]] [[package]]
name = "yaxpeax-arm" name = "yaxpeax-arm"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1c6a2af41f88546a08df3bc77aadf7263884d6dffdac5b32dea7dc2df23f241" checksum = "1db82aac85bc577d19b6255bf54ad97241c436eeb997ba159f399adacc5fb69e"
dependencies = [ dependencies = [
"bitvec", "bitvec",
"yaxpeax-arch", "yaxpeax-arch",

View File

@ -28,6 +28,7 @@ supports-color = "3.0"
time = { version = "0.3", features = ["formatting", "local-offset"] } time = { version = "0.3", features = ["formatting", "local-offset"] }
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
typed-path = "0.10"
[target.'cfg(target_env = "musl")'.dependencies] [target.'cfg(target_env = "musl")'.dependencies]
mimalloc = "0.1" mimalloc = "0.1"

View File

@ -1,8 +1,6 @@
use std::{ use std::{
fs,
io::stdout, io::stdout,
mem, mem,
path::{Path, PathBuf},
str::FromStr, str::FromStr,
sync::{ sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
@ -27,7 +25,11 @@ use objdiff_core::{
watcher::{create_watcher, Watcher}, watcher::{create_watcher, Watcher},
BuildConfig, BuildConfig,
}, },
config::{build_globset, ProjectConfig, ProjectObject}, config::{
build_globset,
path::{check_path_buf, platform_path, platform_path_serde_option},
ProjectConfig, ProjectObject, ProjectObjectMetadata,
},
diff, diff,
diff::{ diff::{
ConfigEnum, ConfigPropertyId, ConfigPropertyKind, DiffObjConfig, MappingConfig, ObjDiff, ConfigEnum, ConfigPropertyId, ConfigPropertyKind, DiffObjConfig, MappingConfig, ObjDiff,
@ -40,6 +42,7 @@ use objdiff_core::{
obj::ObjInfo, obj::ObjInfo,
}; };
use ratatui::prelude::*; use ratatui::prelude::*;
use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
use crate::{ use crate::{
util::{ util::{
@ -53,21 +56,21 @@ use crate::{
/// Diff two object files. (Interactive or one-shot mode) /// Diff two object files. (Interactive or one-shot mode)
#[argp(subcommand, name = "diff")] #[argp(subcommand, name = "diff")]
pub struct Args { pub struct Args {
#[argp(option, short = '1')] #[argp(option, short = '1', from_str_fn(platform_path))]
/// Target object file /// Target object file
target: Option<PathBuf>, target: Option<Utf8PlatformPathBuf>,
#[argp(option, short = '2')] #[argp(option, short = '2', from_str_fn(platform_path))]
/// Base object file /// Base object file
base: Option<PathBuf>, base: Option<Utf8PlatformPathBuf>,
#[argp(option, short = 'p')] #[argp(option, short = 'p', from_str_fn(platform_path))]
/// Project directory /// Project directory
project: Option<PathBuf>, project: Option<Utf8PlatformPathBuf>,
#[argp(option, short = 'u')] #[argp(option, short = 'u')]
/// Unit name within project /// Unit name within project
unit: Option<String>, unit: Option<String>,
#[argp(option, short = 'o')] #[argp(option, short = 'o', from_str_fn(platform_path))]
/// Output file (one-shot mode) ("-" for stdout) /// Output file (one-shot mode) ("-" for stdout)
output: Option<PathBuf>, output: Option<Utf8PlatformPathBuf>,
#[argp(option)] #[argp(option)]
/// Output format (json, json-pretty, proto) (default: json) /// Output format (json, json-pretty, proto) (default: json)
format: Option<String>, format: Option<String>,
@ -89,86 +92,61 @@ pub struct Args {
} }
pub fn run(args: Args) -> Result<()> { pub fn run(args: Args) -> Result<()> {
let (target_path, base_path, project_config) = match ( let (target_path, base_path, project_config) =
&args.target, match (&args.target, &args.base, &args.project, &args.unit) {
&args.base,
&args.project,
&args.unit,
) {
(Some(_), Some(_), None, None) (Some(_), Some(_), None, None)
| (Some(_), None, None, None) | (Some(_), None, None, None)
| (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None), | (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None),
(None, None, p, u) => { (None, None, p, u) => {
let project = match p { let project = match p {
Some(project) => project.clone(), Some(project) => project.clone(),
_ => std::env::current_dir().context("Failed to get the current directory")?, _ => check_path_buf(
std::env::current_dir().context("Failed to get the current directory")?,
)
.context("Current directory is not valid UTF-8")?,
}; };
let Some((project_config, project_config_info)) = let Some((project_config, project_config_info)) =
objdiff_core::config::try_project_config(&project) objdiff_core::config::try_project_config(project.as_ref())
else { else {
bail!("Project config not found in {}", &project.display()) bail!("Project config not found in {}", &project)
}; };
let mut project_config = project_config.with_context(|| { let project_config = project_config.with_context(|| {
format!("Reading project config {}", project_config_info.path.display()) format!("Reading project config {}", project_config_info.path.display())
})?; })?;
let object = { let target_obj_dir = project_config
let resolve_paths = |o: &mut ProjectObject| { .target_dir
o.resolve_paths( .as_ref()
&project, .map(|p| project.join(p.with_platform_encoding()));
project_config.target_dir.as_deref(), let base_obj_dir = project_config
project_config.base_dir.as_deref(), .base_dir
) .as_ref()
}; .map(|p| project.join(p.with_platform_encoding()));
if let Some(u) = u { let objects = project_config
let unit_path =
PathBuf::from_str(u).ok().and_then(|p| fs::canonicalize(p).ok());
let Some(object) = project_config
.units .units
.as_deref_mut() .iter()
.unwrap_or_default() .flatten()
.iter_mut() .map(|o| {
.find_map(|obj| { ObjectConfig::new(
if obj.name.as_deref() == Some(u) { o,
resolve_paths(obj); &project,
return Some(obj); target_obj_dir.as_deref(),
} base_obj_dir.as_deref(),
)
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 { .collect::<Vec<_>>();
bail!("Unit not found: {}", u) let object = if let Some(u) = u {
}; objects
.iter()
object .find(|obj| obj.name == *u)
.ok_or_else(|| anyhow!("Unit not found: {}", u))?
} else if let Some(symbol_name) = &args.symbol { } else if let Some(symbol_name) = &args.symbol {
let mut idx = None; let mut idx = None;
let mut count = 0usize; let mut count = 0usize;
for (i, obj) in project_config for (i, obj) in objects.iter().enumerate() {
.units
.as_deref_mut()
.unwrap_or_default()
.iter_mut()
.enumerate()
{
resolve_paths(obj);
if obj if obj
.target_path .target_path
.as_deref() .as_deref()
.map(|o| obj::read::has_function(o, symbol_name)) .map(|o| obj::read::has_function(o.as_ref(), symbol_name))
.transpose()? .transpose()?
.unwrap_or(false) .unwrap_or(false)
{ {
@ -181,7 +159,7 @@ pub fn run(args: Args) -> Result<()> {
} }
match (count, idx) { match (count, idx) {
(0, None) => bail!("Symbol not found: {}", symbol_name), (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!( (2.., Some(_)) => bail!(
"Multiple instances of {} were found, try specifying a unit", "Multiple instances of {} were found, try specifying a unit",
symbol_name symbol_name
@ -190,7 +168,6 @@ pub fn run(args: Args) -> Result<()> {
} }
} else { } else {
bail!("Must specify one of: symbol, project and unit, target and base objects") bail!("Must specify one of: symbol, project and unit, target and base objects")
}
}; };
let target_path = object.target_path.clone(); let target_path = object.target_path.clone();
let base_path = object.base_path.clone(); let base_path = object.base_path.clone();
@ -245,20 +222,20 @@ fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)>
fn run_oneshot( fn run_oneshot(
args: &Args, args: &Args,
output: &Path, output: &Utf8PlatformPath,
target_path: Option<&Path>, target_path: Option<&Utf8PlatformPath>,
base_path: Option<&Path>, base_path: Option<&Utf8PlatformPath>,
) -> Result<()> { ) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?; let output_format = OutputFormat::from_option(args.format.as_deref())?;
let (diff_config, mapping_config) = build_config_from_args(args)?; let (diff_config, mapping_config) = build_config_from_args(args)?;
let target = target_path let target = target_path
.map(|p| { .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()?; .transpose()?;
let base = base_path let base = base_path
.map(|p| { .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()?; .transpose()?;
let result = let result =
@ -272,10 +249,10 @@ fn run_oneshot(
pub struct AppState { pub struct AppState {
pub jobs: JobQueue, pub jobs: JobQueue,
pub waker: Arc<TermWaker>, pub waker: Arc<TermWaker>,
pub project_dir: Option<PathBuf>, pub project_dir: Option<Utf8PlatformPathBuf>,
pub project_config: Option<ProjectConfig>, pub project_config: Option<ProjectConfig>,
pub target_path: Option<PathBuf>, pub target_path: Option<Utf8PlatformPathBuf>,
pub base_path: Option<PathBuf>, pub base_path: Option<Utf8PlatformPathBuf>,
pub left_obj: Option<(ObjInfo, ObjDiff)>, pub left_obj: Option<(ObjInfo, ObjDiff)>,
pub right_obj: Option<(ObjInfo, ObjDiff)>, pub right_obj: Option<(ObjInfo, ObjDiff)>,
pub prev_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<Utf8PlatformPathBuf>,
#[serde(default, with = "platform_path_serde_option")]
pub base_path: Option<Utf8PlatformPathBuf>,
pub metadata: ProjectObjectMetadata,
pub complete: Option<bool>,
}
impl ObjectConfig {
pub fn new(
object: &ProjectObject,
project_dir: &Utf8PlatformPath,
target_obj_dir: Option<&Utf8PlatformPath>,
base_obj_dir: Option<&Utf8PlatformPath>,
) -> Self {
let target_path = if let (Some(target_obj_dir), Some(path), None) =
(target_obj_dir, &object.path, &object.target_path)
{
Some(target_obj_dir.join(path.with_platform_encoding()))
} else 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 { impl AppState {
fn reload(&mut self) -> Result<()> { fn reload(&mut self) -> Result<()> {
let config = create_objdiff_config(self); let config = create_objdiff_config(self);
@ -355,8 +379,8 @@ impl Wake for TermWaker {
fn run_interactive( fn run_interactive(
args: Args, args: Args,
target_path: Option<PathBuf>, target_path: Option<Utf8PlatformPathBuf>,
base_path: Option<PathBuf>, base_path: Option<Utf8PlatformPathBuf>,
project_config: Option<ProjectConfig>, project_config: Option<ProjectConfig>,
) -> Result<()> { ) -> Result<()> {
let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") }; 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()?; let watch_patterns = project_config.build_watch_patterns()?;
state.watcher = Some(create_watcher( state.watcher = Some(create_watcher(
state.modified.clone(), state.modified.clone(),
project_dir, project_dir.as_ref(),
build_globset(&watch_patterns)?, build_globset(&watch_patterns)?,
Waker::from(state.waker.clone()), Waker::from(state.waker.clone()),
)?); )?);

View File

@ -1,10 +1,4 @@
use std::{ use std::{collections::HashSet, fs::File, io::Read, time::Instant};
collections::HashSet,
fs::File,
io::Read,
path::{Path, PathBuf},
time::Instant,
};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use argp::FromArgs; use argp::FromArgs;
@ -14,15 +8,19 @@ use objdiff_core::{
ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata, ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
REPORT_VERSION, REPORT_VERSION,
}, },
config::ProjectObject, config::path::platform_path,
diff, obj, diff, obj,
obj::{ObjSectionKind, ObjSymbolFlags}, obj::{ObjSectionKind, ObjSymbolFlags},
}; };
use prost::Message; use prost::Message;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use tracing::{info, warn}; 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)] #[derive(FromArgs, PartialEq, Debug)]
/// Generate a progress report for a project. /// Generate a progress report for a project.
@ -43,12 +41,12 @@ pub enum SubCommand {
/// Generate a progress report for a project. /// Generate a progress report for a project.
#[argp(subcommand, name = "generate")] #[argp(subcommand, name = "generate")]
pub struct GenerateArgs { pub struct GenerateArgs {
#[argp(option, short = 'p')] #[argp(option, short = 'p', from_str_fn(platform_path))]
/// Project directory /// Project directory
project: Option<PathBuf>, project: Option<Utf8PlatformPathBuf>,
#[argp(option, short = 'o')] #[argp(option, short = 'o', from_str_fn(platform_path))]
/// Output file /// Output file
output: Option<PathBuf>, output: Option<Utf8PlatformPathBuf>,
#[argp(switch, short = 'd')] #[argp(switch, short = 'd')]
/// Deduplicate global and weak symbols (runs single-threaded) /// Deduplicate global and weak symbols (runs single-threaded)
deduplicate: bool, deduplicate: bool,
@ -61,15 +59,15 @@ pub struct GenerateArgs {
/// List any changes from a previous report. /// List any changes from a previous report.
#[argp(subcommand, name = "changes")] #[argp(subcommand, name = "changes")]
pub struct ChangesArgs { pub struct ChangesArgs {
#[argp(positional)] #[argp(positional, from_str_fn(platform_path))]
/// Previous report file /// Previous report file
previous: PathBuf, previous: Utf8PlatformPathBuf,
#[argp(positional)] #[argp(positional, from_str_fn(platform_path))]
/// Current report file /// Current report file
current: PathBuf, current: Utf8PlatformPathBuf,
#[argp(option, short = 'o')] #[argp(option, short = 'o', from_str_fn(platform_path))]
/// Output file /// Output file
output: Option<PathBuf>, output: Option<Utf8PlatformPathBuf>,
#[argp(option, short = 'f')] #[argp(option, short = 'f')]
/// Output format (json, json-pretty, proto) (default: json) /// Output format (json, json-pretty, proto) (default: json)
format: Option<String>, format: Option<String>,
@ -84,10 +82,10 @@ pub fn run(args: Args) -> Result<()> {
fn generate(args: GenerateArgs) -> Result<()> { fn generate(args: GenerateArgs) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?; let output_format = OutputFormat::from_option(args.format.as_deref())?;
let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new(".")); let project_dir = args.project.as_deref().unwrap_or_else(|| Utf8PlatformPath::new("."));
info!("Loading project {}", project_dir.display()); 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((Ok(config), _)) => config,
Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err), Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err),
None => bail!("No project configuration found"), None => bail!("No project configuration found"),
@ -98,37 +96,33 @@ fn generate(args: GenerateArgs) -> Result<()> {
if args.deduplicate { 1 } else { rayon::current_num_threads() } if args.deduplicate { 1 } else { rayon::current_num_threads() }
); );
let target_obj_dir =
project.target_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
let base_obj_dir =
project.base_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding()));
let objects = project
.units
.iter()
.flatten()
.map(|o| {
ObjectConfig::new(o, project_dir, target_obj_dir.as_deref(), base_obj_dir.as_deref())
})
.collect::<Vec<_>>();
let start = Instant::now(); let start = Instant::now();
let mut units = vec![]; let mut units = vec![];
let mut existing_functions: HashSet<String> = HashSet::new(); let mut existing_functions: HashSet<String> = HashSet::new();
if args.deduplicate { if args.deduplicate {
// If deduplicating, we need to run single-threaded // If deduplicating, we need to run single-threaded
for object in project.units.as_deref_mut().unwrap_or_default() { for object in &objects {
if let Some(unit) = report_object( if let Some(unit) = report_object(object, Some(&mut existing_functions))? {
object,
project_dir,
project.target_dir.as_deref(),
project.base_dir.as_deref(),
Some(&mut existing_functions),
)? {
units.push(unit); units.push(unit);
} }
} }
} else { } else {
let vec = project let vec = objects
.units .par_iter()
.as_deref_mut() .map(|object| report_object(object, None))
.unwrap_or_default()
.par_iter_mut()
.map(|object| {
report_object(
object,
project_dir,
project.target_dir.as_deref(),
project.base_dir.as_deref(),
None,
)
})
.collect::<Result<Vec<Option<ReportUnit>>>>()?; .collect::<Result<Vec<Option<ReportUnit>>>>()?;
units = vec.into_iter().flatten().collect(); units = vec.into_iter().flatten().collect();
} }
@ -151,20 +145,16 @@ fn generate(args: GenerateArgs) -> Result<()> {
} }
fn report_object( fn report_object(
object: &mut ProjectObject, object: &ObjectConfig,
project_dir: &Path,
target_dir: Option<&Path>,
base_dir: Option<&Path>,
mut existing_functions: Option<&mut HashSet<String>>, mut existing_functions: Option<&mut HashSet<String>>,
) -> Result<Option<ReportUnit>> { ) -> Result<Option<ReportUnit>> {
object.resolve_paths(project_dir, target_dir, base_dir);
match (&object.target_path, &object.base_path) { match (&object.target_path, &object.base_path) {
(None, Some(_)) if !object.complete().unwrap_or(false) => { (None, Some(_)) if !object.complete.unwrap_or(false) => {
warn!("Skipping object without target: {}", object.name()); warn!("Skipping object without target: {}", object.name);
return Ok(None); return Ok(None);
} }
(None, None) => { (None, None) => {
warn!("Skipping object without target or base: {}", object.name()); warn!("Skipping object without target or base: {}", object.name);
return Ok(None); return Ok(None);
} }
_ => {} _ => {}
@ -178,35 +168,31 @@ fn report_object(
.target_path .target_path
.as_ref() .as_ref()
.map(|p| { .map(|p| {
obj::read::read(p, &diff_config) obj::read::read(p.as_ref(), &diff_config)
.with_context(|| format!("Failed to open {}", p.display())) .with_context(|| format!("Failed to open {}", p))
}) })
.transpose()?; .transpose()?;
let base = object let base = object
.base_path .base_path
.as_ref() .as_ref()
.map(|p| { .map(|p| {
obj::read::read(p, &diff_config) obj::read::read(p.as_ref(), &diff_config)
.with_context(|| format!("Failed to open {}", p.display())) .with_context(|| format!("Failed to open {}", p))
}) })
.transpose()?; .transpose()?;
let result = let result =
diff::diff_objs(&diff_config, &mapping_config, target.as_ref(), base.as_ref(), None)?; diff::diff_objs(&diff_config, &mapping_config, target.as_ref(), base.as_ref(), None)?;
let metadata = ReportUnitMetadata { let metadata = ReportUnitMetadata {
complete: object.complete(), complete: object.metadata.complete,
module_name: target module_name: target
.as_ref() .as_ref()
.and_then(|o| o.split_meta.as_ref()) .and_then(|o| o.split_meta.as_ref())
.and_then(|m| m.module_name.clone()), .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), 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()), source_path: object.metadata.source_path.as_ref().map(|p| p.to_string()),
progress_categories: object progress_categories: object.metadata.progress_categories.clone().unwrap_or_default(),
.metadata auto_generated: object.metadata.auto_generated,
.as_ref()
.and_then(|m| m.progress_categories.clone())
.unwrap_or_default(),
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
}; };
let mut measures = Measures { total_units: 1, ..Default::default() }; let mut measures = Measures { total_units: 1, ..Default::default() };
let mut sections = vec![]; let mut sections = vec![];
@ -218,7 +204,7 @@ fn report_object(
let section_match_percent = section_diff.match_percent.unwrap_or_else(|| { let section_match_percent = section_diff.match_percent.unwrap_or_else(|| {
// Support cases where we don't have a target object, // Support cases where we don't have a target object,
// assume complete means 100% match // assume complete means 100% match
if object.complete().unwrap_or(false) { if object.complete.unwrap_or(false) {
100.0 100.0
} else { } else {
0.0 0.0
@ -260,7 +246,7 @@ fn report_object(
let match_percent = symbol_diff.match_percent.unwrap_or_else(|| { let match_percent = symbol_diff.match_percent.unwrap_or_else(|| {
// Support cases where we don't have a target object, // Support cases where we don't have a target object,
// assume complete means 100% match // assume complete means 100% match
if object.complete().unwrap_or(false) { if object.complete.unwrap_or(false) {
100.0 100.0
} else { } else {
0.0 0.0
@ -294,7 +280,7 @@ fn report_object(
measures.calc_fuzzy_match_percent(); measures.calc_fuzzy_match_percent();
measures.calc_matched_percent(); measures.calc_matched_percent();
Ok(Some(ReportUnit { Ok(Some(ReportUnit {
name: object.name().to_string(), name: object.name.clone(),
measures: Some(measures), measures: Some(measures),
sections, sections,
functions, functions,
@ -304,7 +290,7 @@ fn report_object(
fn changes(args: ChangesArgs) -> Result<()> { fn changes(args: ChangesArgs) -> Result<()> {
let output_format = OutputFormat::from_option(args.format.as_deref())?; 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 // Special case for comparing two reports from stdin
let mut data = vec![]; let mut data = vec![];
std::io::stdin().read_to_end(&mut data)?; std::io::stdin().read_to_end(&mut data)?;
@ -419,15 +405,14 @@ fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
.collect() .collect()
} }
fn read_report(path: &Path) -> Result<Report> { fn read_report(path: &Utf8PlatformPath) -> Result<Report> {
if path == Path::new("-") { if path == Utf8PlatformPath::new("-") {
let mut data = vec![]; let mut data = vec![];
std::io::stdin().read_to_end(&mut data)?; std::io::stdin().read_to_end(&mut data)?;
return Report::parse(&data).with_context(|| "Failed to load report from stdin"); 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 file = File::open(path).with_context(|| format!("Failed to open {}", path))?;
let mmap = unsafe { memmap2::Mmap::map(&file) } let mmap =
.with_context(|| format!("Failed to map {}", path.display()))?; unsafe { memmap2::Mmap::map(&file) }.with_context(|| format!("Failed to map {}", path))?;
Report::parse(mmap.as_ref()) Report::parse(mmap.as_ref()).with_context(|| format!("Failed to load report {}", path))
.with_context(|| format!("Failed to load report {}", path.display()))
} }

View File

@ -34,9 +34,12 @@ impl OutputFormat {
} }
} }
pub fn write_output<T>(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()> pub fn write_output<T, P>(input: &T, output: Option<P>, format: OutputFormat) -> Result<()>
where T: serde::Serialize + prost::Message { where
match output { T: serde::Serialize + prost::Message,
P: AsRef<Path>,
{
match output.as_ref().map(|p| p.as_ref()) {
Some(output) if output != Path::new("-") => { Some(output) if output != Path::new("-") => {
info!("Writing to {}", output.display()); info!("Writing to {}", output.display());
let file = File::options() let file = File::options()

View File

@ -16,12 +16,14 @@ documentation = "https://docs.rs/objdiff-core"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features] [features]
default = ["std"]
all = [ all = [
# Features # Features
"bindings", "bindings",
"build", "build",
"config", "config",
"dwarf", "dwarf",
"serde",
# Architectures # Architectures
"mips", "mips",
"ppc", "ppc",
@ -31,30 +33,21 @@ all = [
] ]
# Implicit, used to check if any arch is enabled # Implicit, used to check if any arch is enabled
any-arch = [ any-arch = [
"config",
"dep:bimap",
"dep:byteorder", "dep:byteorder",
"dep:flagset", "dep:flagset",
"dep:heck", "dep:heck",
"dep:log", "dep:log",
"dep:memmap2",
"dep:num-traits", "dep:num-traits",
"dep:prettyplease", "dep:prettyplease",
"dep:proc-macro2", "dep:proc-macro2",
"dep:quote", "dep:quote",
"dep:serde",
"dep:serde_json",
"dep:similar", "dep:similar",
"dep:strum", "dep:strum",
"dep:syn", "dep:syn",
] ]
bindings = [ bindings = [
"dep:pbjson",
"dep:pbjson-build",
"dep:prost", "dep:prost",
"dep:prost-build", "dep:prost-build",
"dep:serde",
"dep:serde_json",
] ]
build = [ build = [
"dep:notify", "dep:notify",
@ -68,15 +61,31 @@ build = [
"dep:winapi", "dep:winapi",
] ]
config = [ config = [
"dep:bimap",
"dep:filetime",
"dep:globset", "dep:globset",
"dep:semver", "dep:semver",
"dep:serde", "dep:typed-path",
"dep:serde_json",
"dep:serde_yaml",
] ]
dwarf = ["dep:gimli"] 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 = [ mips = [
"any-arch", "any-arch",
"dep:rabbitizer", "dep:rabbitizer",
@ -108,65 +117,66 @@ arm64 = [
wasm = [ wasm = [
"any-arch", "any-arch",
"bindings", "bindings",
"dep:console_error_panic_hook",
"dep:console_log",
"dep:log", "dep:log",
"dep:tsify-next", "dep:talc",
"dep:wasm-bindgen", "dep:spin",
"dep:wit-bindgen",
] ]
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = ["all"] features = ["all"]
[dependencies] [dependencies]
anyhow = "1.0" anyhow = { version = "1.0", default-features = false }
bimap = { version = "0.6", features = ["serde"], optional = true } byteorder = { version = "1.5", default-features = false, optional = true }
byteorder = { version = "1.5", optional = true }
filetime = { version = "0.2", optional = true } filetime = { version = "0.2", optional = true }
flagset = { version = "0.4", optional = true } flagset = { version = "0.4", default-features = false, optional = true }
log = { version = "0.4", optional = true } log = { version = "0.4", default-features = false, optional = true }
memmap2 = { version = "0.9", optional = true } memmap2 = { version = "0.9", optional = true }
num-traits = { version = "0.2", optional = true } num-traits = { version = "0.2", default-features = false, optional = true }
object = { version = "0.36", features = ["read_core", "std", "elf", "pe"], default-features = false } object = { version = "0.36", default-features = false, features = ["read_core", "elf", "pe"] }
pbjson = { version = "0.7", optional = true } pbjson = { version = "0.7", default-features = false, optional = true }
prost = { version = "0.13", optional = true } prost = { version = "0.13", default-features = false, features = ["prost-derive"], optional = true }
serde = { version = "1.0", features = ["derive"], optional = true } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true }
similar = { version = "2.6", default-features = false, 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", features = ["derive"], optional = true } strum = { version = "0.26", default-features = false, features = ["derive"], optional = true }
wasm-bindgen = { version = "0.2", optional = true } typed-path = { version = "0.10", default-features = false, 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 }
# config # config
globset = { version = "0.4", features = ["serde1"], optional = true } globset = { version = "0.4", default-features = false, optional = true }
semver = { version = "1.0", optional = true } semver = { version = "1.0", default-features = false, optional = true }
serde_json = { version = "1.0", optional = true } serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true }
serde_yaml = { version = "0.9", optional = true }
# dwarf # 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 # ppc
cwdemangle = { version = "1.0", optional = true } 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 } ppc750cl = { version = "0.3", optional = true }
# mips # mips
rabbitizer = { version = "1.12", optional = true } rabbitizer = { version = "1.12", optional = true }
# x86 # x86
cpp_demangle = { version = "0.4", optional = true } cpp_demangle = { version = "0.4", default-features = false, features = ["alloc"], optional = true }
iced-x86 = { version = "1.21", default-features = false, features = ["std", "decoder", "intel", "gas", "masm", "nasm", "exhaustive_enums"], 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.10", optional = true } msvc-demangler = { version = "0.11", optional = true }
# arm # arm
unarm = { version = "1.6", optional = true } unarm = { version = "1.7", optional = true }
arm-attr = { version = "0.1", optional = true } arm-attr = { version = "0.2", optional = true }
# arm64 # arm64
yaxpeax-arch = { 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, features = ["std"], 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 # build
notify = { version = "8.0.0", optional = true } notify = { version = "8.0.0", optional = true }
@ -196,6 +206,6 @@ prettyplease = { version = "0.2", optional = true }
proc-macro2 = { version = "1.0", optional = true } proc-macro2 = { version = "1.0", optional = true }
prost-build = { version = "0.13", optional = true } prost-build = { version = "0.13", optional = true }
quote = { version = "1.0", optional = true } quote = { version = "1.0", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true } serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", optional = true } serde_json = { version = "1.0" }
syn = { version = "2.0", optional = true } syn = { version = "2.0", optional = true }

View File

@ -54,6 +54,8 @@ fn compile_protos() {
} }
} }
#[cfg(feature = "serde")]
{
let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set"); let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set");
pbjson_build::Builder::new() pbjson_build::Builder::new()
.register_descriptors(&descriptor_set) .register_descriptors(&descriptor_set)
@ -62,3 +64,4 @@ fn compile_protos() {
.build(&[".objdiff"]) .build(&[".objdiff"])
.expect("Failed to build pbjson"); .expect("Failed to build pbjson");
} }
}

View File

@ -100,7 +100,7 @@ pub fn generate_diff_config() {
} }
let value = &item.value; let value = &item.value;
variants.extend(quote! { variants.extend(quote! {
#[serde(rename = #value, alias = #variant_name)] #[cfg_attr(feature = "serde", serde(rename = #value, alias = #variant_name))]
#variant_ident, #variant_ident,
}); });
full_variants.extend(quote! { #enum_ident::#variant_ident, }); full_variants.extend(quote! { #enum_ident::#variant_ident, });
@ -134,8 +134,8 @@ pub fn generate_diff_config() {
}); });
} }
enums.extend(quote! { enums.extend(quote! {
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, serde::Deserialize, serde::Serialize)] #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum #enum_ident { pub enum #enum_ident {
#variants #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 = (); type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
#variant_from_str #variant_from_str
@ -244,7 +244,7 @@ pub fn generate_diff_config() {
let default = b.default; let default = b.default;
if default { if default {
property_fields.extend(quote! { property_fields.extend(quote! {
#[serde(default = "default_true")] #[cfg_attr(feature = "serde", serde(default = "default_true"))]
}); });
} }
property_fields.extend(quote! { 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 = (); type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
#config_property_id_from_str #config_property_id_from_str
@ -433,6 +433,7 @@ pub fn generate_diff_config() {
Choice(&'static str), Choice(&'static str),
} }
impl ConfigPropertyValue { impl ConfigPropertyValue {
#[cfg(feature = "serde")]
pub fn to_json(&self) -> serde_json::Value { pub fn to_json(&self) -> serde_json::Value {
match self { match self {
ConfigPropertyValue::Boolean(value) => serde_json::Value::Bool(*value), 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)] #[derive(Clone, Debug)]
pub enum ConfigPropertyKind { pub enum ConfigPropertyKind {
Boolean, Boolean,
Choice(&'static [ConfigEnumVariantInfo]), Choice(&'static [ConfigEnumVariantInfo]),
} }
#enums #enums
#[cfg(feature = "serde")]
#[inline(always)] #[inline(always)]
fn default_true() -> bool { true } fn default_true() -> bool { true }
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[derive(Clone, Debug)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize), serde(default))]
#[serde(default)]
pub struct DiffObjConfig { pub struct DiffObjConfig {
#property_fields #property_fields
} }

View File

@ -1,14 +1,18 @@
use std::{ use alloc::{
borrow::Cow, borrow::Cow,
collections::{BTreeMap, HashMap}, collections::BTreeMap,
format,
string::{String, ToString},
vec,
vec::Vec,
}; };
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use arm_attr::{enums::CpuArch, tag::Tag, BuildAttrs}; use arm_attr::{enums::CpuArch, tag::Tag, BuildAttrs};
use object::{ use object::{
elf::{self, SHT_ARM_ATTRIBUTES}, elf::{self, SHT_ARM_ATTRIBUTES},
Endian, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, SectionIndex, Endian, File, Object, ObjectSection, ObjectSymbol, Relocation, RelocationFlags, SectionKind,
SectionKind, Symbol, SymbolKind, Symbol, SymbolKind,
}; };
use unarm::{ use unarm::{
args::{Argument, OffsetImm, OffsetReg, Register}, args::{Argument, OffsetImm, OffsetReg, Register},
@ -24,7 +28,7 @@ use crate::{
pub struct ObjArchArm { pub struct ObjArchArm {
/// Maps section index, to list of disasm modes (arm, thumb or data) sorted by address /// Maps section index, to list of disasm modes (arm, thumb or data) sorted by address
disasm_modes: HashMap<SectionIndex, Vec<DisasmMode>>, disasm_modes: BTreeMap<usize, Vec<DisasmMode>>,
detected_version: Option<ArmVersion>, detected_version: Option<ArmVersion>,
endianness: object::Endianness, endianness: object::Endianness,
} }
@ -78,7 +82,7 @@ impl ObjArchArm {
Ok(None) Ok(None)
} }
fn elf_get_mapping_symbols(file: &File) -> HashMap<SectionIndex, Vec<DisasmMode>> { fn elf_get_mapping_symbols(file: &File) -> BTreeMap<usize, Vec<DisasmMode>> {
file.sections() file.sections()
.filter(|s| s.kind() == SectionKind::Text) .filter(|s| s.kind() == SectionKind::Text)
.map(|s| { .map(|s| {
@ -89,7 +93,7 @@ impl ObjArchArm {
.filter_map(|s| DisasmMode::from_symbol(&s)) .filter_map(|s| DisasmMode::from_symbol(&s))
.collect(); .collect();
mapping_symbols.sort_unstable_by_key(|x| x.address); mapping_symbols.sort_unstable_by_key(|x| x.address);
(s.index(), mapping_symbols) (s.index().0, mapping_symbols)
}) })
.collect() .collect()
} }
@ -121,7 +125,7 @@ impl ObjArch for ObjArchArm {
let fallback_mappings = [DisasmMode { address: start_addr, mapping: ParseMode::Arm }]; let fallback_mappings = [DisasmMode { address: start_addr, mapping: ParseMode::Arm }];
let mapping_symbols = self let mapping_symbols = self
.disasm_modes .disasm_modes
.get(&SectionIndex(section_index)) .get(&section_index)
.map(|x| x.as_slice()) .map(|x| x.as_slice())
.unwrap_or(&fallback_mappings); .unwrap_or(&fallback_mappings);
let first_mapping_idx = mapping_symbols let first_mapping_idx = mapping_symbols

View File

@ -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 anyhow::{bail, Result};
use object::{elf, File, Relocation, RelocationFlags}; use object::{elf, File, Relocation, RelocationFlags};

View File

@ -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 anyhow::{anyhow, bail, Result};
use object::{ use object::{

View File

@ -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 anyhow::{bail, Result};
use byteorder::ByteOrder; use byteorder::ByteOrder;

View File

@ -1,6 +1,10 @@
use std::{ use alloc::{
borrow::Cow, borrow::Cow,
collections::{BTreeMap, HashMap, HashSet}, collections::{BTreeMap, BTreeSet},
format,
string::{String, ToString},
vec,
vec::Vec,
}; };
use anyhow::{bail, ensure, Result}; 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 // 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. // that register to hold some other value, unrelated to pool relocation addresses.
fn clear_overwritten_gprs(ins: Ins, gpr_pool_relocs: &mut HashMap<u8, ObjReloc>) { fn clear_overwritten_gprs(ins: Ins, gpr_pool_relocs: &mut BTreeMap<u8, ObjReloc>) {
let mut def_args = Arguments::default(); let mut def_args = Arguments::default();
ins.parse_defs(&mut def_args); ins.parse_defs(&mut def_args);
for arg in def_args { for arg in def_args {
@ -576,11 +580,11 @@ fn generate_fake_pool_reloc_for_addr_mapping(
func_address: u64, func_address: u64,
code: &[u8], code: &[u8],
relocations: &[ObjReloc], relocations: &[ObjReloc],
) -> HashMap<u32, ObjReloc> { ) -> BTreeMap<u32, ObjReloc> {
let mut visited_ins_addrs = HashSet::new(); let mut visited_ins_addrs = BTreeSet::new();
let mut pool_reloc_for_addr = HashMap::new(); let mut pool_reloc_for_addr = BTreeMap::new();
let mut ins_iters_with_gpr_state = 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(); let mut gpr_state_at_bctr = BTreeMap::new();
while let Some((ins_iter, mut gpr_pool_relocs)) = ins_iters_with_gpr_state.pop() { while let Some((ins_iter, mut gpr_pool_relocs)) = ins_iters_with_gpr_state.pop() {
for (cur_addr, ins) in ins_iter { for (cur_addr, ins) in ins_iter {

View File

@ -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 anyhow::{anyhow, bail, ensure, Result};
use iced_x86::{ use iced_x86::{

View File

@ -1,4 +1,7 @@
#![allow(clippy::needless_lifetimes)] // Generated serde code #![allow(clippy::needless_lifetimes)] // Generated serde code
use alloc::string::ToString;
use crate::{ use crate::{
diff::{ diff::{
ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo,
@ -13,6 +16,7 @@ use crate::{
// Protobuf diff types // Protobuf diff types
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.rs")); include!(concat!(env!("OUT_DIR"), "/objdiff.diff.rs"));
#[cfg(feature = "serde")]
include!(concat!(env!("OUT_DIR"), "/objdiff.diff.serde.rs")); include!(concat!(env!("OUT_DIR"), "/objdiff.diff.serde.rs"));
impl DiffResult { impl DiffResult {

View File

@ -1,5 +1,3 @@
#[cfg(feature = "any-arch")] #[cfg(feature = "any-arch")]
pub mod diff; pub mod diff;
pub mod report; pub mod report;
#[cfg(feature = "wasm")]
pub mod wasm;

View File

@ -1,12 +1,18 @@
#![allow(clippy::needless_lifetimes)] // Generated serde code #![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 anyhow::{bail, Result};
use prost::Message; use prost::Message;
use serde_json::error::Category;
// Protobuf report types // Protobuf report types
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs")); include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
#[cfg(feature = "serde")]
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs")); include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
pub const REPORT_VERSION: u32 = 2; pub const REPORT_VERSION: u32 = 2;
@ -15,23 +21,30 @@ impl Report {
/// Attempts to parse the report as binary protobuf or JSON. /// Attempts to parse the report as binary protobuf or JSON.
pub fn parse(data: &[u8]) -> Result<Self> { pub fn parse(data: &[u8]) -> Result<Self> {
if data.is_empty() { if data.is_empty() {
bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof)); bail!("Empty data");
} }
let report = if data[0] == b'{' { let report = if data[0] == b'{' {
// Load as JSON // Load as JSON
#[cfg(feature = "serde")]
{
Self::from_json(data)? Self::from_json(data)?
}
#[cfg(not(feature = "serde"))]
bail!("JSON report parsing requires the `serde` feature")
} else { } else {
// Load as binary protobuf // Load as binary protobuf
Self::decode(data)? Self::decode(data).map_err(|e| anyhow::Error::msg(e.to_string()))?
}; };
Ok(report) Ok(report)
} }
#[cfg(feature = "serde")]
/// Attempts to parse the report as JSON, migrating from the legacy report format if necessary. /// Attempts to parse the report as JSON, migrating from the legacy report format if necessary.
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> { fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
match serde_json::from_slice::<Self>(bytes) { match serde_json::from_slice::<Self>(bytes) {
Ok(report) => Ok(report), Ok(report) => Ok(report),
Err(e) => { Err(e) => {
use serde_json::error::Category;
match e.classify() { match e.classify() {
Category::Io | Category::Eof | Category::Syntax => Err(e), Category::Io | Category::Eof | Category::Syntax => Err(e),
Category::Data => { Category::Data => {
@ -304,7 +317,8 @@ impl FromIterator<Measures> for Measures {
} }
// Older JSON report types // 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 { struct LegacyReport {
fuzzy_match_percent: f32, fuzzy_match_percent: f32,
total_code: u64, total_code: u64,
@ -341,7 +355,8 @@ impl From<LegacyReport> 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 { struct LegacyReportUnit {
name: String, name: String,
fuzzy_match_percent: f32, fuzzy_match_percent: f32,
@ -351,11 +366,11 @@ struct LegacyReportUnit {
matched_data: u64, matched_data: u64,
total_functions: u32, total_functions: u32,
matched_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<bool>, complete: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
module_name: Option<String>, module_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
module_id: Option<u32>, module_id: Option<u32>,
sections: Vec<LegacyReportItem>, sections: Vec<LegacyReportItem>,
functions: Vec<LegacyReportItem>, functions: Vec<LegacyReportItem>,
@ -389,16 +404,20 @@ impl From<LegacyReportUnit> 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 { struct LegacyReportItem {
name: String, name: String,
#[serde(skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
demangled_name: Option<String>, demangled_name: Option<String>,
#[serde( #[cfg_attr(
feature = "serde",
serde(
default, default,
skip_serializing_if = "Option::is_none", skip_serializing_if = "Option::is_none",
serialize_with = "serialize_hex", serialize_with = "serialize_hex",
deserialize_with = "deserialize_hex" deserialize_with = "deserialize_hex"
)
)] )]
address: Option<u64>, address: Option<u64>,
size: u64, size: u64,
@ -419,6 +438,7 @@ impl From<LegacyReportItem> for ReportItem {
} }
} }
#[cfg(feature = "serde")]
fn serialize_hex<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error> fn serialize_hex<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
where S: serde::Serializer { where S: serde::Serializer {
if let Some(x) = x { if let Some(x) = x {
@ -428,6 +448,7 @@ where S: serde::Serializer {
} }
} }
#[cfg(feature = "serde")]
fn deserialize_hex<'de, D>(d: D) -> Result<Option<u64>, D::Error> fn deserialize_hex<'de, D>(d: D) -> Result<Option<u64>, D::Error>
where D: serde::Deserializer<'de> { where D: serde::Deserializer<'de> {
use serde::Deserialize; use serde::Deserialize;

View File

@ -1,81 +0,0 @@
use prost::Message;
use wasm_bindgen::prelude::*;
use crate::{bindings::diff::DiffResult, diff, obj};
fn parse_object(
data: Option<Box<[u8]>>,
config: &diff::DiffObjConfig,
) -> Result<Option<obj::ObjInfo>, JsError> {
data.as_ref().map(|data| obj::read::parse(data, config)).transpose().to_js()
}
fn parse_and_run_diff(
left: Option<Box<[u8]>>,
right: Option<Box<[u8]>>,
diff_config: diff::DiffObjConfig,
mapping_config: diff::MappingConfig,
) -> Result<DiffResult, JsError> {
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<DiffResult, JsError> {
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<Box<[u8]>>,
// right: Option<Box<[u8]>>,
// config: diff::DiffObjConfig,
// ) -> Result<String, JsError> {
// 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<Box<[u8]>>,
right: Option<Box<[u8]>>,
diff_config: diff::DiffObjConfig,
mapping_config: diff::MappingConfig,
) -> Result<Box<[u8]>, 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<Self::Output, JsError>;
}
impl<T, E: std::fmt::Display> ToJsResult for Result<T, E> {
type Output = T;
fn to_js(self) -> Result<T, JsError> { self.map_err(to_js_error) }
}

View File

@ -1,9 +1,8 @@
pub mod watcher; pub mod watcher;
use std::{ use std::process::Command;
path::{Path, PathBuf},
process::Command, use typed_path::Utf8PlatformPathBuf;
};
pub struct BuildStatus { pub struct BuildStatus {
pub success: bool, pub success: bool,
@ -25,14 +24,14 @@ impl Default for BuildStatus {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BuildConfig { pub struct BuildConfig {
pub project_dir: Option<PathBuf>, pub project_dir: Option<Utf8PlatformPathBuf>,
pub custom_make: Option<String>, pub custom_make: Option<String>,
pub custom_args: Option<Vec<String>>, pub custom_args: Option<Vec<String>>,
#[allow(unused)] #[allow(unused)]
pub selected_wsl_distro: Option<String>, pub selected_wsl_distro: Option<String>,
} }
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 { let Some(cwd) = &config.project_dir else {
return BuildStatus { return BuildStatus {
success: false, success: false,

View File

@ -1,37 +1,47 @@
use std::{ pub mod path;
use alloc::{
collections::BTreeMap, collections::BTreeMap,
fs, string::{String, ToString},
fs::File, vec::Vec,
io::{BufReader, BufWriter, Read},
path::{Path, PathBuf},
}; };
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use filetime::FileTime;
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
use path::unix_path_serde_option;
use typed_path::Utf8UnixPathBuf;
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] #[derive(Default, Clone)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
pub struct ProjectConfig { 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<String>, pub min_version: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub custom_make: Option<String>, pub custom_make: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub custom_args: Option<Vec<String>>, pub custom_args: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(
pub target_dir: Option<PathBuf>, feature = "serde",
#[serde(default, skip_serializing_if = "Option::is_none")] serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
pub base_dir: Option<PathBuf>, )]
#[serde(default, skip_serializing_if = "Option::is_none")] pub target_dir: Option<Utf8UnixPathBuf>,
#[cfg_attr(
feature = "serde",
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
)]
pub base_dir: Option<Utf8UnixPathBuf>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub build_base: Option<bool>, pub build_base: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub build_target: Option<bool>, pub build_target: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub watch_patterns: Option<Vec<String>>, pub watch_patterns: Option<Vec<String>>,
#[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<Vec<ProjectObject>>, pub units: Option<Vec<ProjectObject>>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub progress_categories: Option<Vec<ProjectProgressCategory>>, pub progress_categories: Option<Vec<ProjectProgressCategory>>,
} }
@ -39,11 +49,6 @@ impl ProjectConfig {
#[inline] #[inline]
pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() } pub fn units(&self) -> &[ProjectObject] { self.units.as_deref().unwrap_or_default() }
#[inline]
pub fn units_mut(&mut self) -> &mut Vec<ProjectObject> {
self.units.get_or_insert_with(Vec::new)
}
#[inline] #[inline]
pub fn progress_categories(&self) -> &[ProjectProgressCategory] { pub fn progress_categories(&self) -> &[ProjectProgressCategory] {
self.progress_categories.as_deref().unwrap_or_default() self.progress_categories.as_deref().unwrap_or_default()
@ -66,55 +71,62 @@ impl ProjectConfig {
} }
} }
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] #[derive(Default, Clone)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
pub struct ProjectObject { 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<String>, pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(
pub path: Option<PathBuf>, feature = "serde",
#[serde(default, skip_serializing_if = "Option::is_none")] serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
pub target_path: Option<PathBuf>, )]
#[serde(default, skip_serializing_if = "Option::is_none")] pub path: Option<Utf8UnixPathBuf>,
pub base_path: Option<PathBuf>, #[cfg_attr(
#[serde(default, skip_serializing_if = "Option::is_none")] feature = "serde",
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
)]
pub target_path: Option<Utf8UnixPathBuf>,
#[cfg_attr(
feature = "serde",
serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
)]
pub base_path: Option<Utf8UnixPathBuf>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[deprecated(note = "Use metadata.reverse_fn_order")] #[deprecated(note = "Use metadata.reverse_fn_order")]
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
#[deprecated(note = "Use metadata.complete")] #[deprecated(note = "Use metadata.complete")]
pub complete: Option<bool>, pub complete: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub scratch: Option<ScratchConfig>, pub scratch: Option<ScratchConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub metadata: Option<ProjectObjectMetadata>, pub metadata: Option<ProjectObjectMetadata>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub symbol_mappings: Option<SymbolMappings>, pub symbol_mappings: Option<BTreeMap<String, String>>,
} }
#[cfg_attr(feature = "wasm", tsify_next::declare)] #[derive(Default, Clone)]
pub type SymbolMappings = BTreeMap<String, String>; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))]
pub struct ProjectObjectMetadata { 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<bool>, pub complete: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(
pub source_path: Option<String>, feature = "serde",
#[serde(default, skip_serializing_if = "Option::is_none")] serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
)]
pub source_path: Option<Utf8UnixPathBuf>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub progress_categories: Option<Vec<String>>, pub progress_categories: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub auto_generated: Option<bool>, pub auto_generated: Option<bool>,
} }
#[derive(Default, Clone, serde::Serialize, serde::Deserialize)] #[derive(Default, Clone)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
pub struct ProjectProgressCategory { pub struct ProjectProgressCategory {
#[serde(default)]
pub id: String, pub id: String,
#[serde(default)]
pub name: String, pub name: String,
} }
@ -123,33 +135,12 @@ impl ProjectObject {
if let Some(name) = &self.name { if let Some(name) = &self.name {
name name
} else if let Some(path) = &self.path { } else if let Some(path) = &self.path {
path.to_str().unwrap_or("[invalid path]") path.as_str()
} else { } else {
"[unknown]" "[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<bool> { pub fn complete(&self) -> Option<bool> {
#[expect(deprecated)] #[expect(deprecated)]
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete) 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) 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()) 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<bool> {
self.metadata.as_ref().and_then(|m| m.auto_generated)
}
} }
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[derive(Default, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize), serde(default))]
pub struct ScratchConfig { 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<String>, pub platform: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub compiler: Option<String>, pub compiler: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub c_flags: Option<String>, pub c_flags: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(
pub ctx_path: Option<PathBuf>, feature = "serde",
#[serde(default, skip_serializing_if = "Option::is_none")] serde(with = "unix_path_serde_option", skip_serializing_if = "Option::is_none")
)]
pub ctx_path: Option<Utf8UnixPathBuf>,
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub build_ctx: Option<bool>, pub build_ctx: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
pub preset_id: Option<u32>, pub preset_id: Option<u32>,
} }
@ -197,16 +199,20 @@ pub fn default_watch_patterns() -> Vec<Glob> {
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect() DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
} }
#[cfg(feature = "std")]
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]
pub struct ProjectConfigInfo { pub struct ProjectConfigInfo {
pub path: PathBuf, pub path: std::path::PathBuf,
pub timestamp: Option<FileTime>, pub timestamp: Option<filetime::FileTime>,
} }
pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> { #[cfg(feature = "std")]
pub fn try_project_config(
dir: &std::path::Path,
) -> Option<(Result<ProjectConfig>, ProjectConfigInfo)> {
for filename in CONFIG_FILENAMES.iter() { for filename in CONFIG_FILENAMES.iter() {
let config_path = dir.join(filename); 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; continue;
}; };
let metadata = file.metadata(); let metadata = file.metadata();
@ -214,12 +220,9 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
if !metadata.is_file() { if !metadata.is_file() {
continue; continue;
} }
let ts = FileTime::from_last_modification_time(&metadata); let ts = filetime::FileTime::from_last_modification_time(&metadata);
let mut reader = BufReader::new(file); let mut reader = std::io::BufReader::new(file);
let mut result = match filename.contains("json") { let mut result = read_json_config(&mut reader);
true => read_json_config(&mut reader),
false => read_yml_config(&mut reader),
};
if let Ok(config) = &result { if let Ok(config) = &result {
// Validate min_version if present // Validate min_version if present
if let Err(e) = validate_min_version(config) { if let Err(e) = validate_min_version(config) {
@ -232,40 +235,42 @@ pub fn try_project_config(dir: &Path) -> Option<(Result<ProjectConfig>, ProjectC
None None
} }
#[cfg(feature = "std")]
pub fn save_project_config( pub fn save_project_config(
config: &ProjectConfig, config: &ProjectConfig,
info: &ProjectConfigInfo, info: &ProjectConfigInfo,
) -> Result<ProjectConfigInfo> { ) -> Result<ProjectConfigInfo> {
if let Some(last_ts) = info.timestamp { if let Some(last_ts) = info.timestamp {
// Check if the file has changed since we last read it // Check if the file has changed since we last read it
if let Ok(metadata) = fs::metadata(&info.path) { if let Ok(metadata) = std::fs::metadata(&info.path) {
let ts = FileTime::from_last_modification_time(&metadata); let ts = filetime::FileTime::from_last_modification_time(&metadata);
if ts != last_ts { if ts != last_ts {
return Err(anyhow!("Config file has changed since last read")); return Err(anyhow!("Config file has changed since last read"));
} }
} }
} }
let mut writer = let mut writer = std::io::BufWriter::new(
BufWriter::new(File::create(&info.path).context("Failed to create config file")?); 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"); let ext = info.path.extension().and_then(|ext| ext.to_str()).unwrap_or("json");
match ext { match ext {
"json" => serde_json::to_writer_pretty(&mut writer, config).context("Failed to write JSON"), "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}")), _ => Err(anyhow!("Unknown config file extension: {ext}")),
}?; }?;
let file = writer.into_inner().context("Failed to flush file")?; let file = writer.into_inner().context("Failed to flush file")?;
let metadata = file.metadata().context("Failed to get file metadata")?; 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) }) Ok(ProjectConfigInfo { path: info.path.clone(), timestamp: Some(ts) })
} }
fn validate_min_version(config: &ProjectConfig) -> Result<()> { fn validate_min_version(config: &ProjectConfig) -> Result<()> {
let Some(min_version) = &config.min_version else { return Ok(()) }; let Some(min_version) = &config.min_version else { return Ok(()) };
let version = semver::Version::parse(env!("CARGO_PKG_VERSION")) let version = semver::Version::parse(env!("CARGO_PKG_VERSION"))
.map_err(|e| anyhow::Error::msg(e.to_string()))
.context("Failed to parse package version")?; .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 { if version >= min_version {
Ok(()) Ok(())
} else { } else {
@ -273,15 +278,12 @@ fn validate_min_version(config: &ProjectConfig) -> Result<()> {
} }
} }
fn read_yml_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> { #[cfg(feature = "std")]
Ok(serde_yaml::from_reader(reader)?) fn read_json_config<R: std::io::Read>(reader: &mut R) -> Result<ProjectConfig> {
}
fn read_json_config<R: Read>(reader: &mut R) -> Result<ProjectConfig> {
Ok(serde_json::from_reader(reader)?) Ok(serde_json::from_reader(reader)?)
} }
pub fn build_globset(vec: &[Glob]) -> std::result::Result<GlobSet, globset::Error> { pub fn build_globset(vec: &[Glob]) -> Result<GlobSet, globset::Error> {
let mut builder = GlobSetBuilder::new(); let mut builder = GlobSetBuilder::new();
for glob in vec { for glob in vec {
builder.add(glob.clone()); builder.add(glob.clone());

View File

@ -0,0 +1,65 @@
// For argp::FromArgs
#[cfg(feature = "std")]
pub fn platform_path(value: &str) -> Result<typed_path::Utf8PlatformPathBuf, String> {
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, alloc::string::FromUtf8Error> {
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<S>(path: &Option<Utf8UnixPathBuf>, s: S) -> Result<S::Ok, S::Error>
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<Option<Utf8UnixPathBuf>, D::Error>
where D: Deserializer<'de> {
Ok(Option::<String>::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<S>(path: &Option<Utf8PlatformPathBuf>, s: S) -> Result<S::Ok, S::Error>
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<Option<Utf8PlatformPathBuf>, D::Error>
where D: Deserializer<'de> {
Ok(Option::<String>::deserialize(deserializer)?.map(Utf8PlatformPathBuf::from))
}
}

View File

@ -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 anyhow::{anyhow, Result};
use similar::{capture_diff_slices_deadline, Algorithm}; use similar::{capture_diff_slices, Algorithm};
use super::FunctionRelocDiffs; use super::FunctionRelocDiffs;
use crate::{ use crate::{
@ -118,8 +123,7 @@ fn diff_instructions(
left_code: &ProcessCodeResult, left_code: &ProcessCodeResult,
right_code: &ProcessCodeResult, right_code: &ProcessCodeResult,
) -> Result<()> { ) -> Result<()> {
let ops = let ops = capture_diff_slices(Algorithm::Patience, &left_code.ops, &right_code.ops);
capture_diff_slices_deadline(Algorithm::Patience, &left_code.ops, &right_code.ops, None);
if ops.is_empty() { if ops.is_empty() {
left_diff.extend( left_diff.extend(
left_code left_code
@ -138,7 +142,7 @@ fn diff_instructions(
for op in ops { for op in ops {
let (_tag, left_range, right_range) = op.as_tag_tuple(); 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_diff.extend(
left_code.insts[left_range.clone()] left_code.insts[left_range.clone()]
.iter() .iter()

View File

@ -1,10 +1,8 @@
use std::{ use alloc::{vec, vec::Vec};
cmp::{max, min, Ordering}, use core::{cmp::Ordering, ops::Range};
ops::Range,
};
use anyhow::{anyhow, Result}; 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 super::code::{address_eq, section_name_eq};
use crate::{ 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); right.symbols.iter().map(|s| s.section_address + s.size).max().unwrap_or(0).min(right.size);
let left_data = &left.data[..left_max as usize]; let left_data = &left.data[..left_max as usize];
let right_data = &right.data[..right_max as usize]; let right_data = &right.data[..right_max as usize];
let ops = 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 match_percent = get_diff_ratio(&ops, left_data.len(), right_data.len()) * 100.0;
let mut left_diff = Vec::<ObjDataDiff>::new(); let mut left_diff = Vec::<ObjDataDiff>::new();
@ -151,27 +149,27 @@ pub fn diff_data_section(
let (tag, left_range, right_range) = op.as_tag_tuple(); let (tag, left_range, right_range) = op.as_tag_tuple();
let left_len = left_range.len(); let left_len = left_range.len();
let right_len = right_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 { let kind = match tag {
similar::DiffTag::Equal => ObjDataDiffKind::None, similar::DiffTag::Equal => ObjDataDiffKind::None,
similar::DiffTag::Delete => ObjDataDiffKind::Delete, similar::DiffTag::Delete => ObjDataDiffKind::Delete,
similar::DiffTag::Insert => ObjDataDiffKind::Insert, similar::DiffTag::Insert => ObjDataDiffKind::Insert,
similar::DiffTag::Replace => { similar::DiffTag::Replace => {
// Ensure replacements are equal length // Ensure replacements are equal length
len = min(left_len, right_len); len = left_len.min(right_len);
ObjDataDiffKind::Replace ObjDataDiffKind::Replace
} }
}; };
let left_data = &left.data[left_range]; let left_data = &left.data[left_range];
let right_data = &right.data[right_range]; let right_data = &right.data[right_range];
left_diff.push(ObjDataDiff { 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, kind,
len, len,
..Default::default() ..Default::default()
}); });
right_diff.push(ObjDataDiff { 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, kind,
len, len,
..Default::default() ..Default::default()
@ -283,7 +281,7 @@ pub fn diff_data_symbol(
right_range, 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 bytes_match_ratio = get_diff_ratio(&ops, left_data.len(), right_data.len());
let mut match_ratio = bytes_match_ratio; let mut match_ratio = bytes_match_ratio;
@ -375,7 +373,7 @@ pub fn diff_bss_section(
) -> Result<(ObjSectionDiff, ObjSectionDiff)> { ) -> Result<(ObjSectionDiff, ObjSectionDiff)> {
let left_sizes = left.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>(); let left_sizes = left.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
let right_sizes = right.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>(); let right_sizes = right.symbols.iter().map(|s| (s.section_address, s.size)).collect::<Vec<_>>();
let ops = capture_diff_slices_deadline(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; let mut match_percent = get_diff_ratio(&ops, left_sizes.len(), right_sizes.len()) * 100.0;
// Use the highest match percent between two options: // Use the highest match percent between two options:

View File

@ -1,3 +1,5 @@
use alloc::string::{String, ToString};
use crate::{ use crate::{
diff::{ObjInsArgDiff, ObjInsDiff}, diff::{ObjInsArgDiff, ObjInsDiff},
obj::{ObjInsArg, ObjInsArgValue, ObjReloc, ObjSymbol}, obj::{ObjInsArg, ObjInsArgValue, ObjReloc, ObjSymbol},

View File

@ -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 anyhow::Result;
use crate::{ use crate::{
config::SymbolMappings,
diff::{ diff::{
code::{diff_code, no_diff_code, process_code_symbol}, code::{diff_code, no_diff_code, process_code_symbol},
data::{ data::{
@ -473,12 +478,11 @@ struct SectionMatch {
section_kind: ObjSectionKind, section_kind: ObjSectionKind,
} }
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)] #[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "wasm", derive(tsify_next::Tsify), tsify(from_wasm_abi))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(default))]
#[serde(default)]
pub struct MappingConfig { pub struct MappingConfig {
/// Manual symbol mappings /// Manual symbol mappings
pub mappings: SymbolMappings, pub mappings: BTreeMap<String, String>,
/// The right object symbol name that we're selecting a left symbol for /// The right object symbol name that we're selecting a left symbol for
pub selecting_left: Option<String>, pub selecting_left: Option<String>,
/// The left object symbol name that we're selecting a right symbol for /// The left object symbol name that we're selecting a right symbol for
@ -500,8 +504,8 @@ fn apply_symbol_mappings(
left: &ObjInfo, left: &ObjInfo,
right: &ObjInfo, right: &ObjInfo,
mapping_config: &MappingConfig, mapping_config: &MappingConfig,
left_used: &mut HashSet<SymbolRef>, left_used: &mut BTreeSet<SymbolRef>,
right_used: &mut HashSet<SymbolRef>, right_used: &mut BTreeSet<SymbolRef>,
matches: &mut Vec<SymbolMatch>, matches: &mut Vec<SymbolMatch>,
) -> Result<()> { ) -> Result<()> {
// If we're selecting a symbol to use as a comparison, mark it as used // If we're selecting a symbol to use as a comparison, mark it as used
@ -563,8 +567,8 @@ fn matching_symbols(
mappings: &MappingConfig, mappings: &MappingConfig,
) -> Result<Vec<SymbolMatch>> { ) -> Result<Vec<SymbolMatch>> {
let mut matches = Vec::new(); let mut matches = Vec::new();
let mut left_used = HashSet::new(); let mut left_used = BTreeSet::new();
let mut right_used = HashSet::new(); let mut right_used = BTreeSet::new();
if let Some(left) = left { if let Some(left) = left {
if let Some(right) = right { if let Some(right) = right {
apply_symbol_mappings( apply_symbol_mappings(
@ -645,7 +649,7 @@ fn matching_symbols(
fn unmatched_symbols<'section, 'used>( fn unmatched_symbols<'section, 'used>(
section: &'section ObjSection, section: &'section ObjSection,
section_idx: usize, section_idx: usize,
used: Option<&'used HashSet<SymbolRef>>, used: Option<&'used BTreeSet<SymbolRef>>,
) -> impl Iterator<Item = (usize, &'section ObjSymbol)> + 'used ) -> impl Iterator<Item = (usize, &'section ObjSymbol)> + 'used
where where
'section: 'used, 'section: 'used,
@ -660,7 +664,7 @@ fn find_symbol(
obj: Option<&ObjInfo>, obj: Option<&ObjInfo>,
in_symbol: &ObjSymbol, in_symbol: &ObjSymbol,
in_section: &ObjSection, in_section: &ObjSection,
used: Option<&HashSet<SymbolRef>>, used: Option<&BTreeSet<SymbolRef>>,
) -> Option<SymbolRef> { ) -> Option<SymbolRef> {
let obj = obj?; let obj = obj?;
// Try to find an exact name match // Try to find an exact name match

View File

@ -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 anyhow::{anyhow, bail, Context, Result};
use typed_path::{Utf8PlatformPathBuf, Utf8UnixPathBuf};
use crate::{ use crate::{
build::{run_make, BuildConfig, BuildStatus}, build::{run_make, BuildConfig, BuildStatus},
@ -10,7 +11,7 @@ use crate::{
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CreateScratchConfig { pub struct CreateScratchConfig {
pub build_config: BuildConfig, pub build_config: BuildConfig,
pub context_path: Option<PathBuf>, pub context_path: Option<Utf8UnixPathBuf>,
pub build_context: bool, pub build_context: bool,
// Scratch fields // Scratch fields
@ -18,7 +19,7 @@ pub struct CreateScratchConfig {
pub platform: String, pub platform: String,
pub compiler_flags: String, pub compiler_flags: String,
pub function_name: String, pub function_name: String,
pub target_obj: PathBuf, pub target_obj: Utf8PlatformPathBuf,
pub preset_id: Option<u32>, pub preset_id: Option<u32>,
} }
@ -47,26 +48,25 @@ fn run_create_scratch(
if let Some(context_path) = &config.context_path { if let Some(context_path) = &config.context_path {
if config.build_context { if config.build_context {
update_status(status, "Building context".to_string(), 0, 2, &cancel)?; 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: true, .. } => {}
BuildStatus { success: false, stdout, stderr, .. } => { BuildStatus { success: false, stdout, stderr, .. } => {
bail!("Failed to build context:\n{stdout}\n{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( context = Some(
fs::read_to_string(&context_path) 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)?; update_status(status, "Creating scratch".to_string(), 1, 2, &cancel)?;
let diff_flags = [format!("--disassemble={}", config.function_name)]; let diff_flags = [format!("--disassemble={}", config.function_name)];
let diff_flags = serde_json::to_string(&diff_flags)?; 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(&config.target_obj)
let file = reqwest::blocking::multipart::Part::file(&obj_path) .with_context(|| format!("Failed to open {}", config.target_obj))?;
.with_context(|| format!("Failed to open {}", obj_path.display()))?;
let mut form = reqwest::blocking::multipart::Form::new() let mut form = reqwest::blocking::multipart::Form::new()
.text("compiler", config.compiler.clone()) .text("compiler", config.compiler.clone())
.text("platform", config.platform.clone()) .text("platform", config.platform.clone())

View File

@ -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 anyhow::{anyhow, Error, Result};
use time::OffsetDateTime; use time::OffsetDateTime;
use typed_path::Utf8PlatformPathBuf;
use crate::{ use crate::{
build::{run_make, BuildConfig, BuildStatus}, build::{run_make, BuildConfig, BuildStatus},
@ -14,8 +15,8 @@ pub struct ObjDiffConfig {
pub build_config: BuildConfig, pub build_config: BuildConfig,
pub build_base: bool, pub build_base: bool,
pub build_target: bool, pub build_target: bool,
pub target_path: Option<PathBuf>, pub target_path: Option<Utf8PlatformPathBuf>,
pub base_path: Option<PathBuf>, pub base_path: Option<Utf8PlatformPathBuf>,
pub diff_obj_config: DiffObjConfig, pub diff_obj_config: DiffObjConfig,
pub mapping_config: MappingConfig, pub mapping_config: MappingConfig,
} }
@ -43,20 +44,12 @@ fn run_build(
.ok_or_else(|| Error::msg("Missing project dir"))?; .ok_or_else(|| Error::msg("Missing project dir"))?;
if let Some(target_path) = &config.target_path { if let Some(target_path) = &config.target_path {
target_path_rel = Some(target_path.strip_prefix(project_dir).map_err(|_| { target_path_rel = Some(target_path.strip_prefix(project_dir).map_err(|_| {
anyhow!( anyhow!("Target path '{}' doesn't begin with '{}'", target_path, project_dir)
"Target path '{}' doesn't begin with '{}'",
target_path.display(),
project_dir.display()
)
})?); })?);
} }
if let Some(base_path) = &config.base_path { if let Some(base_path) = &config.base_path {
base_path_rel = Some(base_path.strip_prefix(project_dir).map_err(|_| { base_path_rel = Some(base_path.strip_prefix(project_dir).map_err(|_| {
anyhow!( anyhow!("Base path '{}' doesn't begin with '{}'", base_path, project_dir)
"Base path '{}' doesn't begin with '{}'",
base_path.display(),
project_dir.display()
)
})?); })?);
}; };
} }
@ -80,13 +73,13 @@ fn run_build(
Some(target_path_rel) if config.build_target => { Some(target_path_rel) if config.build_target => {
update_status( update_status(
context, context,
format!("Building target {}", target_path_rel.display()), format!("Building target {}", target_path_rel),
step_idx, step_idx,
total, total,
&cancel, &cancel,
)?; )?;
step_idx += 1; step_idx += 1;
run_make(&config.build_config, target_path_rel) run_make(&config.build_config, target_path_rel.as_ref())
} }
_ => BuildStatus::default(), _ => BuildStatus::default(),
}; };
@ -95,13 +88,13 @@ fn run_build(
Some(base_path_rel) if config.build_base => { Some(base_path_rel) if config.build_base => {
update_status( update_status(
context, context,
format!("Building base {}", base_path_rel.display()), format!("Building base {}", base_path_rel),
step_idx, step_idx,
total, total,
&cancel, &cancel,
)?; )?;
step_idx += 1; step_idx += 1;
run_make(&config.build_config, base_path_rel) run_make(&config.build_config, base_path_rel.as_ref())
} }
_ => BuildStatus::default(), _ => BuildStatus::default(),
}; };
@ -112,18 +105,18 @@ fn run_build(
Some(target_path) if first_status.success => { Some(target_path) if first_status.success => {
update_status( update_status(
context, context,
format!("Loading target {}", target_path.display()), format!("Loading target {}", target_path),
step_idx, step_idx,
total, total,
&cancel, &cancel,
)?; )?;
step_idx += 1; 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), Ok(obj) => Some(obj),
Err(e) => { Err(e) => {
first_status = BuildStatus { first_status = BuildStatus {
success: false, success: false,
stdout: format!("Loading object '{}'", target_path.display()), stdout: format!("Loading object '{}'", target_path),
stderr: format!("{:#}", e), stderr: format!("{:#}", e),
..Default::default() ..Default::default()
}; };
@ -142,18 +135,18 @@ fn run_build(
Some(base_path) if second_status.success => { Some(base_path) if second_status.success => {
update_status( update_status(
context, context,
format!("Loading base {}", base_path.display()), format!("Loading base {}", base_path),
step_idx, step_idx,
total, total,
&cancel, &cancel,
)?; )?;
step_idx += 1; 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), Ok(obj) => Some(obj),
Err(e) => { Err(e) => {
second_status = BuildStatus { second_status = BuildStatus {
success: false, success: false,
stdout: format!("Loading object '{}'", base_path.display()), stdout: format!("Loading object '{}'", base_path),
stderr: format!("{:#}", e), stderr: format!("{:#}", e),
..Default::default() ..Default::default()
}; };

View File

@ -1,3 +1,6 @@
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
#[cfg(feature = "any-arch")] #[cfg(feature = "any-arch")]
pub mod arch; pub mod arch;
#[cfg(feature = "bindings")] #[cfg(feature = "bindings")]
@ -14,3 +17,5 @@ pub mod jobs;
pub mod obj; pub mod obj;
#[cfg(feature = "any-arch")] #[cfg(feature = "any-arch")]
pub mod util; pub mod util;
#[cfg(feature = "wasm")]
pub mod wasm;

View File

@ -1,9 +1,9 @@
pub mod read; pub mod read;
pub mod split_meta; 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 flagset::{flags, FlagSet};
use object::RelocationFlags; use object::RelocationFlags;
use split_meta::SplitMeta; use split_meta::SplitMeta;
@ -152,8 +152,9 @@ pub struct ObjSymbol {
pub struct ObjInfo { pub struct ObjInfo {
pub arch: Box<dyn ObjArch>, pub arch: Box<dyn ObjArch>,
pub path: Option<PathBuf>, pub path: Option<String>,
pub timestamp: Option<FileTime>, #[cfg(feature = "std")]
pub timestamp: Option<filetime::FileTime>,
pub sections: Vec<ObjSection>, pub sections: Vec<ObjSection>,
/// Common BSS symbols /// Common BSS symbols
pub common: Vec<ObjSymbol>, pub common: Vec<ObjSymbol>,

View File

@ -1,13 +1,12 @@
use std::{ use alloc::{
collections::{HashMap, HashSet}, collections::{BTreeMap, BTreeSet},
fs, format,
io::Cursor, string::{String, ToString},
mem::size_of, vec,
path::Path, vec::Vec,
}; };
use anyhow::{anyhow, bail, ensure, Context, Result}; use anyhow::{anyhow, bail, ensure, Context, Result};
use filetime::FileTime;
use flagset::Flags; use flagset::Flags;
use object::{ use object::{
endian::LittleEndian as LE, endian::LittleEndian as LE,
@ -160,7 +159,7 @@ fn symbols_by_section(
section: &ObjSection, section: &ObjSection,
section_symbols: &[Symbol<'_, '_>], section_symbols: &[Symbol<'_, '_>],
split_meta: Option<&SplitMeta>, split_meta: Option<&SplitMeta>,
name_counts: &mut HashMap<String, u32>, name_counts: &mut BTreeMap<String, u32>,
) -> Result<Vec<ObjSymbol>> { ) -> Result<Vec<ObjSymbol>> {
let mut result = Vec::<ObjSymbol>::new(); let mut result = Vec::<ObjSymbol>::new();
for symbol in section_symbols { for symbol in section_symbols {
@ -377,33 +376,37 @@ fn line_info(obj_file: &File<'_>, sections: &mut [ObjSection], obj_data: &[u8])
// DWARF 1.1 // DWARF 1.1
if let Some(section) = obj_file.section_by_name(".line") { if let Some(section) = obj_file.section_by_name(".line") {
let data = section.uncompressed_data()?; 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); 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 let text_section_index = text_sections
.next() .next()
.ok_or_else(|| anyhow!("Next text section not found for line info"))? .ok_or_else(|| anyhow!("Next text section not found for line info"))?
.index() .index()
.0; .0;
let start = reader.position();
let size = read_u32(obj_file, &mut reader)?; let mut section_data = &reader[..];
let base_address = read_u32(obj_file, &mut reader)? as u64; 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) = let Some(out_section) =
sections.iter_mut().find(|s| s.orig_index == text_section_index) sections.iter_mut().find(|s| s.orig_index == text_section_index)
else { else {
// Skip line info for sections we filtered out // Skip line info for sections we filtered out
reader.set_position(start + size as u64);
continue; continue;
}; };
let end = start + size as u64; while !section_data.is_empty() {
while reader.position() < end { let line_number = read_u32(obj_file, &mut section_data)?;
let line_number = read_u32(obj_file, &mut reader)?; let statement_pos = read_u16(obj_file, &mut section_data)?;
let statement_pos = read_u16(obj_file, &mut reader)?;
if statement_pos != 0xFFFF { if statement_pos != 0xFFFF {
log::warn!("Unhandled statement pos {}", statement_pos); log::warn!("Unhandled statement pos {}", statement_pos);
} }
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); out_section.line_info.insert(base_address + address_delta, line_number);
log::debug!("Line: {:#x} -> {}", 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+ // DWARF 2+
#[cfg(feature = "dwarf")] #[cfg(feature = "dwarf")]
{ {
fn gimli_error(e: gimli::Error) -> anyhow::Error { anyhow::anyhow!("DWARF error: {e:?}") }
let dwarf_cow = gimli::DwarfSections::load(|id| { let dwarf_cow = gimli::DwarfSections::load(|id| {
Ok::<_, gimli::Error>( Ok::<_, gimli::Error>(
obj_file obj_file
.section_by_name(id.name()) .section_by_name(id.name())
.and_then(|section| section.uncompressed_data().ok()) .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() { let endian = match obj_file.endianness() {
object::Endianness::Little => gimli::RunTimeEndian::Little, object::Endianness::Little => gimli::RunTimeEndian::Little,
object::Endianness::Big => gimli::RunTimeEndian::Big, object::Endianness::Big => gimli::RunTimeEndian::Big,
}; };
let dwarf = dwarf_cow.borrow(|section| gimli::EndianSlice::new(section, endian)); let dwarf = dwarf_cow.borrow(|section| gimli::EndianSlice::new(section, endian));
let mut iter = dwarf.units(); let mut iter = dwarf.units();
if let Some(header) = iter.next()? { if let Some(header) = iter.next().map_err(gimli_error)? {
let unit = dwarf.unit(header)?; let unit = dwarf.unit(header).map_err(gimli_error)?;
if let Some(program) = unit.line_program.clone() { if let Some(program) = unit.line_program.clone() {
let mut text_sections = let mut text_sections =
obj_file.sections().filter(|s| s.kind() == SectionKind::Text); 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); .map(|s| &mut s.line_info);
let mut rows = program.rows(); let mut rows = program.rows();
while let Some((_header, row)) = rows.next_row()? { while let Some((_header, row)) = rows.next_row().map_err(gimli_error)? {
if let (Some(line), Some(lines)) = (row.line(), &mut lines) { if let (Some(line), Some(lines)) = (row.line(), &mut lines) {
lines.insert(row.address(), line.get() as u32); 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"); log::warn!("Multiple units found in DWARF data, only processing the first");
} }
} }
@ -638,7 +643,7 @@ fn combine_sections(section: ObjSection, combine: ObjSection) -> Result<ObjSecti
} }
fn combine_data_sections(sections: &mut Vec<ObjSection>) -> Result<()> { fn combine_data_sections(sections: &mut Vec<ObjSection>) -> Result<()> {
let names_to_combine: HashSet<_> = sections let names_to_combine: BTreeSet<_> = sections
.iter() .iter()
.filter(|s| s.kind == ObjSectionKind::Data) .filter(|s| s.kind == ObjSectionKind::Data)
.map(|s| s.name.clone()) .map(|s| s.name.clone())
@ -677,14 +682,15 @@ fn combine_data_sections(sections: &mut Vec<ObjSection>) -> Result<()> {
Ok(()) Ok(())
} }
pub fn read(obj_path: &Path, config: &DiffObjConfig) -> Result<ObjInfo> { #[cfg(feature = "std")]
pub fn read(obj_path: &std::path::Path, config: &DiffObjConfig) -> Result<ObjInfo> {
let (data, timestamp) = { let (data, timestamp) = {
let file = fs::File::open(obj_path)?; let file = std::fs::File::open(obj_path)?;
let timestamp = FileTime::from_last_modification_time(&file.metadata()?); let timestamp = filetime::FileTime::from_last_modification_time(&file.metadata()?);
(unsafe { memmap2::Mmap::map(&file) }?, timestamp) (unsafe { memmap2::Mmap::map(&file) }?, timestamp)
}; };
let mut obj = parse(&data, config)?; 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); obj.timestamp = Some(timestamp);
Ok(obj) Ok(obj)
} }
@ -710,7 +716,7 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
} }
let mut sections = filter_sections(&obj_file, split_meta.as_ref())?; let mut sections = filter_sections(&obj_file, split_meta.as_ref())?;
let mut section_name_counts: HashMap<String, u32> = HashMap::new(); let mut section_name_counts: BTreeMap<String, u32> = BTreeMap::new();
for section in &mut sections { for section in &mut sections {
section.symbols = symbols_by_section( section.symbols = symbols_by_section(
arch.as_ref(), arch.as_ref(),
@ -733,12 +739,21 @@ pub fn parse(data: &[u8], config: &DiffObjConfig) -> Result<ObjInfo> {
} }
line_info(&obj_file, &mut sections, data)?; line_info(&obj_file, &mut sections, data)?;
let common = common_symbols(arch.as_ref(), &obj_file, split_meta.as_ref())?; let common = common_symbols(arch.as_ref(), &obj_file, split_meta.as_ref())?;
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<bool> { #[cfg(feature = "std")]
pub fn has_function(obj_path: &std::path::Path, symbol_name: &str) -> Result<bool> {
let data = { let data = {
let file = fs::File::open(obj_path)?; let file = std::fs::File::open(obj_path)?;
unsafe { memmap2::Mmap::map(&file) }? unsafe { memmap2::Mmap::map(&file) }?
}; };
Ok(File::parse(&*data)? Ok(File::parse(&*data)?

View File

@ -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}; use object::{elf::SHT_NOTE, Endian, ObjectSection};
pub const SPLITMETA_SECTION: &str = ".note.split"; 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"); const NT_SPLIT_VIRTUAL_ADDRESSES: u32 = u32::from_be_bytes(*b"VIRT");
impl SplitMeta { impl SplitMeta {
pub fn from_section<E>(section: object::Section, e: E, is_64: bool) -> io::Result<Self> pub fn from_section<E>(section: object::Section, e: E, is_64: bool) -> Result<Self>
where E: Endian { where E: Endian {
let mut result = SplitMeta::default(); 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)?; let mut iter = NoteIterator::new(data.as_ref(), section.align(), e, is_64)?;
while let Some(note) = iter.next(e)? { while let Some(note) = iter.next(e)? {
if note.name != ELF_NOTE_SPLIT { if note.name != ELF_NOTE_SPLIT {
@ -39,19 +40,18 @@ impl SplitMeta {
match note.n_type { match note.n_type {
NT_SPLIT_GENERATOR => { NT_SPLIT_GENERATOR => {
let string = String::from_utf8(note.desc.to_vec()) 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); result.generator = Some(string);
} }
NT_SPLIT_MODULE_NAME => { NT_SPLIT_MODULE_NAME => {
let string = String::from_utf8(note.desc.to_vec()) 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); result.module_name = Some(string);
} }
NT_SPLIT_MODULE_ID => { NT_SPLIT_MODULE_ID => {
result.module_id = result.module_id = Some(e.read_u32_bytes(
Some(e.read_u32_bytes(note.desc.try_into().map_err(|_| { note.desc.try_into().map_err(|_| anyhow!("Invalid module ID size"))?,
io::Error::new(io::ErrorKind::InvalidData, "Invalid module ID size") ));
})?));
} }
NT_SPLIT_VIRTUAL_ADDRESSES => { NT_SPLIT_VIRTUAL_ADDRESSES => {
let vec = if is_64 { let vec = if is_64 {
@ -79,10 +79,11 @@ impl SplitMeta {
Ok(result) Ok(result)
} }
pub fn to_writer<E, W>(&self, writer: &mut W, e: E, is_64: bool) -> io::Result<()> #[cfg(feature = "std")]
pub fn to_writer<E, W>(&self, writer: &mut W, e: E, is_64: bool) -> std::io::Result<()>
where where
E: Endian, E: Endian,
W: Write + ?Sized, W: std::io::Write + ?Sized,
{ {
if let Some(generator) = &self.generator { if let Some(generator) = &self.generator {
write_note_header(writer, e, NT_SPLIT_GENERATOR, generator.len())?; 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. /// Convert an object::read::Error to a String.
fn object_io_error(err: object::read::Error) -> io::Error { #[inline]
io::Error::new(io::ErrorKind::InvalidData, err) fn object_error(err: object::read::Error) -> anyhow::Error { anyhow::Error::new(err) }
}
/// An ELF note entry. /// An ELF note entry.
struct Note<'data> { struct Note<'data> {
@ -161,27 +161,27 @@ where E: Endian
impl<'data, E> NoteIterator<'data, E> impl<'data, E> NoteIterator<'data, E>
where E: Endian where E: Endian
{ {
fn new(data: &'data [u8], align: u64, e: E, is_64: bool) -> io::Result<Self> { fn new(data: &'data [u8], align: u64, e: E, is_64: bool) -> Result<Self> {
Ok(if is_64 { Ok(if is_64 {
NoteIterator::B64( 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 { } else {
NoteIterator::B32( NoteIterator::B32(
object::read::elf::NoteIterator::new(e, align as u32, data) 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<Option<Note<'data>>> { fn next(&mut self, e: E) -> Result<Option<Note<'data>>> {
match self { 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), n_type: note.n_type(e),
name: note.name(), name: note.name(),
desc: note.desc(), 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), n_type: note.n_type(e),
name: note.name(), name: note.name(),
desc: note.desc(), desc: note.desc(),
@ -192,7 +192,8 @@ where E: Endian
fn align_size_to_4(size: usize) -> usize { (size + 3) & !3 } fn align_size_to_4(size: usize) -> usize { (size + 3) & !3 }
fn align_data_to_4<W: Write + ?Sized>(writer: &mut W, len: usize) -> io::Result<()> { #[cfg(feature = "std")]
fn align_data_to_4<W: std::io::Write + ?Sized>(writer: &mut W, len: usize) -> std::io::Result<()> {
const ALIGN_BYTES: &[u8] = &[0; 4]; const ALIGN_BYTES: &[u8] = &[0; 4];
if len % 4 != 0 { if len % 4 != 0 {
writer.write_all(&ALIGN_BYTES[..4 - len % 4])?; writer.write_all(&ALIGN_BYTES[..4 - len % 4])?;
@ -208,10 +209,11 @@ fn align_data_to_4<W: Write + ?Sized>(writer: &mut W, len: usize) -> io::Result<
// Desc | variable size, padded to a 4 byte boundary // Desc | variable size, padded to a 4 byte boundary
const NOTE_HEADER_SIZE: usize = 12 + ((ELF_NOTE_SPLIT.len() + 4) & !3); const NOTE_HEADER_SIZE: usize = 12 + ((ELF_NOTE_SPLIT.len() + 4) & !3);
fn write_note_header<E, W>(writer: &mut W, e: E, kind: u32, desc_len: usize) -> io::Result<()> #[cfg(feature = "std")]
fn write_note_header<E, W>(writer: &mut W, e: E, kind: u32, desc_len: usize) -> std::io::Result<()>
where where
E: Endian, 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(ELF_NOTE_SPLIT.len() as u32 + 1))?; // Name Size
writer.write_all(&e.write_u32_bytes(desc_len as u32))?; // Desc Size writer.write_all(&e.write_u32_bytes(desc_len as u32))?; // Desc Size

View File

@ -1,18 +1,15 @@
use std::{ use alloc::format;
fmt::{LowerHex, UpperHex}, use core::fmt;
io::Read,
};
use anyhow::Result; use anyhow::Result;
use byteorder::{NativeEndian, ReadBytesExt};
use num_traits::PrimInt; use num_traits::PrimInt;
use object::{Endian, Object}; use object::{Endian, Object};
// https://stackoverflow.com/questions/44711012/how-do-i-format-a-signed-integer-to-a-sign-aware-hexadecimal-representation // https://stackoverflow.com/questions/44711012/how-do-i-format-a-signed-integer-to-a-sign-aware-hexadecimal-representation
pub struct ReallySigned<N: PrimInt>(pub(crate) N); pub struct ReallySigned<N: PrimInt>(pub(crate) N);
impl<N: PrimInt> LowerHex for ReallySigned<N> { impl<N: PrimInt> fmt::LowerHex for ReallySigned<N> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let num = self.0.to_i64().unwrap(); let num = self.0.to_i64().unwrap();
let prefix = if f.alternate() { "0x" } else { "" }; let prefix = if f.alternate() { "0x" } else { "" };
let bare_hex = format!("{:x}", num.abs()); let bare_hex = format!("{:x}", num.abs());
@ -20,8 +17,8 @@ impl<N: PrimInt> LowerHex for ReallySigned<N> {
} }
} }
impl<N: PrimInt> UpperHex for ReallySigned<N> { impl<N: PrimInt> fmt::UpperHex for ReallySigned<N> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let num = self.0.to_i64().unwrap(); let num = self.0.to_i64().unwrap();
let prefix = if f.alternate() { "0x" } else { "" }; let prefix = if f.alternate() { "0x" } else { "" };
let bare_hex = format!("{:X}", num.abs()); let bare_hex = format!("{:X}", num.abs());
@ -29,10 +26,18 @@ impl<N: PrimInt> UpperHex for ReallySigned<N> {
} }
} }
pub fn read_u32<R: Read>(obj_file: &object::File, reader: &mut R) -> Result<u32> { pub fn read_u32(obj_file: &object::File, reader: &mut &[u8]) -> Result<u32> {
Ok(obj_file.endianness().read_u32(reader.read_u32::<NativeEndian>()?)) 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<R: Read>(obj_file: &object::File, reader: &mut R) -> Result<u16> { pub fn read_u16(obj_file: &object::File, reader: &mut &[u8]) -> Result<u16> {
Ok(obj_file.endianness().read_u16(reader.read_u16::<NativeEndian>()?)) 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))
} }

View File

@ -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<diff::DiffObjConfig>);
impl GuestTypes for Component {
type DiffConfig = ResourceDiffConfig;
type Object = obj::ObjInfo;
fn run_diff(
left: Option<ObjectBorrow>,
right: Option<ObjectBorrow>,
diff_config: DiffConfigBorrow,
) -> Result<Vec<u8>, String> {
let diff_config = diff_config.get::<ResourceDiffConfig>().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<String, String> {
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<u8>, diff_config: DiffConfigBorrow) -> Result<Object, String> {
let diff_config = diff_config.get::<ResourceDiffConfig>().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<DiffResult> {
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);

View File

@ -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
//! <https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md>
//!
//! 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
}

View File

@ -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<spin::Mutex<()>, 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 {} }

View File

@ -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<string, string>;
}
resource object {
parse: static func(
data: list<u8>,
config: borrow<diff-config>,
) -> result<object, string>;
}
run-diff: func(
left: option<borrow<object>>,
right: option<borrow<object>>,
config: borrow<diff-config>,
) -> result<list<u8>, string>;
}
world api {
export diff;
export init: func() -> result<_, string>;
export version: func() -> string;
}

View File

@ -29,7 +29,7 @@ bytes = "1.9"
cfg-if = "1.0" cfg-if = "1.0"
const_format = "0.2" const_format = "0.2"
cwdemangle = "1.0" cwdemangle = "1.0"
cwextab = "1.0" cwextab = { version = "1.0", git = "https://github.com/encounter/cwextab.git" }
dirs = "5.0" dirs = "5.0"
egui = "0.30" egui = "0.30"
egui_extras = "0.30" egui_extras = "0.30"
@ -51,6 +51,7 @@ serde_json = "1.0"
shell-escape = "0.1" shell-escape = "0.1"
strum = { version = "0.26", features = ["derive"] } strum = { version = "0.26", features = ["derive"] }
time = { version = "0.3", features = ["formatting", "local-offset"] } time = { version = "0.3", features = ["formatting", "local-offset"] }
typed-path = "0.10"
# Keep version in sync with egui # Keep version in sync with egui
[dependencies.eframe] [dependencies.eframe]

View File

@ -1,4 +1,5 @@
use std::{ use std::{
collections::BTreeMap,
default::Default, default::Default,
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
@ -15,13 +16,15 @@ use globset::Glob;
use objdiff_core::{ use objdiff_core::{
build::watcher::{create_watcher, Watcher}, build::watcher::{create_watcher, Watcher},
config::{ config::{
build_globset, default_watch_patterns, save_project_config, ProjectConfig, build_globset, default_watch_patterns, path::platform_path_serde_option,
ProjectConfigInfo, ProjectObject, ScratchConfig, SymbolMappings, DEFAULT_WATCH_PATTERNS, save_project_config, ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig,
DEFAULT_WATCH_PATTERNS,
}, },
diff::DiffObjConfig, diff::DiffObjConfig,
jobs::{Job, JobQueue, JobResult}, jobs::{Job, JobQueue, JobResult},
}; };
use time::UtcOffset; use time::UtcOffset;
use typed_path::{Utf8PlatformPath, Utf8PlatformPathBuf};
use crate::{ use crate::{
app_config::{deserialize_config, AppConfigVersion}, app_config::{deserialize_config, AppConfigVersion},
@ -90,26 +93,57 @@ impl Default for ViewState {
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ObjectConfig { pub struct ObjectConfig {
pub name: String, pub name: String,
pub target_path: Option<PathBuf>, #[serde(default, with = "platform_path_serde_option")]
pub base_path: Option<PathBuf>, pub target_path: Option<Utf8PlatformPathBuf>,
#[serde(default, with = "platform_path_serde_option")]
pub base_path: Option<Utf8PlatformPathBuf>,
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>, pub complete: Option<bool>,
pub scratch: Option<ScratchConfig>,
pub source_path: Option<String>,
#[serde(default)] #[serde(default)]
pub symbol_mappings: SymbolMappings, pub hidden: bool,
pub scratch: Option<ScratchConfig>,
#[serde(default, with = "platform_path_serde_option")]
pub source_path: Option<Utf8PlatformPathBuf>,
#[serde(default)]
pub symbol_mappings: BTreeMap<String, String>,
} }
impl From<&ProjectObject> for ObjectConfig { impl ObjectConfig {
fn from(object: &ProjectObject) -> Self { 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 { Self {
name: object.name().to_string(), name: object.name().to_string(),
target_path: object.target_path.clone(), target_path,
base_path: object.base_path.clone(), base_path,
reverse_fn_order: object.reverse_fn_order(), reverse_fn_order: object.reverse_fn_order(),
complete: object.complete(), complete: object.complete(),
hidden: object.hidden(),
scratch: object.scratch.clone(), scratch: object.scratch.clone(),
source_path: object.source_path().cloned(), source_path,
symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(), symbol_mappings: object.symbol_mappings.clone().unwrap_or_default(),
} }
} }
@ -120,7 +154,7 @@ fn bool_true() -> bool { true }
pub struct AppState { pub struct AppState {
pub config: AppConfig, pub config: AppConfig,
pub objects: Vec<ProjectObject>, pub objects: Vec<ObjectConfig>,
pub object_nodes: Vec<ProjectObjectNode>, pub object_nodes: Vec<ProjectObjectNode>,
pub watcher_change: bool, pub watcher_change: bool,
pub config_change: bool, pub config_change: bool,
@ -170,12 +204,12 @@ pub struct AppConfig {
pub custom_args: Option<Vec<String>>, pub custom_args: Option<Vec<String>>,
#[serde(default)] #[serde(default)]
pub selected_wsl_distro: Option<String>, pub selected_wsl_distro: Option<String>,
#[serde(default)] #[serde(default, with = "platform_path_serde_option")]
pub project_dir: Option<PathBuf>, pub project_dir: Option<Utf8PlatformPathBuf>,
#[serde(default)] #[serde(default, with = "platform_path_serde_option")]
pub target_obj_dir: Option<PathBuf>, pub target_obj_dir: Option<Utf8PlatformPathBuf>,
#[serde(default)] #[serde(default, with = "platform_path_serde_option")]
pub base_obj_dir: Option<PathBuf>, pub base_obj_dir: Option<Utf8PlatformPathBuf>,
#[serde(default)] #[serde(default)]
pub selected_obj: Option<ObjectConfig>, pub selected_obj: Option<ObjectConfig>,
#[serde(default = "bool_true")] #[serde(default = "bool_true")]
@ -189,7 +223,7 @@ pub struct AppConfig {
#[serde(default = "default_watch_patterns")] #[serde(default = "default_watch_patterns")]
pub watch_patterns: Vec<Glob>, pub watch_patterns: Vec<Glob>,
#[serde(default)] #[serde(default)]
pub recent_projects: Vec<PathBuf>, pub recent_projects: Vec<String>,
#[serde(default)] #[serde(default)]
pub diff_obj_config: DiffObjConfig, pub diff_obj_config: DiffObjConfig,
} }
@ -217,12 +251,12 @@ impl Default for AppConfig {
} }
impl AppState { 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); self.config.recent_projects.retain(|p| p != &path);
if self.config.recent_projects.len() > 9 { if self.config.recent_projects.len() > 9 {
self.config.recent_projects.truncate(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.project_dir = Some(path);
self.config.target_obj_dir = None; self.config.target_obj_dir = None;
self.config.base_obj_dir = None; self.config.base_obj_dir = None;
@ -240,7 +274,7 @@ impl AppState {
self.selecting_right = None; 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.target_obj_dir = Some(path);
self.config.selected_obj = None; self.config.selected_obj = None;
self.obj_change = true; self.obj_change = true;
@ -249,7 +283,7 @@ impl AppState {
self.selecting_right = None; 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.base_obj_dir = Some(path);
self.config.selected_obj = None; self.config.selected_obj = None;
self.obj_change = true; self.obj_change = true;
@ -360,14 +394,8 @@ impl AppState {
Some(object.symbol_mappings.clone()) Some(object.symbol_mappings.clone())
}; };
} }
if let Some(existing) = if let Some(existing) = self.objects.iter_mut().find(|u| u.name == object.name) {
self.objects.iter_mut().find(|u| u.name.as_ref().is_some_and(|n| n == &object.name)) existing.symbol_mappings = object.symbol_mappings.clone();
{
existing.symbol_mappings = if object.symbol_mappings.is_empty() {
None
} else {
Some(object.symbol_mappings.clone())
};
} }
} }
// Save the updated project config // Save the updated project config
@ -530,7 +558,12 @@ impl App {
match build_globset(&state.config.watch_patterns) match build_globset(&state.config.watch_patterns)
.map_err(anyhow::Error::new) .map_err(anyhow::Error::new)
.and_then(|globset| { .and_then(|globset| {
create_watcher(self.modified.clone(), project_dir, globset, egui_waker(ctx)) create_watcher(
self.modified.clone(),
project_dir.as_ref(),
globset,
egui_waker(ctx),
)
.map_err(anyhow::Error::new) .map_err(anyhow::Error::new)
}) { }) {
Ok(watcher) => self.watcher = Some(watcher), Ok(watcher) => self.watcher = Some(watcher),
@ -672,8 +705,11 @@ impl eframe::App for App {
}; };
ui.separator(); ui.separator();
for path in recent_projects { for path in recent_projects {
if ui.button(format!("{}", path.display())).clicked() { if ui.button(&path).clicked() {
state.write().unwrap().set_project_dir(path); state
.write()
.unwrap()
.set_project_dir(Utf8PlatformPathBuf::from(path));
ui.close_menu(); ui.close_menu();
} }
} }
@ -776,8 +812,8 @@ impl eframe::App for App {
} }
#[inline] #[inline]
fn file_modified(path: &Path, last_ts: FileTime) -> bool { fn file_modified<P: AsRef<Path>>(path: P, last_ts: FileTime) -> bool {
if let Ok(metadata) = fs::metadata(path) { if let Ok(metadata) = fs::metadata(path.as_ref()) {
FileTime::from_last_modification_time(&metadata) != last_ts FileTime::from_last_modification_time(&metadata) != last_ts
} else { } else {
false false

View File

@ -1,14 +1,15 @@
use std::path::PathBuf; use std::collections::BTreeMap;
use eframe::Storage; use eframe::Storage;
use globset::Glob; use globset::Glob;
use objdiff_core::{ use objdiff_core::{
config::{ScratchConfig, SymbolMappings}, config::ScratchConfig,
diff::{ diff::{
ArmArchVersion, ArmR9Usage, DiffObjConfig, FunctionRelocDiffs, MipsAbi, MipsInstrCategory, ArmArchVersion, ArmR9Usage, DiffObjConfig, FunctionRelocDiffs, MipsAbi, MipsInstrCategory,
X86Formatter, X86Formatter,
}, },
}; };
use typed_path::{Utf8PlatformPathBuf, Utf8UnixPathBuf};
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY}; use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
@ -62,7 +63,7 @@ pub struct ScratchConfigV2 {
#[serde(default)] #[serde(default)]
pub c_flags: Option<String>, pub c_flags: Option<String>,
#[serde(default)] #[serde(default)]
pub ctx_path: Option<PathBuf>, pub ctx_path: Option<String>,
#[serde(default)] #[serde(default)]
pub build_ctx: Option<bool>, pub build_ctx: Option<bool>,
#[serde(default)] #[serde(default)]
@ -75,7 +76,7 @@ impl ScratchConfigV2 {
platform: self.platform, platform: self.platform,
compiler: self.compiler, compiler: self.compiler,
c_flags: self.c_flags, c_flags: self.c_flags,
ctx_path: self.ctx_path, ctx_path: self.ctx_path.map(Utf8UnixPathBuf::from),
build_ctx: self.build_ctx, build_ctx: self.build_ctx,
preset_id: self.preset_id, preset_id: self.preset_id,
} }
@ -85,26 +86,27 @@ impl ScratchConfigV2 {
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV2 { pub struct ObjectConfigV2 {
pub name: String, pub name: String,
pub target_path: Option<PathBuf>, pub target_path: Option<String>,
pub base_path: Option<PathBuf>, pub base_path: Option<String>,
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>, pub complete: Option<bool>,
pub scratch: Option<ScratchConfigV2>, pub scratch: Option<ScratchConfigV2>,
pub source_path: Option<String>, pub source_path: Option<String>,
#[serde(default)] #[serde(default)]
pub symbol_mappings: SymbolMappings, pub symbol_mappings: BTreeMap<String, String>,
} }
impl ObjectConfigV2 { impl ObjectConfigV2 {
fn into_config(self) -> ObjectConfig { fn into_config(self) -> ObjectConfig {
ObjectConfig { ObjectConfig {
name: self.name, name: self.name,
target_path: self.target_path, target_path: self.target_path.map(Utf8PlatformPathBuf::from),
base_path: self.base_path, base_path: self.base_path.map(Utf8PlatformPathBuf::from),
reverse_fn_order: self.reverse_fn_order, reverse_fn_order: self.reverse_fn_order,
complete: self.complete, complete: self.complete,
hidden: false,
scratch: self.scratch.map(|scratch| scratch.into_config()), scratch: self.scratch.map(|scratch| scratch.into_config()),
source_path: self.source_path, source_path: None,
symbol_mappings: self.symbol_mappings, symbol_mappings: self.symbol_mappings,
} }
} }
@ -120,11 +122,11 @@ pub struct AppConfigV2 {
#[serde(default)] #[serde(default)]
pub selected_wsl_distro: Option<String>, pub selected_wsl_distro: Option<String>,
#[serde(default)] #[serde(default)]
pub project_dir: Option<PathBuf>, pub project_dir: Option<String>,
#[serde(default)] #[serde(default)]
pub target_obj_dir: Option<PathBuf>, pub target_obj_dir: Option<String>,
#[serde(default)] #[serde(default)]
pub base_obj_dir: Option<PathBuf>, pub base_obj_dir: Option<String>,
#[serde(default)] #[serde(default)]
pub selected_obj: Option<ObjectConfigV2>, pub selected_obj: Option<ObjectConfigV2>,
#[serde(default = "bool_true")] #[serde(default = "bool_true")]
@ -138,7 +140,7 @@ pub struct AppConfigV2 {
#[serde(default)] #[serde(default)]
pub watch_patterns: Vec<Glob>, pub watch_patterns: Vec<Glob>,
#[serde(default)] #[serde(default)]
pub recent_projects: Vec<PathBuf>, pub recent_projects: Vec<String>,
#[serde(default)] #[serde(default)]
pub diff_obj_config: DiffObjConfigV1, pub diff_obj_config: DiffObjConfigV1,
} }
@ -150,9 +152,9 @@ impl AppConfigV2 {
custom_make: self.custom_make, custom_make: self.custom_make,
custom_args: self.custom_args, custom_args: self.custom_args,
selected_wsl_distro: self.selected_wsl_distro, selected_wsl_distro: self.selected_wsl_distro,
project_dir: self.project_dir, project_dir: self.project_dir.map(Utf8PlatformPathBuf::from),
target_obj_dir: self.target_obj_dir, target_obj_dir: self.target_obj_dir.map(Utf8PlatformPathBuf::from),
base_obj_dir: self.base_obj_dir, base_obj_dir: self.base_obj_dir.map(Utf8PlatformPathBuf::from),
selected_obj: self.selected_obj.map(|obj| obj.into_config()), selected_obj: self.selected_obj.map(|obj| obj.into_config()),
build_base: self.build_base, build_base: self.build_base,
build_target: self.build_target, build_target: self.build_target,
@ -175,7 +177,7 @@ pub struct ScratchConfigV1 {
#[serde(default)] #[serde(default)]
pub c_flags: Option<String>, pub c_flags: Option<String>,
#[serde(default)] #[serde(default)]
pub ctx_path: Option<PathBuf>, pub ctx_path: Option<String>,
#[serde(default)] #[serde(default)]
pub build_ctx: bool, pub build_ctx: bool,
} }
@ -186,7 +188,7 @@ impl ScratchConfigV1 {
platform: self.platform, platform: self.platform,
compiler: self.compiler, compiler: self.compiler,
c_flags: self.c_flags, 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), build_ctx: self.build_ctx.then_some(true),
preset_id: None, preset_id: None,
} }
@ -196,8 +198,8 @@ impl ScratchConfigV1 {
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV1 { pub struct ObjectConfigV1 {
pub name: String, pub name: String,
pub target_path: Option<PathBuf>, pub target_path: Option<String>,
pub base_path: Option<PathBuf>, pub base_path: Option<String>,
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>, pub complete: Option<bool>,
pub scratch: Option<ScratchConfigV1>, pub scratch: Option<ScratchConfigV1>,
@ -208,12 +210,12 @@ impl ObjectConfigV1 {
fn into_config(self) -> ObjectConfig { fn into_config(self) -> ObjectConfig {
ObjectConfig { ObjectConfig {
name: self.name, name: self.name,
target_path: self.target_path, target_path: self.target_path.map(Utf8PlatformPathBuf::from),
base_path: self.base_path, base_path: self.base_path.map(Utf8PlatformPathBuf::from),
reverse_fn_order: self.reverse_fn_order, reverse_fn_order: self.reverse_fn_order,
complete: self.complete, complete: self.complete,
scratch: self.scratch.map(|scratch| scratch.into_config()), scratch: self.scratch.map(|scratch| scratch.into_config()),
source_path: self.source_path, source_path: None,
..Default::default() ..Default::default()
} }
} }
@ -298,11 +300,11 @@ pub struct AppConfigV1 {
#[serde(default)] #[serde(default)]
pub selected_wsl_distro: Option<String>, pub selected_wsl_distro: Option<String>,
#[serde(default)] #[serde(default)]
pub project_dir: Option<PathBuf>, pub project_dir: Option<String>,
#[serde(default)] #[serde(default)]
pub target_obj_dir: Option<PathBuf>, pub target_obj_dir: Option<String>,
#[serde(default)] #[serde(default)]
pub base_obj_dir: Option<PathBuf>, pub base_obj_dir: Option<String>,
#[serde(default)] #[serde(default)]
pub selected_obj: Option<ObjectConfigV1>, pub selected_obj: Option<ObjectConfigV1>,
#[serde(default = "bool_true")] #[serde(default = "bool_true")]
@ -316,7 +318,7 @@ pub struct AppConfigV1 {
#[serde(default)] #[serde(default)]
pub watch_patterns: Vec<Glob>, pub watch_patterns: Vec<Glob>,
#[serde(default)] #[serde(default)]
pub recent_projects: Vec<PathBuf>, pub recent_projects: Vec<String>,
#[serde(default)] #[serde(default)]
pub diff_obj_config: DiffObjConfigV1, pub diff_obj_config: DiffObjConfigV1,
} }
@ -328,9 +330,9 @@ impl AppConfigV1 {
custom_make: self.custom_make, custom_make: self.custom_make,
custom_args: self.custom_args, custom_args: self.custom_args,
selected_wsl_distro: self.selected_wsl_distro, selected_wsl_distro: self.selected_wsl_distro,
project_dir: self.project_dir, project_dir: self.project_dir.map(Utf8PlatformPathBuf::from),
target_obj_dir: self.target_obj_dir, target_obj_dir: self.target_obj_dir.map(Utf8PlatformPathBuf::from),
base_obj_dir: self.base_obj_dir, base_obj_dir: self.base_obj_dir.map(Utf8PlatformPathBuf::from),
selected_obj: self.selected_obj.map(|obj| obj.into_config()), selected_obj: self.selected_obj.map(|obj| obj.into_config()),
build_base: self.build_base, build_base: self.build_base,
build_target: self.build_target, build_target: self.build_target,
@ -347,8 +349,8 @@ impl AppConfigV1 {
#[derive(serde::Deserialize, serde::Serialize)] #[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV0 { pub struct ObjectConfigV0 {
pub name: String, pub name: String,
pub target_path: PathBuf, pub target_path: String,
pub base_path: PathBuf, pub base_path: String,
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
} }
@ -356,8 +358,8 @@ impl ObjectConfigV0 {
fn into_config(self) -> ObjectConfig { fn into_config(self) -> ObjectConfig {
ObjectConfig { ObjectConfig {
name: self.name, name: self.name,
target_path: Some(self.target_path), target_path: Some(Utf8PlatformPathBuf::from(self.target_path)),
base_path: Some(self.base_path), base_path: Some(Utf8PlatformPathBuf::from(self.base_path)),
reverse_fn_order: self.reverse_fn_order, reverse_fn_order: self.reverse_fn_order,
..Default::default() ..Default::default()
} }
@ -368,9 +370,9 @@ impl ObjectConfigV0 {
pub struct AppConfigV0 { pub struct AppConfigV0 {
pub custom_make: Option<String>, pub custom_make: Option<String>,
pub selected_wsl_distro: Option<String>, pub selected_wsl_distro: Option<String>,
pub project_dir: Option<PathBuf>, pub project_dir: Option<String>,
pub target_obj_dir: Option<PathBuf>, pub target_obj_dir: Option<String>,
pub base_obj_dir: Option<PathBuf>, pub base_obj_dir: Option<String>,
pub selected_obj: Option<ObjectConfigV0>, pub selected_obj: Option<ObjectConfigV0>,
pub build_target: bool, pub build_target: bool,
pub auto_update_check: bool, pub auto_update_check: bool,
@ -383,9 +385,9 @@ impl AppConfigV0 {
AppConfig { AppConfig {
custom_make: self.custom_make, custom_make: self.custom_make,
selected_wsl_distro: self.selected_wsl_distro, selected_wsl_distro: self.selected_wsl_distro,
project_dir: self.project_dir, project_dir: self.project_dir.map(Utf8PlatformPathBuf::from),
target_obj_dir: self.target_obj_dir, target_obj_dir: self.target_obj_dir.map(Utf8PlatformPathBuf::from),
base_obj_dir: self.base_obj_dir, base_obj_dir: self.base_obj_dir.map(Utf8PlatformPathBuf::from),
selected_obj: self.selected_obj.map(|obj| obj.into_config()), selected_obj: self.selected_obj.map(|obj| obj.into_config()),
build_target: self.build_target, build_target: self.build_target,
auto_update_check: self.auto_update_check, auto_update_check: self.auto_update_check,

View File

@ -1,8 +1,7 @@
use std::path::{Component, Path};
use anyhow::Result; use anyhow::Result;
use globset::Glob; 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}; use crate::app::{AppState, ObjectConfig};
@ -47,32 +46,19 @@ fn find_dir<'a>(
unreachable!(); unreachable!();
} }
fn build_nodes( fn build_nodes(units: &mut [ObjectConfig]) -> Vec<ProjectObjectNode> {
units: &mut [ProjectObject],
project_dir: &Path,
target_obj_dir: Option<&Path>,
base_obj_dir: Option<&Path>,
) -> Vec<ProjectObjectNode> {
let mut nodes = vec![]; let mut nodes = vec![];
for (idx, unit) in units.iter_mut().enumerate() { 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 mut out_nodes = &mut nodes;
let path = if let Some(name) = &unit.name { let path = Utf8UnixPath::new(&unit.name);
Path::new(name)
} else if let Some(path) = &unit.path {
path
} else {
continue;
};
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
for component in parent.components() { for component in parent.components() {
if let Component::Normal(name) = component { if let Utf8UnixComponent::Normal(name) = component {
let name = name.to_str().unwrap();
out_nodes = find_dir(name, out_nodes); 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)); out_nodes.push(ProjectObjectNode::Unit(filename, idx));
} }
// Within the top-level module directories, join paths. Leave the // 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 { let Some(project_dir) = &state.config.project_dir else {
return Ok(()); 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?; let project_config = result?;
state.config.custom_make = project_config.custom_make.clone(); state.config.custom_make = project_config.custom_make.clone();
state.config.custom_args = project_config.custom_args.clone(); state.config.custom_args = project_config.custom_args.clone();
state.config.target_obj_dir = state.config.target_obj_dir = project_config
project_config.target_dir.as_deref().map(|p| project_dir.join(p)); .target_dir
state.config.base_obj_dir = project_config.base_dir.as_deref().map(|p| project_dir.join(p)); .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_base = project_config.build_base.unwrap_or(true);
state.config.build_target = project_config.build_target.unwrap_or(false); state.config.build_target = project_config.build_target.unwrap_or(false);
if let Some(watch_patterns) = &project_config.watch_patterns { 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(); DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
} }
state.watcher_change = true; state.watcher_change = true;
state.objects = project_config.units.clone().unwrap_or_default(); state.objects = project_config
state.object_nodes = build_nodes( .units
&mut state.objects, .as_deref()
.unwrap_or_default()
.iter()
.map(|o| {
ObjectConfig::new(
o,
project_dir, project_dir,
state.config.target_obj_dir.as_deref(), state.config.target_obj_dir.as_deref(),
state.config.base_obj_dir.as_deref(), state.config.base_obj_dir.as_deref(),
); )
})
.collect::<Vec<_>>();
state.object_nodes = build_nodes(&mut state.objects);
state.current_project_config = Some(project_config); state.current_project_config = Some(project_config);
state.project_config_info = Some(info); state.project_config_info = Some(info);
// Reload selected object // Reload selected object
if let Some(selected_obj) = &state.config.selected_obj { if let Some(selected_obj) = &state.config.selected_obj {
if let Some(obj) = state.objects.iter().find(|o| o.name() == selected_obj.name) { if let Some(obj) = state.objects.iter().find(|o| o.name == selected_obj.name) {
let config = ObjectConfig::from(obj); state.set_selected_obj(obj.clone());
state.set_selected_obj(config);
} else { } else {
state.clear_selected_obj(); state.clear_selected_obj();
} }

View File

@ -73,7 +73,7 @@ fn create_scratch_config(
platform: scratch_config.platform.clone().unwrap_or_default(), platform: scratch_config.platform.clone().unwrap_or_default(),
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(), compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
function_name, function_name,
target_obj: target_path.to_path_buf(), target_obj: target_path.clone(),
preset_id: scratch_config.preset_id, preset_id: scratch_config.preset_id,
}) })
} }

View File

@ -1,9 +1,6 @@
#[cfg(all(windows, feature = "wsl"))] #[cfg(all(windows, feature = "wsl"))]
use std::string::FromUtf16Error; use std::string::FromUtf16Error;
use std::{ use std::{mem::take, path::MAIN_SEPARATOR};
mem::take,
path::{Path, PathBuf, MAIN_SEPARATOR},
};
#[cfg(all(windows, feature = "wsl"))] #[cfg(all(windows, feature = "wsl"))]
use anyhow::{Context, Result}; use anyhow::{Context, Result};
@ -13,13 +10,14 @@ use egui::{
}; };
use globset::Glob; use globset::Glob;
use objdiff_core::{ use objdiff_core::{
config::{ProjectObject, DEFAULT_WATCH_PATTERNS}, config::{path::check_path_buf, DEFAULT_WATCH_PATTERNS},
diff::{ diff::{
ConfigEnum, ConfigEnumVariantInfo, ConfigPropertyId, ConfigPropertyKind, ConfigEnum, ConfigEnumVariantInfo, ConfigPropertyId, ConfigPropertyKind,
ConfigPropertyValue, CONFIG_GROUPS, ConfigPropertyValue, CONFIG_GROUPS,
}, },
jobs::{check_update::CheckUpdateResult, Job, JobQueue, JobResult}, jobs::{check_update::CheckUpdateResult, Job, JobQueue, JobResult},
}; };
use typed_path::Utf8PlatformPathBuf;
use crate::{ use crate::{
app::{AppConfig, AppState, AppStateRef, ObjectConfig}, app::{AppConfig, AppState, AppStateRef, ObjectConfig},
@ -89,7 +87,7 @@ impl ConfigViewState {
if let Ok(obj_path) = path.strip_prefix(base_dir) { if let Ok(obj_path) = path.strip_prefix(base_dir) {
let target_path = target_dir.join(obj_path); let target_path = target_dir.join(obj_path);
guard.set_selected_obj(ObjectConfig { guard.set_selected_obj(ObjectConfig {
name: obj_path.display().to_string(), name: obj_path.to_string(),
target_path: Some(target_path), target_path: Some(target_path),
base_path: Some(path), base_path: Some(path),
..Default::default() ..Default::default()
@ -97,7 +95,7 @@ impl ConfigViewState {
} else if let Ok(obj_path) = path.strip_prefix(target_dir) { } else if let Ok(obj_path) = path.strip_prefix(target_dir) {
let base_path = base_dir.join(obj_path); let base_path = base_dir.join(obj_path);
guard.set_selected_obj(ObjectConfig { guard.set_selected_obj(ObjectConfig {
name: obj_path.display().to_string(), name: obj_path.to_string(),
target_path: Some(path), target_path: Some(path),
base_path: Some(base_path), base_path: Some(base_path),
..Default::default() ..Default::default()
@ -169,10 +167,7 @@ pub fn config_ui(
) { ) {
let mut state_guard = state.write().unwrap(); let mut state_guard = state.write().unwrap();
let AppState { let AppState {
config: config: AppConfig { target_obj_dir, base_obj_dir, selected_obj, auto_update_check, .. },
AppConfig {
project_dir, target_obj_dir, base_obj_dir, selected_obj, auto_update_check, ..
},
objects, objects,
object_nodes, object_nodes,
.. ..
@ -223,9 +218,9 @@ pub fn config_ui(
} }
}); });
let selected_index = selected_obj.as_ref().and_then(|selected_obj| { let selected_index = selected_obj
objects.iter().position(|obj| obj.name.as_ref() == Some(&selected_obj.name)) .as_ref()
}); .and_then(|selected_obj| objects.iter().position(|obj| obj.name == selected_obj.name));
let mut new_selected_index = selected_index; let mut new_selected_index = selected_index;
if objects.is_empty() { if objects.is_empty() {
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { 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, config_state.show_hidden,
) )
}) { }) {
display_node( display_node(ui, &mut new_selected_index, objects, &node, appearance, node_open);
ui,
&mut new_selected_index,
project_dir.as_deref(),
objects,
&node,
appearance,
node_open,
);
} }
}); });
} }
if new_selected_index != selected_index { if new_selected_index != selected_index {
if let Some(idx) = new_selected_index { if let Some(idx) = new_selected_index {
// Will set obj_changed, which will trigger a rebuild // 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); state_guard.set_selected_obj(config);
} }
} }
@ -353,9 +340,8 @@ pub fn config_ui(
fn display_unit( fn display_unit(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<usize>, selected_obj: &mut Option<usize>,
project_dir: Option<&Path>,
name: &str, name: &str,
units: &[ProjectObject], units: &[ObjectConfig],
index: usize, index: usize,
appearance: &Appearance, appearance: &Appearance,
) { ) {
@ -363,7 +349,7 @@ fn display_unit(
let selected = *selected_obj == Some(index); let selected = *selected_obj == Some(index);
let color = if selected { let color = if selected {
appearance.emphasized_text_color appearance.emphasized_text_color
} else if let Some(complete) = object.complete() { } else if let Some(complete) = object.complete {
if complete { if complete {
appearance.insert_color appearance.insert_color
} else { } else {
@ -382,26 +368,22 @@ fn display_unit(
.color(color), .color(color),
) )
.ui(ui); .ui(ui);
if get_source_path(project_dir, object).is_some() { if object.source_path.is_some() {
response.context_menu(|ui| object_context_ui(ui, object, project_dir)); response.context_menu(|ui| object_context_ui(ui, object));
} }
if response.clicked() { if response.clicked() {
*selected_obj = Some(index); *selected_obj = Some(index);
} }
} }
fn get_source_path(project_dir: Option<&Path>, object: &ProjectObject) -> Option<PathBuf> { fn object_context_ui(ui: &mut egui::Ui, object: &ObjectConfig) {
project_dir.and_then(|dir| object.source_path().map(|path| dir.join(path))) if let Some(source_path) = &object.source_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) {
if ui if ui
.button("Open source file") .button("Open source file")
.on_hover_text("Open the source file in the default editor") .on_hover_text("Open the source file in the default editor")
.clicked() .clicked()
{ {
log::info!("Opening file {}", source_path.display()); log::info!("Opening file {}", source_path);
if let Err(e) = open::that_detached(&source_path) { if let Err(e) = open::that_detached(&source_path) {
log::error!("Failed to open source file: {e}"); log::error!("Failed to open source file: {e}");
} }
@ -422,15 +404,14 @@ enum NodeOpen {
fn display_node( fn display_node(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<usize>, selected_obj: &mut Option<usize>,
project_dir: Option<&Path>, units: &[ObjectConfig],
units: &[ProjectObject],
node: &ProjectObjectNode, node: &ProjectObjectNode,
appearance: &Appearance, appearance: &Appearance,
node_open: NodeOpen, node_open: NodeOpen,
) { ) {
match node { match node {
ProjectObjectNode::Unit(name, idx) => { 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) => { ProjectObjectNode::Dir(name, children) => {
let contains_obj = selected_obj.map(|idx| contains_node(node, idx)); let contains_obj = selected_obj.map(|idx| contains_node(node, idx));
@ -456,7 +437,7 @@ fn display_node(
.open(open) .open(open)
.show(ui, |ui| { .show(ui, |ui| {
for node in children { 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( fn filter_node(
units: &[ProjectObject], units: &[ObjectConfig],
node: &ProjectObjectNode, node: &ProjectObjectNode,
search: &str, search: &str,
filter_diffable: bool, filter_diffable: bool,
@ -485,8 +466,8 @@ fn filter_node(
let unit = &units[*idx]; let unit = &units[*idx];
if (search.is_empty() || name.to_ascii_lowercase().contains(search)) if (search.is_empty() || name.to_ascii_lowercase().contains(search))
&& (!filter_diffable || (unit.base_path.is_some() && unit.target_path.is_some())) && (!filter_diffable || (unit.base_path.is_some() && unit.target_path.is_some()))
&& (!filter_incomplete || matches!(unit.complete(), None | Some(false))) && (!filter_incomplete || matches!(unit.complete, None | Some(false)))
&& (show_hidden || !unit.hidden()) && (show_hidden || !unit.hidden)
{ {
Some(node.clone()) Some(node.clone())
} else { } else {
@ -524,13 +505,16 @@ fn subheading(ui: &mut egui::Ui, text: &str, appearance: &Appearance) {
); );
} }
fn format_path(path: &Option<PathBuf>, appearance: &Appearance) -> RichText { fn format_path(path: &Option<Utf8PlatformPathBuf>, appearance: &Appearance) -> RichText {
let mut color = appearance.replace_color; let mut color = appearance.replace_color;
let text = if let Some(dir) = path { let text = if let Some(dir) = path {
if let Some(rel) = dirs::home_dir().and_then(|home| dir.strip_prefix(&home).ok()) { if let Some(rel) = dirs::home_dir()
format!("~{}{}", MAIN_SEPARATOR, rel.display()) .and_then(|home| check_path_buf(home).ok())
.and_then(|home| dir.strip_prefix(&home).ok())
{
format!("~{}{}", MAIN_SEPARATOR, rel)
} else { } else {
format!("{}", dir.display()) dir.to_string()
} }
} else { } else {
color = appearance.delete_color; color = appearance.delete_color;
@ -544,7 +528,7 @@ pub const CONFIG_DISABLED_TEXT: &str =
fn pick_folder_ui( fn pick_folder_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
dir: &Option<PathBuf>, dir: &Option<Utf8PlatformPathBuf>,
label: &str, label: &str,
tooltip: impl FnOnce(&mut egui::Ui), tooltip: impl FnOnce(&mut egui::Ui),
appearance: &Appearance, appearance: &Appearance,

View File

@ -1,16 +1,18 @@
use std::{future::Future, path::PathBuf, pin::Pin, thread::JoinHandle}; use std::{future::Future, path::PathBuf, pin::Pin, thread::JoinHandle};
use objdiff_core::config::path::check_path_buf;
use pollster::FutureExt; use pollster::FutureExt;
use rfd::FileHandle; use rfd::FileHandle;
use typed_path::Utf8PlatformPathBuf;
#[derive(Default)] #[derive(Default)]
pub enum FileDialogResult { pub enum FileDialogResult {
#[default] #[default]
None, None,
ProjectDir(PathBuf), ProjectDir(Utf8PlatformPathBuf),
TargetDir(PathBuf), TargetDir(Utf8PlatformPathBuf),
BaseDir(PathBuf), BaseDir(Utf8PlatformPathBuf),
Object(PathBuf), Object(Utf8PlatformPathBuf),
} }
#[derive(Default)] #[derive(Default)]
@ -22,7 +24,7 @@ impl FileDialogState {
pub fn queue<InitCb, ResultCb>(&mut self, init: InitCb, result_cb: ResultCb) pub fn queue<InitCb, ResultCb>(&mut self, init: InitCb, result_cb: ResultCb)
where where
InitCb: FnOnce() -> Pin<Box<dyn Future<Output = Option<FileHandle>> + Send>>, InitCb: FnOnce() -> Pin<Box<dyn Future<Output = Option<FileHandle>> + Send>>,
ResultCb: FnOnce(PathBuf) -> FileDialogResult + Send + 'static, ResultCb: FnOnce(Utf8PlatformPathBuf) -> FileDialogResult + Send + 'static,
{ {
if self.thread.is_some() { if self.thread.is_some() {
return; return;
@ -30,7 +32,8 @@ impl FileDialogState {
let future = init(); let future = init();
self.thread = Some(std::thread::spawn(move || { self.thread = Some(std::thread::spawn(move || {
if let Some(handle) = future.block_on() { 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 { } else {
FileDialogResult::None FileDialogResult::None
} }

View File

@ -280,12 +280,10 @@ impl DiffViewState {
let Ok(state) = state.read() else { let Ok(state) = state.read() else {
return; return;
}; };
if let (Some(project_dir), Some(source_path)) = ( if let Some(source_path) =
&state.config.project_dir, state.config.selected_obj.as_ref().and_then(|obj| obj.source_path.as_ref())
state.config.selected_obj.as_ref().and_then(|obj| obj.source_path.as_ref()), {
) { log::info!("Opening file {}", source_path);
let source_path = project_dir.join(source_path);
log::info!("Opening file {}", source_path.display());
open::that_detached(source_path).unwrap_or_else(|err| { open::that_detached(source_path).unwrap_or_else(|err| {
log::error!("Failed to open source file: {err}"); log::error!("Failed to open source file: {err}");
}); });

View File

@ -1,6 +1,6 @@
{ {
"name": "objdiff-wasm", "name": "objdiff-wasm",
"version": "2.6.0", "version": "2.7.1",
"description": "A local diffing tool for decompilation projects.", "description": "A local diffing tool for decompilation projects.",
"author": { "author": {
"name": "Luke Street", "name": "Luke Street",

107
objdiff-wasm/src/display.ts Normal file
View File

@ -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: '<unknown>', diff_index});
} else {
cb({type: 'branch_dest', address: arg.branch_dest - baseAddr, diff_index});
}
break;
}
}
if (diff.branch_to) {
cb({type: 'basic_color', text: ' ~> ', index: diff.branch_to.branch_index});
}
}

View File

@ -1,20 +1,20 @@
import {ArgumentValue, DiffResult, InstructionDiff, RelocationTarget} from "../gen/diff_pb"; import {DiffResult} from "../gen/diff_pb";
import type { import type {
ArmArchVersion, ConfigProperty,
ArmR9Usage, MappingConfig,
DiffObjConfig, SymbolMappings,
MipsAbi,
MipsInstrCategory,
X86Formatter
} from '../pkg'; } from '../pkg';
import {AnyHandlerData, InMessage, OutMessage} from './worker'; import {AnyHandlerData, InMessage, OutMessage} from './worker';
// Export wasm types // Export wasm types
export {ArmArchVersion, ArmR9Usage, MipsAbi, MipsInstrCategory, X86Formatter, DiffObjConfig}; export {ConfigProperty, MappingConfig, SymbolMappings};
// Export protobuf types // Export protobuf types
export * from '../gen/diff_pb'; export * from '../gen/diff_pb';
// Export display types
export * from './display';
interface PromiseCallbacks<T> { interface PromiseCallbacks<T> {
start: number; start: number;
resolve: (value: T | PromiseLike<T>) => void; resolve: (value: T | PromiseLike<T>) => void;
@ -111,12 +111,18 @@ async function defer<T>(message: AnyHandlerData, worker?: Worker): Promise<T> {
return promise; return promise;
} }
export async function runDiff(left: Uint8Array | undefined, right: Uint8Array | undefined, diff_config?: DiffObjConfig): Promise<DiffResult> { export async function runDiff(
left: Uint8Array | null | undefined,
right: Uint8Array | null | undefined,
properties?: ConfigProperty[],
mappingConfig?: MappingConfig,
): Promise<DiffResult> {
const data = await defer<Uint8Array>({ const data = await defer<Uint8Array>({
type: 'run_diff_proto', type: 'run_diff_proto',
left, left,
right, right,
diff_config properties,
mappingConfig,
}); });
const parseStart = performance.now(); const parseStart = performance.now();
const result = DiffResult.fromBinary(data, {readUnknownField: false}); 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`); console.debug(`Parsing message took ${end - parseStart}ms`);
return result; 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: '<unknown>', diff_index});
} else {
cb({type: 'branch_dest', address: arg.branch_dest - baseAddr, diff_index});
}
break;
}
}
if (diff.branch_to) {
cb({type: 'basic_color', text: ' ~> ', index: diff.branch_to.branch_index});
}
}

View File

@ -2,7 +2,6 @@ import wasmInit, * as exports from '../pkg';
const handlers = { const handlers = {
init: init, init: init,
// run_diff_json: run_diff_json,
run_diff_proto: run_diff_proto, run_diff_proto: run_diff_proto,
} as const; } as const;
type ExtractData<T> = T extends (arg: infer U) => Promise<unknown> ? U : never; type ExtractData<T> = T extends (arg: infer U) => Promise<unknown> ? U : never;
@ -29,24 +28,16 @@ async function initIfNeeded() {
return wasmReady; return wasmReady;
} }
// async function run_diff_json({left, right, config}: { async function run_diff_proto({left, right, properties, mappingConfig}: {
// left: Uint8Array | undefined, left: Uint8Array | null | undefined,
// right: Uint8Array | undefined, right: Uint8Array | null | undefined,
// config?: exports.DiffObjConfig, properties?: exports.ConfigProperty[],
// }): Promise<string> { mappingConfig?: exports.MappingConfig,
// 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,
}): Promise<Uint8Array> { }): Promise<Uint8Array> {
diff_config = diff_config || {}; const diffConfig = exports.config_from_properties(properties || []);
mapping_config = mapping_config || {}; const leftObj = left ? exports.parse_object(left, diffConfig) : null;
return exports.run_diff_proto(left, right, diff_config, mapping_config); const rightObj = right ? exports.parse_object(right, diffConfig) : null;
return exports.run_diff(leftObj, rightObj, diffConfig, mappingConfig || {});
} }
export type AnyHandlerData = HandlerData[keyof HandlerData]; export type AnyHandlerData = HandlerData[keyof HandlerData];

View File

@ -28,6 +28,7 @@ export default defineConfig([
// https://github.com/egoist/tsup/issues/278 // https://github.com/egoist/tsup/issues/278
async onSuccess() { async onSuccess() {
await fs.copyFile('pkg/objdiff_core_bg.wasm', 'dist/objdiff_core_bg.wasm'); 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');
} }
} }
]); ]);