diff --git a/objdiff-cli/src/cmd/report.rs b/objdiff-cli/src/cmd/report.rs index 9b8f191..0c7b563 100644 --- a/objdiff-cli/src/cmd/report.rs +++ b/objdiff-cli/src/cmd/report.rs @@ -11,7 +11,8 @@ use argp::FromArgs; use objdiff_core::{ bindings::report::{ ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, Report, - ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata, + ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata, + REPORT_VERSION, }, config::ProjectObject, diff, obj, @@ -129,7 +130,17 @@ fn generate(args: GenerateArgs) -> Result<()> { units = vec.into_iter().flatten().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(); info!("Report generated in {}.{:03}s", duration.as_secs(), duration.subsec_millis()); write_output(&report, args.output.as_deref(), output_format)?; @@ -145,7 +156,7 @@ fn report_object( ) -> Result> { object.resolve_paths(project_dir, target_dir, base_dir); 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()); return Ok(None); } @@ -173,13 +184,19 @@ fn report_object( let result = diff::diff_objs(&config, target.as_ref(), base.as_ref(), None)?; let metadata = ReportUnitMetadata { - complete: object.complete, + complete: object.complete(), module_name: target .as_ref() .and_then(|o| o.split_meta.as_ref()) .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), - 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 sections = vec![]; @@ -191,7 +208,7 @@ fn report_object( let section_match_percent = section_diff.match_percent.unwrap_or_else(|| { // Support cases where we don't have a target object, // assume complete means 100% match - if object.complete == Some(true) { + if object.complete().unwrap_or(false) { 100.0 } else { 0.0 @@ -233,7 +250,7 @@ fn report_object( let match_percent = symbol_diff.match_percent.unwrap_or_else(|| { // Support cases where we don't have a target object, // assume complete means 100% match - if object.complete == Some(true) { + if object.complete().unwrap_or(false) { 100.0 } else { 0.0 @@ -259,6 +276,10 @@ fn report_object( 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_matched_percent(); Ok(Some(ReportUnit { diff --git a/objdiff-core/Cargo.toml b/objdiff-core/Cargo.toml index 1014403..8caf4aa 100644 --- a/objdiff-core/Cargo.toml +++ b/objdiff-core/Cargo.toml @@ -15,7 +15,7 @@ A local diffing tool for decompilation projects. crate-type = ["cdylib", "rlib"] [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 config = ["globset", "semver", "serde_json", "serde_yaml"] dwarf = ["gimli"] @@ -23,7 +23,8 @@ mips = ["any-arch", "rabbitizer"] ppc = ["any-arch", "cwdemangle", "cwextab", "ppc750cl"] x86 = ["any-arch", "cpp_demangle", "iced-x86", "msvc-demangler"] 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] anyhow = "1.0.82" @@ -34,8 +35,8 @@ log = "0.4.21" memmap2 = "0.9.4" num-traits = "0.2.18" object = { version = "0.35.0", features = ["read_core", "std", "elf", "pe"], default-features = false } -pbjson = "0.7.0" -prost = "0.13.1" +pbjson = { version = "0.7.0", optional = true } +prost = { version = "0.13.1", optional = true } serde = { version = "1", features = ["derive"] } similar = { version = "2.5.0", default-features = false } strum = { version = "0.26.2", features = ["derive"] } diff --git a/objdiff-core/protos/proto_descriptor.bin b/objdiff-core/protos/proto_descriptor.bin index 46866c0..4551eea 100644 Binary files a/objdiff-core/protos/proto_descriptor.bin and b/objdiff-core/protos/proto_descriptor.bin differ diff --git a/objdiff-core/protos/report.proto b/objdiff-core/protos/report.proto index ebc0901..3a4bbe7 100644 --- a/objdiff-core/protos/report.proto +++ b/objdiff-core/protos/report.proto @@ -24,6 +24,14 @@ message Measures { uint32 matched_functions = 9; // Fully matched functions percent 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 @@ -32,6 +40,19 @@ message Report { Measures measures = 1; // Units within this report 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) @@ -58,6 +79,10 @@ message ReportUnitMetadata { optional uint32 module_id = 3; // The path to the source file of this unit 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 diff --git a/objdiff-core/src/bindings/mod.rs b/objdiff-core/src/bindings/mod.rs index 3eea4e2..44e979e 100644 --- a/objdiff-core/src/bindings/mod.rs +++ b/objdiff-core/src/bindings/mod.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "any-arch")] pub mod diff; pub mod report; #[cfg(feature = "wasm")] diff --git a/objdiff-core/src/bindings/report.rs b/objdiff-core/src/bindings/report.rs index 8b4488b..94557c7 100644 --- a/objdiff-core/src/bindings/report.rs +++ b/objdiff-core/src/bindings/report.rs @@ -1,3 +1,5 @@ +use std::ops::AddAssign; + use anyhow::{bail, Result}; use prost::Message; 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.serde.rs")); +pub const REPORT_VERSION: u32 = 1; + 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'{' { + let report = if data[0] == b'{' { // Load as JSON - Self::from_json(data).map_err(anyhow::Error::new) + Self::from_json(data)? } else { // Load as binary protobuf - Self::decode(data).map_err(anyhow::Error::new) - } + Self::decode(data)? + }; + Ok(report) } fn from_json(bytes: &[u8]) -> Result { @@ -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 { @@ -66,6 +146,16 @@ impl Measures { } else { 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]. impl FromIterator for Measures { fn from_iter(iter: T) -> Self where T: IntoIterator { let mut measures = Measures::default(); for other in iter { - measures.fuzzy_match_percent += other.fuzzy_match_percent * other.total_code as f32; - 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 += other; } measures.calc_fuzzy_match_percent(); measures.calc_matched_percent(); @@ -125,8 +223,10 @@ impl From for Report { total_functions: value.total_functions, matched_functions: value.matched_functions, 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::>(), + ..Default::default() } } } diff --git a/objdiff-core/src/config/mod.rs b/objdiff-core/src/config/mod.rs index f1fd1fb..6a89986 100644 --- a/objdiff-core/src/config/mod.rs +++ b/objdiff-core/src/config/mod.rs @@ -31,6 +31,8 @@ pub struct ProjectConfig { pub watch_patterns: Option>, #[serde(default, alias = "units")] pub objects: Vec, + #[serde(default)] + pub progress_categories: Vec, } #[derive(Default, Clone, serde::Deserialize)] @@ -44,11 +46,37 @@ pub struct ProjectObject { #[serde(default)] pub base_path: Option, #[serde(default)] + #[deprecated(note = "Use metadata.reverse_fn_order")] pub reverse_fn_order: Option, #[serde(default)] + #[deprecated(note = "Use metadata.complete")] pub complete: Option, #[serde(default)] pub scratch: Option, + #[serde(default)] + pub metadata: Option, +} + +#[derive(Default, Clone, serde::Deserialize)] +pub struct ProjectObjectMetadata { + #[serde(default)] + pub complete: Option, + #[serde(default)] + pub reverse_fn_order: Option, + #[serde(default)] + pub source_path: Option, + #[serde(default)] + pub progress_categories: Option>, + #[serde(default)] + pub auto_generated: Option, +} + +#[derive(Default, Clone, serde::Deserialize)] +pub struct ProjectProgressCategory { + #[serde(default)] + pub id: String, + #[serde(default)] + pub name: String, } impl ProjectObject { @@ -82,6 +110,16 @@ impl ProjectObject { self.base_path = Some(project_dir.join(path)); } } + + pub fn complete(&self) -> Option { + #[allow(deprecated)] + self.metadata.as_ref().and_then(|m| m.complete).or(self.complete) + } + + pub fn reverse_fn_order(&self) -> Option { + #[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)] diff --git a/objdiff-core/src/lib.rs b/objdiff-core/src/lib.rs index 48f1e72..02df5a4 100644 --- a/objdiff-core/src/lib.rs +++ b/objdiff-core/src/lib.rs @@ -1,10 +1,11 @@ +#[cfg(feature = "any-arch")] pub mod arch; +#[cfg(feature = "bindings")] pub mod bindings; #[cfg(feature = "config")] pub mod config; +#[cfg(feature = "any-arch")] pub mod diff; +#[cfg(feature = "any-arch")] pub mod obj; pub mod util; - -#[cfg(not(feature = "any-arch"))] -compile_error!("At least one architecture feature must be enabled."); diff --git a/objdiff-core/src/util.rs b/objdiff-core/src/util.rs index 5f99e94..e46d136 100644 --- a/objdiff-core/src/util.rs +++ b/objdiff-core/src/util.rs @@ -9,7 +9,7 @@ use num_traits::PrimInt; use object::{Endian, Object}; // https://stackoverflow.com/questions/44711012/how-do-i-format-a-signed-integer-to-a-sign-aware-hexadecimal-representation -pub(crate) struct ReallySigned(pub(crate) N); +pub struct ReallySigned(pub(crate) N); impl LowerHex for ReallySigned { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { diff --git a/objdiff-gui/src/views/config.rs b/objdiff-gui/src/views/config.rs index f432f5a..812595c 100644 --- a/objdiff-gui/src/views/config.rs +++ b/objdiff-gui/src/views/config.rs @@ -365,7 +365,7 @@ fn display_object( let selected = matches!(selected_obj, Some(obj) if obj.name == object_name); let color = if selected { appearance.emphasized_text_color - } else if let Some(complete) = object.complete { + } else if let Some(complete) = object.complete() { if complete { appearance.insert_color } else { @@ -392,8 +392,8 @@ fn display_object( name: object_name.to_string(), target_path: object.target_path.clone(), base_path: object.base_path.clone(), - reverse_fn_order: object.reverse_fn_order, - complete: object.complete, + reverse_fn_order: object.reverse_fn_order(), + complete: object.complete(), scratch: object.scratch.clone(), }); } @@ -470,7 +470,7 @@ fn filter_node( if (search.is_empty() || name.to_ascii_lowercase().contains(search)) && (!filter_diffable || (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()) } else {