1#![allow(clippy::useless_borrows_in_formatting)] use alloc::{
4 string::{String, ToString},
5 vec,
6 vec::Vec,
7};
8use core::ops::AddAssign;
9
10use anyhow::{Result, bail};
11use prost::Message;
12
13include!(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 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 #[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 Self::decode(data).map_err(|e| anyhow::Error::msg(e.to_string()))?
37 };
38 Ok(report)
39 }
40
41 #[cfg(feature = "serde")]
42 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 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 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 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 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 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 pub fn split(self) -> Vec<(String, Report)> {
181 let mut reports = Vec::new();
182 let mut units = self.units.into_iter().map(Some).collect::<Vec<_>>();
184 for category in &self.categories {
185 if category.id.contains(".") {
186 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 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 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 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 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 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
305impl 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#[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}