Support for progress categories & linked stats

This commit is contained in:
Luke Street 2024-09-03 00:59:06 -06:00
parent 3bd8aaee41
commit 195379968c
10 changed files with 218 additions and 31 deletions

View File

@ -11,7 +11,8 @@ use argp::FromArgs;
use objdiff_core::{ use objdiff_core::{
bindings::report::{ bindings::report::{
ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, Report, ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, Report,
ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata, ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata,
REPORT_VERSION,
}, },
config::ProjectObject, config::ProjectObject,
diff, obj, diff, obj,
@ -129,7 +130,17 @@ fn generate(args: GenerateArgs) -> Result<()> {
units = vec.into_iter().flatten().collect(); units = vec.into_iter().flatten().collect();
} }
let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect(); let measures = units.iter().flat_map(|u| u.measures.into_iter()).collect();
let report = Report { measures: Some(measures), units }; let mut categories = Vec::new();
for category in &project.progress_categories {
categories.push(ReportCategory {
id: category.id.clone(),
name: category.name.clone(),
measures: Some(Default::default()),
});
}
let mut report =
Report { measures: Some(measures), units, version: REPORT_VERSION, categories };
report.calculate_progress_categories();
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());
write_output(&report, args.output.as_deref(), output_format)?; write_output(&report, args.output.as_deref(), output_format)?;
@ -145,7 +156,7 @@ fn report_object(
) -> Result<Option<ReportUnit>> { ) -> Result<Option<ReportUnit>> {
object.resolve_paths(project_dir, target_dir, base_dir); 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 != Some(true) => { (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);
} }
@ -173,13 +184,19 @@ fn report_object(
let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?; let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?;
let metadata = ReportUnitMetadata { let metadata = ReportUnitMetadata {
complete: object.complete, complete: object.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: None, // TODO source_path: object.metadata.as_ref().and_then(|m| m.source_path.clone()),
progress_categories: object
.metadata
.as_ref()
.and_then(|m| m.progress_categories.clone())
.unwrap_or_default(),
auto_generated: object.metadata.as_ref().and_then(|m| m.auto_generated),
}; };
let mut measures = Measures::default(); let mut measures = Measures::default();
let mut sections = vec![]; let mut sections = vec![];
@ -191,7 +208,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 == Some(true) { if object.complete().unwrap_or(false) {
100.0 100.0
} else { } else {
0.0 0.0
@ -233,7 +250,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 == Some(true) { if object.complete().unwrap_or(false) {
100.0 100.0
} else { } else {
0.0 0.0
@ -259,6 +276,10 @@ fn report_object(
measures.total_functions += 1; measures.total_functions += 1;
} }
} }
if metadata.complete.unwrap_or(false) {
measures.complete_code = measures.total_code;
measures.complete_data = measures.total_data;
}
measures.calc_fuzzy_match_percent(); measures.calc_fuzzy_match_percent();
measures.calc_matched_percent(); measures.calc_matched_percent();
Ok(Some(ReportUnit { Ok(Some(ReportUnit {

View File

@ -15,7 +15,7 @@ A local diffing tool for decompilation projects.
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features] [features]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm"] all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
any-arch = [] # Implicit, used to check if any arch is enabled any-arch = [] # Implicit, used to check if any arch is enabled
config = ["globset", "semver", "serde_json", "serde_yaml"] config = ["globset", "semver", "serde_json", "serde_yaml"]
dwarf = ["gimli"] dwarf = ["gimli"]
@ -23,7 +23,8 @@ mips = ["any-arch", "rabbitizer"]
ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"] ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"]
x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"] x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"]
arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"] arm = ["any-arch", "cpp_demangle", "unarm", "arm-attr"]
wasm = ["serde_json", "console_error_panic_hook", "console_log"] bindings = ["serde_json", "prost", "pbjson"]
wasm = ["bindings", "console_error_panic_hook", "console_log"]
[dependencies] [dependencies]
anyhow = "1.0.82" anyhow = "1.0.82"
@ -34,8 +35,8 @@ log = "0.4.21"
memmap2 = "0.9.4" memmap2 = "0.9.4"
num-traits = "0.2.18" num-traits = "0.2.18"
object = { version = "0.35.0", features = ["read_core", "std", "elf", "pe"], default-features = false } object = { version = "0.35.0", features = ["read_core", "std", "elf", "pe"], default-features = false }
pbjson = "0.7.0" pbjson = { version = "0.7.0", optional = true }
prost = "0.13.1" prost = { version = "0.13.1", optional = true }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
similar = { version = "2.5.0", default-features = false } similar = { version = "2.5.0", default-features = false }
strum = { version = "0.26.2", features = ["derive"] } strum = { version = "0.26.2", features = ["derive"] }

View File

@ -24,6 +24,14 @@ message Measures {
uint32 matched_functions = 9; uint32 matched_functions = 9;
// Fully matched functions percent // Fully matched functions percent
float matched_functions_percent = 10; float matched_functions_percent = 10;
// Completed (or "linked") code size in bytes
uint64 complete_code = 11;
// Completed (or "linked") code percent
float complete_code_percent = 12;
// Completed (or "linked") data size in bytes
uint64 complete_data = 13;
// Completed (or "linked") data percent
float complete_data_percent = 14;
} }
// Project progress report // Project progress report
@ -32,6 +40,19 @@ message Report {
Measures measures = 1; Measures measures = 1;
// Units within this report // Units within this report
repeated ReportUnit units = 2; repeated ReportUnit units = 2;
// Report version
uint32 version = 3;
// Progress categories
repeated ReportCategory categories = 4;
}
message ReportCategory {
// The ID of the category
string id = 1;
// The name of the category
string name = 2;
// Progress info for this category
Measures measures = 3;
} }
// A unit of the report (usually a translation unit) // A unit of the report (usually a translation unit)
@ -58,6 +79,10 @@ message ReportUnitMetadata {
optional uint32 module_id = 3; optional uint32 module_id = 3;
// The path to the source file of this unit // The path to the source file of this unit
optional string source_path = 4; optional string source_path = 4;
// Progress categories for this unit
repeated string progress_categories = 5;
// Whether this unit is automatically generated (not user-provided)
optional bool auto_generated = 6;
} }
// A section or function within a unit // A section or function within a unit

View File

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

View File

@ -1,3 +1,5 @@
use std::ops::AddAssign;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use prost::Message; use prost::Message;
use serde_json::error::Category; use serde_json::error::Category;
@ -6,18 +8,21 @@ use serde_json::error::Category;
include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs")); include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs")); include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
pub const REPORT_VERSION: u32 = 1;
impl Report { impl Report {
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!(std::io::Error::from(std::io::ErrorKind::UnexpectedEof));
} }
if data[0] == b'{' { let report = if data[0] == b'{' {
// Load as JSON // Load as JSON
Self::from_json(data).map_err(anyhow::Error::new) Self::from_json(data)?
} else { } else {
// Load as binary protobuf // Load as binary protobuf
Self::decode(data).map_err(anyhow::Error::new) Self::decode(data)?
} };
Ok(report)
} }
fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> { fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
@ -37,6 +42,81 @@ impl Report {
} }
} }
} }
pub fn migrate(&mut self) -> Result<()> {
if self.version == 0 {
self.migrate_v0()?;
}
if self.version != REPORT_VERSION {
bail!("Unsupported report version: {}", self.version);
}
Ok(())
}
fn migrate_v0(&mut self) -> Result<()> {
let Some(measures) = &mut self.measures else {
bail!("Missing measures in report");
};
for unit in &mut self.units {
let Some(unit_measures) = &mut unit.measures else {
bail!("Missing measures in report unit");
};
let Some(metadata) = &mut unit.metadata else {
bail!("Missing metadata in report unit");
};
if metadata.module_name.is_some() || metadata.module_id.is_some() {
metadata.progress_categories = vec!["modules".to_string()];
} else {
metadata.progress_categories = vec!["dol".to_string()];
}
if metadata.complete.unwrap_or(false) {
unit_measures.complete_code = unit_measures.total_code;
unit_measures.complete_data = unit_measures.total_data;
unit_measures.complete_code_percent = 100.0;
unit_measures.complete_data_percent = 100.0;
} else {
unit_measures.complete_code = 0;
unit_measures.complete_data = 0;
unit_measures.complete_code_percent = 0.0;
unit_measures.complete_data_percent = 0.0;
}
measures.complete_code += unit_measures.complete_code;
measures.complete_data += unit_measures.complete_data;
}
measures.calc_matched_percent();
self.version = 1;
Ok(())
}
pub fn calculate_progress_categories(&mut self) {
for unit in &self.units {
let Some(metadata) = unit.metadata.as_ref() else {
continue;
};
let Some(measures) = unit.measures.as_ref() else {
continue;
};
for category_id in &metadata.progress_categories {
let category = match self.categories.iter_mut().find(|c| &c.id == category_id) {
Some(category) => category,
None => {
self.categories.push(ReportCategory {
id: category_id.clone(),
name: String::new(),
measures: Some(Default::default()),
});
self.categories.last_mut().unwrap()
}
};
*category.measures.get_or_insert_with(Default::default) += *measures;
}
}
for category in &mut self.categories {
let measures = category.measures.get_or_insert_with(Default::default);
measures.calc_fuzzy_match_percent();
measures.calc_matched_percent();
}
}
} }
impl Measures { impl Measures {
@ -66,6 +146,16 @@ impl Measures {
} else { } else {
self.matched_functions as f32 / self.total_functions as f32 * 100.0 self.matched_functions as f32 / self.total_functions as f32 * 100.0
}; };
self.complete_code_percent = if self.total_code == 0 {
100.0
} else {
self.complete_code as f32 / self.total_code as f32 * 100.0
};
self.complete_data_percent = if self.total_data == 0 {
100.0
} else {
self.complete_data as f32 / self.total_data as f32 * 100.0
};
} }
} }
@ -75,19 +165,27 @@ impl From<&ReportItem> for ChangeItemInfo {
} }
} }
impl AddAssign for Measures {
fn add_assign(&mut self, other: Self) {
self.fuzzy_match_percent += other.fuzzy_match_percent * other.total_code as f32;
self.total_code += other.total_code;
self.matched_code += other.matched_code;
self.total_data += other.total_data;
self.matched_data += other.matched_data;
self.total_functions += other.total_functions;
self.matched_functions += other.matched_functions;
self.complete_code += other.complete_code;
self.complete_data += other.complete_data;
}
}
/// Allows [collect](Iterator::collect) to be used on an iterator of [Measures]. /// Allows [collect](Iterator::collect) to be used on an iterator of [Measures].
impl FromIterator<Measures> for Measures { impl FromIterator<Measures> for Measures {
fn from_iter<T>(iter: T) -> Self fn from_iter<T>(iter: T) -> Self
where T: IntoIterator<Item = Measures> { where T: IntoIterator<Item = Measures> {
let mut measures = Measures::default(); let mut measures = Measures::default();
for other in iter { for other in iter {
measures.fuzzy_match_percent += other.fuzzy_match_percent * other.total_code as f32; measures += other;
measures.total_code += other.total_code;
measures.matched_code += other.matched_code;
measures.total_data += other.total_data;
measures.matched_data += other.matched_data;
measures.total_functions += other.total_functions;
measures.matched_functions += other.matched_functions;
} }
measures.calc_fuzzy_match_percent(); measures.calc_fuzzy_match_percent();
measures.calc_matched_percent(); measures.calc_matched_percent();
@ -125,8 +223,10 @@ impl From<LegacyReport> for Report {
total_functions: value.total_functions, total_functions: value.total_functions,
matched_functions: value.matched_functions, matched_functions: value.matched_functions,
matched_functions_percent: value.matched_functions_percent, matched_functions_percent: value.matched_functions_percent,
..Default::default()
}), }),
units: value.units.into_iter().map(ReportUnit::from).collect(), units: value.units.into_iter().map(ReportUnit::from).collect::<Vec<_>>(),
..Default::default()
} }
} }
} }

View File

@ -31,6 +31,8 @@ pub struct ProjectConfig {
pub watch_patterns: Option<Vec<Glob>>, pub watch_patterns: Option<Vec<Glob>>,
#[serde(default, alias = "units")] #[serde(default, alias = "units")]
pub objects: Vec<ProjectObject>, pub objects: Vec<ProjectObject>,
#[serde(default)]
pub progress_categories: Vec<ProjectProgressCategory>,
} }
#[derive(Default, Clone, serde::Deserialize)] #[derive(Default, Clone, serde::Deserialize)]
@ -44,11 +46,37 @@ pub struct ProjectObject {
#[serde(default)] #[serde(default)]
pub base_path: Option<PathBuf>, pub base_path: Option<PathBuf>,
#[serde(default)] #[serde(default)]
#[deprecated(note = "Use metadata.reverse_fn_order")]
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
#[serde(default)] #[serde(default)]
#[deprecated(note = "Use metadata.complete")]
pub complete: Option<bool>, pub complete: Option<bool>,
#[serde(default)] #[serde(default)]
pub scratch: Option<ScratchConfig>, pub scratch: Option<ScratchConfig>,
#[serde(default)]
pub metadata: Option<ProjectObjectMetadata>,
}
#[derive(Default, Clone, serde::Deserialize)]
pub struct ProjectObjectMetadata {
#[serde(default)]
pub complete: Option<bool>,
#[serde(default)]
pub reverse_fn_order: Option<bool>,
#[serde(default)]
pub source_path: Option<String>,
#[serde(default)]
pub progress_categories: Option<Vec<String>>,
#[serde(default)]
pub auto_generated: Option<bool>,
}
#[derive(Default, Clone, serde::Deserialize)]
pub struct ProjectProgressCategory {
#[serde(default)]
pub id: String,
#[serde(default)]
pub name: String,
} }
impl ProjectObject { impl ProjectObject {
@ -82,6 +110,16 @@ impl ProjectObject {
self.base_path = Some(project_dir.join(path)); self.base_path = Some(project_dir.join(path));
} }
} }
pub fn complete(&self) -> Option<bool> {
#[allow(deprecated)]
self.metadata.as_ref().and_then(|m| m.complete).or(self.complete)
}
pub fn reverse_fn_order(&self) -> Option<bool> {
#[allow(deprecated)]
self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order)
}
} }
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]

View File

@ -1,10 +1,11 @@
#[cfg(feature = "any-arch")]
pub mod arch; pub mod arch;
#[cfg(feature = "bindings")]
pub mod bindings; pub mod bindings;
#[cfg(feature = "config")] #[cfg(feature = "config")]
pub mod config; pub mod config;
#[cfg(feature = "any-arch")]
pub mod diff; pub mod diff;
#[cfg(feature = "any-arch")]
pub mod obj; pub mod obj;
pub mod util; pub mod util;
#[cfg(not(feature = "any-arch"))]
compile_error!("At least one architecture feature must be enabled.");

View File

@ -9,7 +9,7 @@ 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(crate) 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> LowerHex for ReallySigned<N> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {

View File

@ -365,7 +365,7 @@ fn display_object(
let selected = matches!(selected_obj, Some(obj) if obj.name == object_name); let selected = matches!(selected_obj, Some(obj) if obj.name == object_name);
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 {
@ -392,8 +392,8 @@ fn display_object(
name: object_name.to_string(), name: object_name.to_string(),
target_path: object.target_path.clone(), target_path: object.target_path.clone(),
base_path: object.base_path.clone(), base_path: object.base_path.clone(),
reverse_fn_order: object.reverse_fn_order, reverse_fn_order: object.reverse_fn_order(),
complete: object.complete, complete: object.complete(),
scratch: object.scratch.clone(), scratch: object.scratch.clone(),
}); });
} }
@ -470,7 +470,7 @@ fn filter_node(
if (search.is_empty() || name.to_ascii_lowercase().contains(search)) if (search.is_empty() || name.to_ascii_lowercase().contains(search))
&& (!filter_diffable && (!filter_diffable
|| (object.base_path.is_some() && object.target_path.is_some())) || (object.base_path.is_some() && object.target_path.is_some()))
&& (!filter_incomplete || matches!(object.complete, None | Some(false))) && (!filter_incomplete || matches!(object.complete(), None | Some(false)))
{ {
Some(node.clone()) Some(node.clone())
} else { } else {