Skip to main content

objdiff_core/bindings/
report.rs

1#![allow(clippy::useless_borrows_in_formatting)] // Generated serde code
2
3use alloc::{
4    string::{String, ToString},
5    vec,
6    vec::Vec,
7};
8use core::ops::AddAssign;
9
10use anyhow::{Result, bail};
11use prost::Message;
12
13// Protobuf report types
14include!(concat!(env!("OUT_DIR"), "/objdiff.report.rs"));
15#[cfg(feature = "serde")]
16include!(concat!(env!("OUT_DIR"), "/objdiff.report.serde.rs"));
17
18pub const REPORT_VERSION: u32 = 2;
19
20impl Report {
21    /// Attempts to parse the report as binary protobuf or JSON.
22    pub fn parse(data: &[u8]) -> Result<Self> {
23        if data.is_empty() {
24            bail!("Empty data");
25        }
26        let report = if data[0] == b'{' {
27            // Load as JSON
28            #[cfg(feature = "serde")]
29            {
30                Self::from_json(data)?
31            }
32            #[cfg(not(feature = "serde"))]
33            bail!("JSON report parsing requires the `serde` feature")
34        } else {
35            // Load as binary protobuf
36            Self::decode(data).map_err(|e| anyhow::Error::msg(e.to_string()))?
37        };
38        Ok(report)
39    }
40
41    #[cfg(feature = "serde")]
42    /// Attempts to parse the report as JSON, migrating from the legacy report format if necessary.
43    fn from_json(bytes: &[u8]) -> Result<Self, serde_json::Error> {
44        match serde_json::from_slice::<Self>(bytes) {
45            Ok(report) => Ok(report),
46            Err(e) => {
47                use serde_json::error::Category;
48                match e.classify() {
49                    Category::Io | Category::Eof | Category::Syntax => Err(e),
50                    Category::Data => {
51                        // Try to load as legacy report
52                        match serde_json::from_slice::<LegacyReport>(bytes) {
53                            Ok(legacy_report) => Ok(Report::from(legacy_report)),
54                            Err(_) => Err(e),
55                        }
56                    }
57                }
58            }
59        }
60    }
61
62    /// Migrates the report to the latest version.
63    /// Fails if the report version is newer than supported.
64    pub fn migrate(&mut self) -> Result<()> {
65        if self.version == 0 {
66            self.migrate_v0()?;
67        }
68        if self.version == 1 {
69            self.migrate_v1()?;
70        }
71        if self.version != REPORT_VERSION {
72            bail!("Unsupported report version: {}", self.version);
73        }
74        Ok(())
75    }
76
77    /// Adds `complete_code`, `complete_data`, `complete_code_percent`, and `complete_data_percent`
78    /// to measures, and sets `progress_categories` in unit metadata.
79    fn migrate_v0(&mut self) -> Result<()> {
80        let Some(measures) = &mut self.measures else {
81            bail!("Missing measures in report");
82        };
83        for unit in &mut self.units {
84            let Some(unit_measures) = &mut unit.measures else {
85                bail!("Missing measures in report unit");
86            };
87            let mut complete = false;
88            if let Some(metadata) = &mut unit.metadata {
89                if metadata.module_name.is_some() || metadata.module_id.is_some() {
90                    metadata.progress_categories = vec!["modules".to_string()];
91                } else {
92                    metadata.progress_categories = vec!["dol".to_string()];
93                }
94                complete = metadata.complete.unwrap_or(false);
95            };
96            if complete {
97                unit_measures.complete_code = unit_measures.total_code;
98                unit_measures.complete_data = unit_measures.total_data;
99                unit_measures.complete_code_percent = 100.0;
100                unit_measures.complete_data_percent = 100.0;
101            } else {
102                unit_measures.complete_code = 0;
103                unit_measures.complete_data = 0;
104                unit_measures.complete_code_percent = 0.0;
105                unit_measures.complete_data_percent = 0.0;
106            }
107            measures.complete_code += unit_measures.complete_code;
108            measures.complete_data += unit_measures.complete_data;
109        }
110        measures.calc_matched_percent();
111        self.calculate_progress_categories();
112        self.version = 1;
113        Ok(())
114    }
115
116    /// Adds `total_units` and `complete_units` to measures.
117    fn migrate_v1(&mut self) -> Result<()> {
118        let Some(total_measures) = &mut self.measures else {
119            bail!("Missing measures in report");
120        };
121        for unit in &mut self.units {
122            let Some(measures) = &mut unit.measures else {
123                bail!("Missing measures in report unit");
124            };
125            let complete = unit.metadata.as_ref().and_then(|m| m.complete).unwrap_or(false) as u32;
126            let progress_categories =
127                unit.metadata.as_ref().map(|m| m.progress_categories.as_slice()).unwrap_or(&[]);
128            measures.total_units = 1;
129            measures.complete_units = complete;
130            total_measures.total_units += 1;
131            total_measures.complete_units += complete;
132            for id in progress_categories {
133                if let Some(category) = self.categories.iter_mut().find(|c| &c.id == id) {
134                    let Some(measures) = &mut category.measures else {
135                        bail!("Missing measures in category");
136                    };
137                    measures.total_units += 1;
138                    measures.complete_units += complete;
139                }
140            }
141        }
142        self.version = 2;
143        Ok(())
144    }
145
146    /// Calculate progress categories based on unit metadata.
147    pub fn calculate_progress_categories(&mut self) {
148        for unit in &self.units {
149            let Some(metadata) = unit.metadata.as_ref() else {
150                continue;
151            };
152            let Some(measures) = unit.measures.as_ref() else {
153                continue;
154            };
155            for category_id in &metadata.progress_categories {
156                let category = match self.categories.iter_mut().find(|c| &c.id == category_id) {
157                    Some(category) => category,
158                    None => {
159                        self.categories.push(ReportCategory {
160                            id: category_id.clone(),
161                            name: String::new(),
162                            measures: Some(Default::default()),
163                        });
164                        self.categories.last_mut().unwrap()
165                    }
166                };
167                *category.measures.get_or_insert_with(Default::default) += *measures;
168            }
169        }
170        for category in &mut self.categories {
171            let measures = category.measures.get_or_insert_with(Default::default);
172            measures.calc_fuzzy_match_percent();
173            measures.calc_matched_percent();
174        }
175    }
176
177    /// Split the report into multiple reports based on progress categories.
178    /// Assumes progress categories are in the format `version`, `version.category`.
179    /// This is a hack for projects that generate all versions in a single report.
180    pub fn split(self) -> Vec<(String, Report)> {
181        let mut reports = Vec::new();
182        // Map units to Option to allow taking ownership
183        let mut units = self.units.into_iter().map(Some).collect::<Vec<_>>();
184        for category in &self.categories {
185            if category.id.contains(".") {
186                // Skip subcategories
187                continue;
188            }
189            fn is_sub_category(id: &str, parent: &str, sep: char) -> bool {
190                id.starts_with(parent) && id.get(parent.len()..).is_some_and(|s| s.starts_with(sep))
191            }
192            let mut sub_categories = self
193                .categories
194                .iter()
195                .filter(|c| is_sub_category(&c.id, &category.id, '.'))
196                .cloned()
197                .collect::<Vec<_>>();
198            // Remove category prefix
199            for sub_category in &mut sub_categories {
200                sub_category.id = sub_category.id[category.id.len() + 1..].to_string();
201            }
202            let mut sub_units = units
203                .iter_mut()
204                .filter_map(|opt| {
205                    let unit = opt.as_mut()?;
206                    let metadata = unit.metadata.as_ref()?;
207                    if metadata.progress_categories.contains(&category.id) {
208                        opt.take()
209                    } else {
210                        None
211                    }
212                })
213                .collect::<Vec<_>>();
214            for sub_unit in &mut sub_units {
215                // Remove leading version/ from unit name
216                if let Some(name) =
217                    sub_unit.name.strip_prefix(&category.id).and_then(|s| s.strip_prefix('/'))
218                {
219                    sub_unit.name = name.to_string();
220                }
221                // Filter progress categories
222                let Some(metadata) = sub_unit.metadata.as_mut() else {
223                    continue;
224                };
225                metadata.progress_categories = metadata
226                    .progress_categories
227                    .iter()
228                    .filter(|c| is_sub_category(c, &category.id, '.'))
229                    .map(|c| c[category.id.len() + 1..].to_string())
230                    .collect();
231            }
232            reports.push((category.id.clone(), Report {
233                measures: category.measures,
234                units: sub_units,
235                version: self.version,
236                categories: sub_categories,
237            }));
238        }
239        reports
240    }
241}
242
243impl Measures {
244    /// Average the fuzzy match percentage over total code bytes.
245    pub fn calc_fuzzy_match_percent(&mut self) {
246        if self.total_code == 0 {
247            self.fuzzy_match_percent = 100.0;
248        } else {
249            self.fuzzy_match_percent /= self.total_code as f32;
250        }
251    }
252
253    /// Calculate the percentage of matched code, data, and functions.
254    pub fn calc_matched_percent(&mut self) {
255        self.matched_code_percent = if self.total_code == 0 {
256            100.0
257        } else {
258            self.matched_code as f32 / self.total_code as f32 * 100.0
259        };
260        self.matched_data_percent = if self.total_data == 0 {
261            100.0
262        } else {
263            self.matched_data as f32 / self.total_data as f32 * 100.0
264        };
265        self.matched_functions_percent = if self.total_functions == 0 {
266            100.0
267        } else {
268            self.matched_functions as f32 / self.total_functions as f32 * 100.0
269        };
270        self.complete_code_percent = if self.total_code == 0 {
271            100.0
272        } else {
273            self.complete_code as f32 / self.total_code as f32 * 100.0
274        };
275        self.complete_data_percent = if self.total_data == 0 {
276            100.0
277        } else {
278            self.complete_data as f32 / self.total_data as f32 * 100.0
279        };
280    }
281}
282
283impl From<&ReportItem> for ChangeItemInfo {
284    fn from(value: &ReportItem) -> Self {
285        Self { fuzzy_match_percent: value.fuzzy_match_percent, size: value.size }
286    }
287}
288
289impl AddAssign for Measures {
290    fn add_assign(&mut self, other: Self) {
291        self.fuzzy_match_percent += other.fuzzy_match_percent * other.total_code as f32;
292        self.total_code += other.total_code;
293        self.matched_code += other.matched_code;
294        self.total_data += other.total_data;
295        self.matched_data += other.matched_data;
296        self.total_functions += other.total_functions;
297        self.matched_functions += other.matched_functions;
298        self.complete_code += other.complete_code;
299        self.complete_data += other.complete_data;
300        self.total_units += other.total_units;
301        self.complete_units += other.complete_units;
302    }
303}
304
305/// Allows [collect](Iterator::collect) to be used on an iterator of [Measures].
306impl FromIterator<Measures> for Measures {
307    fn from_iter<T>(iter: T) -> Self
308    where T: IntoIterator<Item = Measures> {
309        let mut measures = Measures::default();
310        for other in iter {
311            measures += other;
312        }
313        measures.calc_fuzzy_match_percent();
314        measures.calc_matched_percent();
315        measures
316    }
317}
318
319// Older JSON report types
320#[derive(Debug, Clone, Default)]
321#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
322struct LegacyReport {
323    fuzzy_match_percent: f32,
324    total_code: u64,
325    matched_code: u64,
326    matched_code_percent: f32,
327    total_data: u64,
328    matched_data: u64,
329    matched_data_percent: f32,
330    total_functions: u32,
331    matched_functions: u32,
332    matched_functions_percent: f32,
333    units: Vec<LegacyReportUnit>,
334}
335
336impl From<LegacyReport> for Report {
337    fn from(value: LegacyReport) -> Self {
338        Self {
339            measures: Some(Measures {
340                fuzzy_match_percent: value.fuzzy_match_percent,
341                total_code: value.total_code,
342                matched_code: value.matched_code,
343                matched_code_percent: value.matched_code_percent,
344                total_data: value.total_data,
345                matched_data: value.matched_data,
346                matched_data_percent: value.matched_data_percent,
347                total_functions: value.total_functions,
348                matched_functions: value.matched_functions,
349                matched_functions_percent: value.matched_functions_percent,
350                ..Default::default()
351            }),
352            units: value.units.into_iter().map(ReportUnit::from).collect::<Vec<_>>(),
353            ..Default::default()
354        }
355    }
356}
357
358#[derive(Debug, Clone, Default)]
359#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
360struct LegacyReportUnit {
361    name: String,
362    fuzzy_match_percent: f32,
363    total_code: u64,
364    matched_code: u64,
365    total_data: u64,
366    matched_data: u64,
367    total_functions: u32,
368    matched_functions: u32,
369    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
370    complete: Option<bool>,
371    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
372    module_name: Option<String>,
373    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
374    module_id: Option<u32>,
375    sections: Vec<LegacyReportItem>,
376    functions: Vec<LegacyReportItem>,
377}
378
379impl From<LegacyReportUnit> for ReportUnit {
380    fn from(value: LegacyReportUnit) -> Self {
381        let mut measures = Measures {
382            fuzzy_match_percent: value.fuzzy_match_percent,
383            total_code: value.total_code,
384            matched_code: value.matched_code,
385            total_data: value.total_data,
386            matched_data: value.matched_data,
387            total_functions: value.total_functions,
388            matched_functions: value.matched_functions,
389            ..Default::default()
390        };
391        measures.calc_matched_percent();
392        Self {
393            name: value.name.clone(),
394            measures: Some(measures),
395            sections: value.sections.into_iter().map(ReportItem::from).collect(),
396            functions: value.functions.into_iter().map(ReportItem::from).collect(),
397            metadata: Some(ReportUnitMetadata {
398                complete: value.complete,
399                module_name: value.module_name.clone(),
400                module_id: value.module_id,
401                ..Default::default()
402            }),
403        }
404    }
405}
406
407#[derive(Debug, Clone, Default)]
408#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
409struct LegacyReportItem {
410    name: String,
411    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
412    demangled_name: Option<String>,
413    #[cfg_attr(
414        feature = "serde",
415        serde(
416            default,
417            skip_serializing_if = "Option::is_none",
418            serialize_with = "serialize_hex",
419            deserialize_with = "deserialize_hex"
420        )
421    )]
422    address: Option<u64>,
423    size: u64,
424    fuzzy_match_percent: f32,
425}
426
427impl From<LegacyReportItem> for ReportItem {
428    fn from(value: LegacyReportItem) -> Self {
429        Self {
430            name: value.name,
431            size: value.size,
432            fuzzy_match_percent: value.fuzzy_match_percent,
433            metadata: Some(ReportItemMetadata {
434                demangled_name: value.demangled_name,
435                virtual_address: value.address,
436            }),
437            address: None,
438        }
439    }
440}
441
442#[cfg(feature = "serde")]
443fn serialize_hex<S>(x: &Option<u64>, s: S) -> Result<S::Ok, S::Error>
444where S: serde::Serializer {
445    if let Some(x) = x { s.serialize_str(&format!("{x:#x}")) } else { s.serialize_none() }
446}
447
448#[cfg(feature = "serde")]
449fn deserialize_hex<'de, D>(d: D) -> Result<Option<u64>, D::Error>
450where D: serde::Deserializer<'de> {
451    use serde::Deserialize;
452    let s = String::deserialize(d)?;
453    if s.is_empty() {
454        Ok(None)
455    } else if !s.starts_with("0x") {
456        Err(serde::de::Error::custom("expected hex string"))
457    } else {
458        u64::from_str_radix(&s[2..], 16).map(Some).map_err(serde::de::Error::custom)
459    }
460}