From cad9b70632da54131ec6726b671d51937d1eff87 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Fri, 16 Aug 2024 00:52:24 -0600 Subject: [PATCH] Support protobuf format for reports This migrates to using protobuf to define the "report" and "changes" formats in objdiff-cli. The JSON output now uses the Proto3 "JSON Mapping", which is slightly incompatible with the existing JSON format. Mainly, 64-bit numbers are represented as strings, and addresses are decimal strings instead of hex. However, the older JSON format is still accepted by "report changes" to ease migration. --- Cargo.lock | 137 ++++++++++++++++- objdiff-cli/Cargo.toml | 10 +- objdiff-cli/build.rs | 26 ++++ objdiff-cli/protos/report.proto | 85 +++++++++++ objdiff-cli/src/cmd/report.rs | 254 +++++++++++++------------------- objdiff-cli/src/main.rs | 2 +- objdiff-cli/src/util/mod.rs | 1 + objdiff-cli/src/util/report.rs | 164 +++++++++++++++++++++ objdiff-core/Cargo.toml | 2 +- objdiff-gui/Cargo.toml | 2 +- 10 files changed, 526 insertions(+), 157 deletions(-) create mode 100644 objdiff-cli/protos/report.proto create mode 100644 objdiff-cli/src/util/report.rs diff --git a/Cargo.lock b/Cargo.lock index ae95d19..ada2bf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1475,6 +1475,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flagset" version = "0.4.5" @@ -1931,6 +1937,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -2192,6 +2204,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" @@ -2479,6 +2500,12 @@ dependencies = [ "bitflags 2.5.0", ] +[[package]] +name = "multimap" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" + [[package]] name = "naga" version = "0.19.2" @@ -2798,13 +2825,18 @@ dependencies = [ [[package]] name = "objdiff-cli" -version = "2.0.0-beta.3" +version = "2.0.0-beta.4" dependencies = [ "anyhow", "argp", "crossterm", "enable-ansi-support", + "memmap2", "objdiff-core", + "pbjson", + "pbjson-build", + "prost", + "prost-build", "ratatui", "rayon", "serde", @@ -2817,7 +2849,7 @@ dependencies = [ [[package]] name = "objdiff-core" -version = "2.0.0-beta.3" +version = "2.0.0-beta.4" dependencies = [ "anyhow", "arm-attr", @@ -2848,7 +2880,7 @@ dependencies = [ [[package]] name = "objdiff-gui" -version = "2.0.0-beta.3" +version = "2.0.0-beta.4" dependencies = [ "anyhow", "bytes", @@ -3060,12 +3092,44 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "pbjson" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e6349fa080353f4a597daffd05cb81572a9c031a6d4fff7e504947496fcc68" +dependencies = [ + "base64 0.21.7", + "serde", +] + +[[package]] +name = "pbjson-build" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eea3058763d6e656105d1403cb04e0a41b7bbac6362d413e7c33be0c32279c9" +dependencies = [ + "heck 0.5.0", + "itertools 0.13.0", + "prost", + "prost-types", +] + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -3178,6 +3242,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2", + "syn 2.0.60", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -3212,6 +3286,59 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +[[package]] +name = "prost" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13db3d3fde688c61e2446b4d843bc27a7e8af269a69440c0308021dc92333cc" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb182580f71dd070f88d01ce3de9f4da5021db7115d2e1c3605a754153b77c1" +dependencies = [ + "bytes", + "heck 0.5.0", + "itertools 0.13.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.60", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18bec9b0adc4eba778b33684b7ba3e7137789434769ee3ce3930463ef904cfca" +dependencies = [ + "anyhow", + "itertools 0.13.0", + "proc-macro2", + "quote", + "syn 2.0.60", +] + +[[package]] +name = "prost-types" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee5168b05f49d4b0ca581206eb14a7b22fafd963efe729ac48eb03266e25cc2" +dependencies = [ + "prost", +] + [[package]] name = "pulldown-cmark" version = "0.9.6" @@ -3309,7 +3436,7 @@ dependencies = [ "compact_str", "crossterm", "indoc", - "itertools", + "itertools 0.12.1", "lru", "paste", "stability", @@ -3988,7 +4115,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", diff --git a/objdiff-cli/Cargo.toml b/objdiff-cli/Cargo.toml index efc069d..6aa7282 100644 --- a/objdiff-cli/Cargo.toml +++ b/objdiff-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "objdiff-cli" -version = "2.0.0-beta.3" +version = "2.0.0-beta.4" edition = "2021" rust-version = "1.70" authors = ["Luke Street "] @@ -18,7 +18,10 @@ anyhow = "1.0.82" argp = "0.3.0" crossterm = "0.27.0" enable-ansi-support = "0.2.1" +memmap2 = "0.9.4" objdiff-core = { path = "../objdiff-core", features = ["all"] } +pbjson = "0.7.0" +prost = "0.13.1" ratatui = "0.26.2" rayon = "1.10.0" serde = { version = "1", features = ["derive"] } @@ -27,3 +30,8 @@ supports-color = "3.0.0" time = { version = "0.3.36", features = ["formatting", "local-offset"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } + +[build-dependencies] +prost-build = "0.13.1" +pbjson-build = "0.7.0" + diff --git a/objdiff-cli/build.rs b/objdiff-cli/build.rs index a488c56..7b02675 100644 --- a/objdiff-cli/build.rs +++ b/objdiff-cli/build.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + fn main() { let output = std::process::Command::new("git") .args(["rev-parse", "HEAD"]) @@ -6,4 +8,28 @@ fn main() { let rev = String::from_utf8(output.stdout).expect("Failed to parse git output"); println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}"); println!("cargo:rustc-rerun-if-changed=.git/HEAD"); + + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("protos"); + let proto_files = vec![root.join("report.proto")]; + + // Tell cargo to recompile if any of these proto files are changed + for proto_file in &proto_files { + println!("cargo:rerun-if-changed={}", proto_file.display()); + } + + let descriptor_path = + PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("proto_descriptor.bin"); + + prost_build::Config::new() + .file_descriptor_set_path(&descriptor_path) + .compile_protos(&proto_files, &[root]) + .expect("Failed to compile protos"); + + let descriptor_set = std::fs::read(descriptor_path).expect("Failed to read descriptor set"); + pbjson_build::Builder::new() + .register_descriptors(&descriptor_set) + .expect("Failed to register descriptors") + .preserve_proto_field_names() + .build(&[".objdiff"]) + .expect("Failed to build pbjson"); } diff --git a/objdiff-cli/protos/report.proto b/objdiff-cli/protos/report.proto new file mode 100644 index 0000000..7fb91a7 --- /dev/null +++ b/objdiff-cli/protos/report.proto @@ -0,0 +1,85 @@ +syntax = "proto3"; + +package objdiff.report; + +message Report { + float fuzzy_match_percent = 1; + uint64 total_code = 2; + uint64 matched_code = 3; + float matched_code_percent = 4; + uint64 total_data = 5; + uint64 matched_data = 6; + float matched_data_percent = 7; + uint32 total_functions = 8; + uint32 matched_functions = 9; + float matched_functions_percent = 10; + repeated ReportUnit units = 11; +} + +message ReportUnit { + string name = 1; + float fuzzy_match_percent = 2; + uint64 total_code = 3; + uint64 matched_code = 4; + uint64 total_data = 5; + uint64 matched_data = 6; + uint32 total_functions = 7; + uint32 matched_functions = 8; + optional bool complete = 9; + optional string module_name = 10; + optional uint32 module_id = 11; + repeated ReportItem sections = 12; + repeated ReportItem functions = 13; +} + +message ReportItem { + string name = 1; + uint64 size = 2; + float fuzzy_match_percent = 3; + optional string demangled_name = 4; + optional uint64 address = 5; +} + +// Used as stdin for the changes command +message ChangesInput { + Report from = 1; + Report to = 2; +} + +message Changes { + ChangeInfo from = 1; + ChangeInfo to = 2; + repeated ChangeUnit units = 3; +} + +message ChangeInfo { + float fuzzy_match_percent = 1; + uint64 total_code = 2; + uint64 matched_code = 3; + float matched_code_percent = 4; + uint64 total_data = 5; + uint64 matched_data = 6; + float matched_data_percent = 7; + uint32 total_functions = 8; + uint32 matched_functions = 9; + float matched_functions_percent = 10; +} + +message ChangeUnit { + string name = 1; + optional ChangeInfo from = 2; + optional ChangeInfo to = 3; + repeated ChangeItem sections = 4; + repeated ChangeItem functions = 5; +} + +message ChangeItem { + string name = 1; + optional ChangeItemInfo from = 2; + optional ChangeItemInfo to = 3; +} + +message ChangeItemInfo { + float fuzzy_match_percent = 1; + uint64 size = 2; +} \ No newline at end of file diff --git a/objdiff-cli/src/cmd/report.rs b/objdiff-cli/src/cmd/report.rs index 40ed7df..42b4c1d 100644 --- a/objdiff-cli/src/cmd/report.rs +++ b/objdiff-cli/src/cmd/report.rs @@ -1,7 +1,8 @@ use std::{ collections::HashSet, fs::File, - io::{BufReader, BufWriter, Write}, + io::{BufWriter, Read, Write}, + ops::DerefMut, path::{Path, PathBuf}, time::Instant, }; @@ -13,9 +14,15 @@ use objdiff_core::{ diff, obj, obj::{ObjSectionKind, ObjSymbolFlags}, }; +use prost::Message; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; use tracing::{info, warn}; +use crate::util::report::{ + ChangeInfo, ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Report, ReportItem, + ReportUnit, +}; + #[derive(FromArgs, PartialEq, Debug)] /// Commands for processing NVIDIA Shield TV alf files. #[argp(subcommand, name = "report")] @@ -39,11 +46,14 @@ pub struct GenerateArgs { /// Project directory project: Option, #[argp(option, short = 'o')] - /// Output JSON file + /// Output file output: Option, #[argp(switch, short = 'd')] /// Deduplicate global and weak symbols (runs single-threaded) deduplicate: bool, + #[argp(option, short = 'f')] + /// Output format (json or proto, default json) + format: Option, } #[derive(FromArgs, PartialEq, Debug)] @@ -51,65 +61,17 @@ pub struct GenerateArgs { #[argp(subcommand, name = "changes")] pub struct ChangesArgs { #[argp(positional)] - /// Previous report JSON file + /// Previous report file previous: PathBuf, #[argp(positional)] - /// Current report JSON file + /// Current report file current: PathBuf, #[argp(option, short = 'o')] - /// Output JSON file + /// Output file output: Option, -} - -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] -struct Report { - fuzzy_match_percent: f32, - total_code: u64, - matched_code: u64, - matched_code_percent: f32, - total_data: u64, - matched_data: u64, - matched_data_percent: f32, - total_functions: u32, - matched_functions: u32, - matched_functions_percent: f32, - units: Vec, -} - -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] -struct ReportUnit { - name: String, - fuzzy_match_percent: f32, - total_code: u64, - matched_code: u64, - total_data: u64, - matched_data: u64, - total_functions: u32, - matched_functions: u32, - #[serde(skip_serializing_if = "Option::is_none")] - complete: Option, - #[serde(skip_serializing_if = "Option::is_none")] - module_name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - module_id: Option, - sections: Vec, - functions: Vec, -} - -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] -struct ReportItem { - name: String, - #[serde(skip_serializing_if = "Option::is_none")] - demangled_name: Option, - #[serde( - default, - skip_serializing_if = "Option::is_none", - serialize_with = "serialize_hex", - deserialize_with = "deserialize_hex" - )] - address: Option, - size: u64, - fuzzy_match_percent: f32, + #[argp(option, short = 'f')] + /// Output format (json or proto, default json) + format: Option, } pub fn run(args: Args) -> Result<()> { @@ -119,7 +81,28 @@ pub fn run(args: Args) -> Result<()> { } } +enum OutputFormat { + Json, + Proto, +} + +impl OutputFormat { + fn from_str(s: &str) -> Result { + match s { + "json" => Ok(Self::Json), + "binpb" | "proto" | "protobuf" => Ok(Self::Proto), + _ => bail!("Invalid output format: {}", s), + } + } +} + fn generate(args: GenerateArgs) -> Result<()> { + let output_format = if let Some(format) = &args.format { + OutputFormat::from_str(format)? + } else { + OutputFormat::Json + }; + let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new(".")); info!("Loading project {}", project_dir.display()); @@ -197,17 +180,46 @@ fn generate(args: GenerateArgs) -> Result<()> { }; let duration = start.elapsed(); info!("Report generated in {}.{:03}s", duration.as_secs(), duration.subsec_millis()); - if let Some(output) = &args.output { + write_output(&report, args.output.as_deref(), output_format)?; + Ok(()) +} + +fn write_output(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()> +where T: serde::Serialize + prost::Message { + if let Some(output) = output { info!("Writing to {}", output.display()); - let mut output = BufWriter::new( - File::create(output) - .with_context(|| format!("Failed to create file {}", output.display()))?, - ); - serde_json::to_writer_pretty(&mut output, &report)?; - output.flush()?; + let file = File::options() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(output) + .with_context(|| format!("Failed to create file {}", output.display()))?; + match format { + OutputFormat::Json => { + let mut output = BufWriter::new(file); + serde_json::to_writer_pretty(&mut output, input) + .context("Failed to write output file")?; + output.flush().context("Failed to flush output file")?; + } + OutputFormat::Proto => { + file.set_len(input.encoded_len() as u64)?; + let map = + unsafe { memmap2::Mmap::map(&file) }.context("Failed to map output file")?; + let mut output = map.make_mut().context("Failed to remap output file")?; + input.encode(&mut output.deref_mut()).context("Failed to encode output")?; + } + } } else { - serde_json::to_writer_pretty(std::io::stdout(), &report)?; - } + match format { + OutputFormat::Json => { + serde_json::to_writer_pretty(std::io::stdout(), input)?; + } + OutputFormat::Proto => { + std::io::stdout().write_all(&input.encode_to_vec())?; + } + } + }; Ok(()) } @@ -335,27 +347,6 @@ fn report_object( Ok(Some(unit)) } -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] -struct Changes { - from: ChangeInfo, - to: ChangeInfo, - units: Vec, -} - -#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] -struct ChangeInfo { - fuzzy_match_percent: f32, - total_code: u64, - matched_code: u64, - matched_code_percent: f32, - total_data: u64, - matched_data: u64, - matched_data_percent: f32, - total_functions: u32, - matched_functions: u32, - matched_functions_percent: f32, -} - impl From<&Report> for ChangeInfo { fn from(report: &Report) -> Self { Self { @@ -402,28 +393,6 @@ impl From<&ReportUnit> for ChangeInfo { } } -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] -struct ChangeUnit { - name: String, - from: Option, - to: Option, - sections: Vec, - functions: Vec, -} - -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] -struct ChangeItem { - name: String, - from: Option, - to: Option, -} - -#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)] -struct ChangeItemInfo { - fuzzy_match_percent: f32, - size: u64, -} - impl From<&ReportItem> for ChangeItemInfo { fn from(value: &ReportItem) -> Self { Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size } @@ -431,11 +400,26 @@ impl From<&ReportItem> for ChangeItemInfo { } fn changes(args: ChangesArgs) -> Result<()> { - let previous = read_report(&args.previous)?; - let current = read_report(&args.current)?; + let output_format = if let Some(format) = &args.format { + OutputFormat::from_str(format)? + } else { + OutputFormat::Json + }; + + let (previous, current) = if args.previous == Path::new("-") && args.current == Path::new("-") { + // Special case for comparing two reports from stdin + let mut data = vec![]; + std::io::stdin().read_to_end(&mut data)?; + let input = ChangesInput::decode(data.as_slice())?; + (input.from.unwrap(), input.to.unwrap()) + } else { + let previous = read_report(&args.previous)?; + let current = read_report(&args.current)?; + (previous, current) + }; let mut changes = Changes { - from: ChangeInfo::from(&previous), - to: ChangeInfo::from(¤t), + from: Some(ChangeInfo::from(&previous)), + to: Some(ChangeInfo::from(¤t)), units: vec![], }; for prev_unit in &previous.units { @@ -466,17 +450,7 @@ fn changes(args: ChangesArgs) -> Result<()> { }); } } - if let Some(output) = &args.output { - info!("Writing to {}", output.display()); - let mut output = BufWriter::new( - File::create(output) - .with_context(|| format!("Failed to create file {}", output.display()))?, - ); - serde_json::to_writer_pretty(&mut output, &changes)?; - output.flush()?; - } else { - serde_json::to_writer_pretty(std::io::stdout(), &changes)?; - } + write_output(&changes, args.output.as_deref(), output_format)?; Ok(()) } @@ -538,30 +512,14 @@ fn process_new_items(items: &[ReportItem]) -> Vec { } fn read_report(path: &Path) -> Result { - serde_json::from_reader(BufReader::new( - File::open(path).with_context(|| format!("Failed to open {}", path.display()))?, - )) - .with_context(|| format!("Failed to read report {}", path.display())) -} - -fn serialize_hex(x: &Option, s: S) -> Result -where S: serde::Serializer { - if let Some(x) = x { - s.serialize_str(&format!("{:#x}", x)) - } else { - s.serialize_none() - } -} - -fn deserialize_hex<'de, D>(d: D) -> Result, D::Error> -where D: serde::Deserializer<'de> { - use serde::Deserialize; - let s = String::deserialize(d)?; - if s.is_empty() { - Ok(None) - } else if !s.starts_with("0x") { - Err(serde::de::Error::custom("expected hex string")) - } else { - u64::from_str_radix(&s[2..], 16).map(Some).map_err(serde::de::Error::custom) + if path == Path::new("-") { + let mut data = vec![]; + std::io::stdin().read_to_end(&mut data)?; + return Report::parse(&data).with_context(|| "Failed to load report from stdin"); } + let file = File::open(path).with_context(|| format!("Failed to open {}", path.display()))?; + let mmap = unsafe { memmap2::Mmap::map(&file) } + .with_context(|| format!("Failed to map {}", path.display()))?; + Report::parse(mmap.as_ref()) + .with_context(|| format!("Failed to load report {}", path.display())) } diff --git a/objdiff-cli/src/main.rs b/objdiff-cli/src/main.rs index a651e22..b131fcb 100644 --- a/objdiff-cli/src/main.rs +++ b/objdiff-cli/src/main.rs @@ -96,7 +96,7 @@ fn main() { // Try to enable ANSI support on Windows. let _ = enable_ansi_support(); // Disable isatty check for supports-color. (e.g. when used with ninja) - env::set_var("IGNORE_IS_TERMINAL", "1"); + unsafe { env::set_var("IGNORE_IS_TERMINAL", "1") }; supports_color::on(Stream::Stdout).is_some_and(|c| c.has_basic) }; diff --git a/objdiff-cli/src/util/mod.rs b/objdiff-cli/src/util/mod.rs index 5ff0ecd..1f743a1 100644 --- a/objdiff-cli/src/util/mod.rs +++ b/objdiff-cli/src/util/mod.rs @@ -1 +1,2 @@ +pub mod report; pub mod term; diff --git a/objdiff-cli/src/util/report.rs b/objdiff-cli/src/util/report.rs new file mode 100644 index 0000000..31581da --- /dev/null +++ b/objdiff-cli/src/util/report.rs @@ -0,0 +1,164 @@ +use anyhow::{bail, Result}; +use prost::Message; +use serde_json::error::Category; + +// Protobuf report types +include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs")); +include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs")); + +impl Report { + pub fn parse(data: &[u8]) -> Result { + if data.is_empty() { + bail!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof)); + } + if data[0] == b'{' { + // Load as JSON + Self::from_json(data).map_err(anyhow::Error::new) + } else { + // Load as binary protobuf + Self::decode(data).map_err(anyhow::Error::new) + } + } + + fn from_json(bytes: &[u8]) -> Result { + match serde_json::from_slice::(bytes) { + Ok(report) => Ok(report), + Err(e) => { + match e.classify() { + Category::Io | Category::Eof | Category::Syntax => Err(e), + Category::Data => { + // Try to load as legacy report + match serde_json::from_slice::(bytes) { + Ok(legacy_report) => Ok(Report::from(legacy_report)), + Err(_) => Err(e), + } + } + } + } + } + } +} + +// Older JSON report types +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +struct LegacyReport { + fuzzy_match_percent: f32, + total_code: u64, + matched_code: u64, + matched_code_percent: f32, + total_data: u64, + matched_data: u64, + matched_data_percent: f32, + total_functions: u32, + matched_functions: u32, + matched_functions_percent: f32, + units: Vec, +} + +impl From for Report { + fn from(value: LegacyReport) -> Self { + Self { + fuzzy_match_percent: value.fuzzy_match_percent, + total_code: value.total_code, + matched_code: value.matched_code, + matched_code_percent: value.matched_code_percent, + total_data: value.total_data, + matched_data: value.matched_data, + matched_data_percent: value.matched_data_percent, + total_functions: value.total_functions, + matched_functions: value.matched_functions, + matched_functions_percent: value.matched_functions_percent, + units: value.units.into_iter().map(ReportUnit::from).collect(), + } + } +} + +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +struct LegacyReportUnit { + name: String, + fuzzy_match_percent: f32, + total_code: u64, + matched_code: u64, + total_data: u64, + matched_data: u64, + total_functions: u32, + matched_functions: u32, + #[serde(skip_serializing_if = "Option::is_none")] + complete: Option, + #[serde(skip_serializing_if = "Option::is_none")] + module_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + module_id: Option, + sections: Vec, + functions: Vec, +} + +impl From for ReportUnit { + fn from(value: LegacyReportUnit) -> Self { + Self { + name: value.name.clone(), + fuzzy_match_percent: value.fuzzy_match_percent, + total_code: value.total_code, + matched_code: value.matched_code, + total_data: value.total_data, + matched_data: value.matched_data, + total_functions: value.total_functions, + matched_functions: value.matched_functions, + complete: value.complete, + module_name: value.module_name.clone(), + module_id: value.module_id, + sections: value.sections.into_iter().map(ReportItem::from).collect(), + functions: value.functions.into_iter().map(ReportItem::from).collect(), + } + } +} + +#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] +struct LegacyReportItem { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + demangled_name: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_hex", + deserialize_with = "deserialize_hex" + )] + address: Option, + size: u64, + fuzzy_match_percent: f32, +} + +impl From for ReportItem { + fn from(value: LegacyReportItem) -> Self { + Self { + name: value.name, + demangled_name: value.demangled_name, + address: value.address, + size: value.size, + fuzzy_match_percent: value.fuzzy_match_percent, + } + } +} + +fn serialize_hex(x: &Option, s: S) -> Result +where S: serde::Serializer { + if let Some(x) = x { + s.serialize_str(&format!("{:#x}", x)) + } else { + s.serialize_none() + } +} + +fn deserialize_hex<'de, D>(d: D) -> Result, D::Error> +where D: serde::Deserializer<'de> { + use serde::Deserialize; + let s = String::deserialize(d)?; + if s.is_empty() { + Ok(None) + } else if !s.starts_with("0x") { + Err(serde::de::Error::custom("expected hex string")) + } else { + u64::from_str_radix(&s[2..], 16).map(Some).map_err(serde::de::Error::custom) + } +} diff --git a/objdiff-core/Cargo.toml b/objdiff-core/Cargo.toml index e7ab182..e2860e6 100644 --- a/objdiff-core/Cargo.toml +++ b/objdiff-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "objdiff-core" -version = "2.0.0-beta.3" +version = "2.0.0-beta.4" edition = "2021" rust-version = "1.70" authors = ["Luke Street "] diff --git a/objdiff-gui/Cargo.toml b/objdiff-gui/Cargo.toml index 873b77d..540c6c9 100644 --- a/objdiff-gui/Cargo.toml +++ b/objdiff-gui/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "objdiff-gui" -version = "2.0.0-beta.3" +version = "2.0.0-beta.4" edition = "2021" rust-version = "1.70" authors = ["Luke Street "]