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.
This commit is contained in:
Luke Street 2024-08-16 00:52:24 -06:00
parent cf937b0be9
commit cad9b70632
10 changed files with 526 additions and 157 deletions

137
Cargo.lock generated
View File

@ -1475,6 +1475,12 @@ dependencies = [
"windows-sys 0.52.0", "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]] [[package]]
name = "flagset" name = "flagset"
version = "0.4.5" version = "0.4.5"
@ -1931,6 +1937,12 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.9" version = "0.3.9"
@ -2192,6 +2204,15 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.11" version = "1.0.11"
@ -2479,6 +2500,12 @@ dependencies = [
"bitflags 2.5.0", "bitflags 2.5.0",
] ]
[[package]]
name = "multimap"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
[[package]] [[package]]
name = "naga" name = "naga"
version = "0.19.2" version = "0.19.2"
@ -2798,13 +2825,18 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-cli" name = "objdiff-cli"
version = "2.0.0-beta.3" version = "2.0.0-beta.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argp", "argp",
"crossterm", "crossterm",
"enable-ansi-support", "enable-ansi-support",
"memmap2",
"objdiff-core", "objdiff-core",
"pbjson",
"pbjson-build",
"prost",
"prost-build",
"ratatui", "ratatui",
"rayon", "rayon",
"serde", "serde",
@ -2817,7 +2849,7 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-core" name = "objdiff-core"
version = "2.0.0-beta.3" version = "2.0.0-beta.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arm-attr", "arm-attr",
@ -2848,7 +2880,7 @@ dependencies = [
[[package]] [[package]]
name = "objdiff-gui" name = "objdiff-gui"
version = "2.0.0-beta.3" version = "2.0.0-beta.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@ -3060,12 +3092,44 @@ dependencies = [
"rustc_version", "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]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 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]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.5" version = "1.1.5"
@ -3178,6 +3242,16 @@ 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 = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" 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]] [[package]]
name = "proc-macro-crate" name = "proc-macro-crate"
version = "1.3.1" version = "1.3.1"
@ -3212,6 +3286,59 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" 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]] [[package]]
name = "pulldown-cmark" name = "pulldown-cmark"
version = "0.9.6" version = "0.9.6"
@ -3309,7 +3436,7 @@ dependencies = [
"compact_str", "compact_str",
"crossterm", "crossterm",
"indoc", "indoc",
"itertools", "itertools 0.12.1",
"lru", "lru",
"paste", "paste",
"stability", "stability",
@ -3988,7 +4115,7 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
dependencies = [ dependencies = [
"heck", "heck 0.4.1",
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion", "rustversion",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "objdiff-cli" name = "objdiff-cli"
version = "2.0.0-beta.3" version = "2.0.0-beta.4"
edition = "2021" edition = "2021"
rust-version = "1.70" rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
@ -18,7 +18,10 @@ anyhow = "1.0.82"
argp = "0.3.0" argp = "0.3.0"
crossterm = "0.27.0" crossterm = "0.27.0"
enable-ansi-support = "0.2.1" enable-ansi-support = "0.2.1"
memmap2 = "0.9.4"
objdiff-core = { path = "../objdiff-core", features = ["all"] } objdiff-core = { path = "../objdiff-core", features = ["all"] }
pbjson = "0.7.0"
prost = "0.13.1"
ratatui = "0.26.2" ratatui = "0.26.2"
rayon = "1.10.0" rayon = "1.10.0"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
@ -27,3 +30,8 @@ supports-color = "3.0.0"
time = { version = "0.3.36", features = ["formatting", "local-offset"] } time = { version = "0.3.36", features = ["formatting", "local-offset"] }
tracing = "0.1.40" tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
[build-dependencies]
prost-build = "0.13.1"
pbjson-build = "0.7.0"

View File

@ -1,3 +1,5 @@
use std::path::PathBuf;
fn main() { fn main() {
let output = std::process::Command::new("git") let output = std::process::Command::new("git")
.args(["rev-parse", "HEAD"]) .args(["rev-parse", "HEAD"])
@ -6,4 +8,28 @@ fn main() {
let rev = String::from_utf8(output.stdout).expect("Failed to parse git output"); let rev = String::from_utf8(output.stdout).expect("Failed to parse git output");
println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}"); println!("cargo:rustc-env=GIT_COMMIT_SHA={rev}");
println!("cargo:rustc-rerun-if-changed=.git/HEAD"); 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");
} }

View File

@ -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;
}

View File

@ -1,7 +1,8 @@
use std::{ use std::{
collections::HashSet, collections::HashSet,
fs::File, fs::File,
io::{BufReader, BufWriter, Write}, io::{BufWriter, Read, Write},
ops::DerefMut,
path::{Path, PathBuf}, path::{Path, PathBuf},
time::Instant, time::Instant,
}; };
@ -13,9 +14,15 @@ use objdiff_core::{
diff, obj, diff, obj,
obj::{ObjSectionKind, ObjSymbolFlags}, obj::{ObjSectionKind, ObjSymbolFlags},
}; };
use prost::Message;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator}; use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
use tracing::{info, warn}; use tracing::{info, warn};
use crate::util::report::{
ChangeInfo, ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Report, ReportItem,
ReportUnit,
};
#[derive(FromArgs, PartialEq, Debug)] #[derive(FromArgs, PartialEq, Debug)]
/// Commands for processing NVIDIA Shield TV alf files. /// Commands for processing NVIDIA Shield TV alf files.
#[argp(subcommand, name = "report")] #[argp(subcommand, name = "report")]
@ -39,11 +46,14 @@ pub struct GenerateArgs {
/// Project directory /// Project directory
project: Option<PathBuf>, project: Option<PathBuf>,
#[argp(option, short = 'o')] #[argp(option, short = 'o')]
/// Output JSON file /// Output file
output: Option<PathBuf>, output: Option<PathBuf>,
#[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,
#[argp(option, short = 'f')]
/// Output format (json or proto, default json)
format: Option<String>,
} }
#[derive(FromArgs, PartialEq, Debug)] #[derive(FromArgs, PartialEq, Debug)]
@ -51,65 +61,17 @@ pub struct GenerateArgs {
#[argp(subcommand, name = "changes")] #[argp(subcommand, name = "changes")]
pub struct ChangesArgs { pub struct ChangesArgs {
#[argp(positional)] #[argp(positional)]
/// Previous report JSON file /// Previous report file
previous: PathBuf, previous: PathBuf,
#[argp(positional)] #[argp(positional)]
/// Current report JSON file /// Current report file
current: PathBuf, current: PathBuf,
#[argp(option, short = 'o')] #[argp(option, short = 'o')]
/// Output JSON file /// Output file
output: Option<PathBuf>, output: Option<PathBuf>,
} #[argp(option, short = 'f')]
/// Output format (json or proto, default json)
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] format: Option<String>,
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<ReportUnit>,
}
#[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<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
module_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
module_id: Option<u32>,
sections: Vec<ReportItem>,
functions: Vec<ReportItem>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct ReportItem {
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
demangled_name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_hex",
deserialize_with = "deserialize_hex"
)]
address: Option<u64>,
size: u64,
fuzzy_match_percent: f32,
} }
pub fn run(args: Args) -> Result<()> { 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<Self> {
match s {
"json" => Ok(Self::Json),
"binpb" | "proto" | "protobuf" => Ok(Self::Proto),
_ => bail!("Invalid output format: {}", s),
}
}
}
fn generate(args: GenerateArgs) -> Result<()> { 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(".")); let project_dir = args.project.as_deref().unwrap_or_else(|| Path::new("."));
info!("Loading project {}", project_dir.display()); info!("Loading project {}", project_dir.display());
@ -197,17 +180,46 @@ fn generate(args: GenerateArgs) -> Result<()> {
}; };
let duration = start.elapsed(); let duration = start.elapsed();
info!("Report generated in {}.{:03}s", duration.as_secs(), duration.subsec_millis()); 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<T>(input: &T, output: Option<&Path>, format: OutputFormat) -> Result<()>
where T: serde::Serialize + prost::Message {
if let Some(output) = output {
info!("Writing to {}", output.display()); info!("Writing to {}", output.display());
let mut output = BufWriter::new( let file = File::options()
File::create(output) .read(true)
.with_context(|| format!("Failed to create file {}", output.display()))?, .write(true)
); .create(true)
serde_json::to_writer_pretty(&mut output, &report)?; .truncate(true)
output.flush()?; .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 { } 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(()) Ok(())
} }
@ -335,27 +347,6 @@ fn report_object(
Ok(Some(unit)) Ok(Some(unit))
} }
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct Changes {
from: ChangeInfo,
to: ChangeInfo,
units: Vec<ChangeUnit>,
}
#[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 { impl From<&Report> for ChangeInfo {
fn from(report: &Report) -> Self { fn from(report: &Report) -> Self {
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<ChangeInfo>,
to: Option<ChangeInfo>,
sections: Vec<ChangeItem>,
functions: Vec<ChangeItem>,
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
struct ChangeItem {
name: String,
from: Option<ChangeItemInfo>,
to: Option<ChangeItemInfo>,
}
#[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
struct ChangeItemInfo {
fuzzy_match_percent: f32,
size: u64,
}
impl From<&ReportItem> for ChangeItemInfo { impl From<&ReportItem> for ChangeItemInfo {
fn from(value: &ReportItem) -> Self { fn from(value: &ReportItem) -> Self {
Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size } 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<()> { fn changes(args: ChangesArgs) -> Result<()> {
let previous = read_report(&args.previous)?; let output_format = if let Some(format) = &args.format {
let current = read_report(&args.current)?; 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 { let mut changes = Changes {
from: ChangeInfo::from(&previous), from: Some(ChangeInfo::from(&previous)),
to: ChangeInfo::from(&current), to: Some(ChangeInfo::from(&current)),
units: vec![], units: vec![],
}; };
for prev_unit in &previous.units { for prev_unit in &previous.units {
@ -466,17 +450,7 @@ fn changes(args: ChangesArgs) -> Result<()> {
}); });
} }
} }
if let Some(output) = &args.output { write_output(&changes, args.output.as_deref(), output_format)?;
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)?;
}
Ok(()) Ok(())
} }
@ -538,30 +512,14 @@ fn process_new_items(items: &[ReportItem]) -> Vec<ChangeItem> {
} }
fn read_report(path: &Path) -> Result<Report> { fn read_report(path: &Path) -> Result<Report> {
serde_json::from_reader(BufReader::new( if path == Path::new("-") {
File::open(path).with_context(|| format!("Failed to open {}", path.display()))?, let mut data = vec![];
)) std::io::stdin().read_to_end(&mut data)?;
.with_context(|| format!("Failed to read report {}", path.display())) return Report::parse(&data).with_context(|| "Failed to load report from stdin");
}
fn serialize_hex<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
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<Option<u64>, 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)
} }
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()))
} }

View File

@ -96,7 +96,7 @@ fn main() {
// Try to enable ANSI support on Windows. // Try to enable ANSI support on Windows.
let _ = enable_ansi_support(); let _ = enable_ansi_support();
// Disable isatty check for supports-color. (e.g. when used with ninja) // 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) supports_color::on(Stream::Stdout).is_some_and(|c| c.has_basic)
}; };

View File

@ -1 +1,2 @@
pub mod report;
pub mod term; pub mod term;

View File

@ -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<Self> {
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<Self, serde_json::Error> {
match serde_json::from_slice::<Self>(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::<LegacyReport>(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<LegacyReportUnit>,
}
impl From<LegacyReport> 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<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
module_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
module_id: Option<u32>,
sections: Vec<LegacyReportItem>,
functions: Vec<LegacyReportItem>,
}
impl From<LegacyReportUnit> 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<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_hex",
deserialize_with = "deserialize_hex"
)]
address: Option<u64>,
size: u64,
fuzzy_match_percent: f32,
}
impl From<LegacyReportItem> 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<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
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<Option<u64>, 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)
}
}

View File

@ -1,6 +1,6 @@
[package] [package]
name = "objdiff-core" name = "objdiff-core"
version = "2.0.0-beta.3" version = "2.0.0-beta.4"
edition = "2021" edition = "2021"
rust-version = "1.70" rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "objdiff-gui" name = "objdiff-gui"
version = "2.0.0-beta.3" version = "2.0.0-beta.4"
edition = "2021" edition = "2021"
rust-version = "1.70" rust-version = "1.70"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]