mirror of
https://github.com/encounter/objdiff.git
synced 2025-12-12 14:46:12 +00:00
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:
@@ -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<PathBuf>,
|
||||
#[argp(option, short = 'o')]
|
||||
/// Output JSON file
|
||||
/// Output file
|
||||
output: Option<PathBuf>,
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
#[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<PathBuf>,
|
||||
}
|
||||
|
||||
#[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<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,
|
||||
#[argp(option, short = 'f')]
|
||||
/// Output format (json or proto, default json)
|
||||
format: Option<String>,
|
||||
}
|
||||
|
||||
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<()> {
|
||||
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<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());
|
||||
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<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 {
|
||||
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<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 {
|
||||
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<ChangeItem> {
|
||||
}
|
||||
|
||||
fn read_report(path: &Path) -> Result<Report> {
|
||||
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<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)
|
||||
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()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user