struct_audit/
diff.rs

1use crate::types::StructLayout;
2use serde::Serialize;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize)]
6pub struct DiffResult {
7    pub added: Vec<StructSummary>,
8    pub removed: Vec<StructSummary>,
9    pub changed: Vec<StructChange>,
10    pub unchanged_count: usize,
11}
12
13#[derive(Debug, Clone, Serialize)]
14pub struct StructSummary {
15    pub name: String,
16    pub size: u64,
17    pub padding_bytes: u64,
18}
19
20#[derive(Debug, Clone, Serialize)]
21pub struct StructChange {
22    pub name: String,
23    pub old_size: u64,
24    pub new_size: u64,
25    pub size_delta: i64,
26    pub old_padding: u64,
27    pub new_padding: u64,
28    pub padding_delta: i64,
29    pub member_changes: Vec<MemberChange>,
30}
31
32#[derive(Debug, Clone, Serialize)]
33pub struct MemberChange {
34    pub kind: MemberChangeKind,
35    pub name: String,
36    pub details: String,
37}
38
39#[derive(Debug, Clone, Serialize, PartialEq)]
40pub enum MemberChangeKind {
41    Added,
42    Removed,
43    OffsetChanged,
44    SizeChanged,
45    TypeChanged,
46}
47
48impl DiffResult {
49    pub fn has_changes(&self) -> bool {
50        !self.added.is_empty() || !self.removed.is_empty() || !self.changed.is_empty()
51    }
52
53    pub fn has_regressions(&self) -> bool {
54        self.changed.iter().any(|c| c.size_delta > 0 || c.padding_delta > 0)
55    }
56}
57
58pub fn diff_layouts(old: &[StructLayout], new: &[StructLayout]) -> DiffResult {
59    let old_map: HashMap<&str, &StructLayout> = old.iter().map(|s| (s.name.as_str(), s)).collect();
60    let new_map: HashMap<&str, &StructLayout> = new.iter().map(|s| (s.name.as_str(), s)).collect();
61
62    let mut added = Vec::new();
63    let mut removed = Vec::new();
64    let mut changed = Vec::new();
65    let mut unchanged_count = 0;
66
67    for (name, old_struct) in &old_map {
68        if !new_map.contains_key(name) {
69            removed.push(StructSummary {
70                name: name.to_string(),
71                size: old_struct.size,
72                padding_bytes: old_struct.metrics.padding_bytes,
73            });
74        }
75    }
76
77    for (name, new_struct) in &new_map {
78        match old_map.get(name) {
79            None => {
80                added.push(StructSummary {
81                    name: name.to_string(),
82                    size: new_struct.size,
83                    padding_bytes: new_struct.metrics.padding_bytes,
84                });
85            }
86            Some(old_struct) => {
87                if let Some(change) = diff_struct(old_struct, new_struct) {
88                    changed.push(change);
89                } else {
90                    unchanged_count += 1;
91                }
92            }
93        }
94    }
95
96    added.sort_by(|a, b| a.name.cmp(&b.name));
97    removed.sort_by(|a, b| a.name.cmp(&b.name));
98    changed.sort_by(|a, b| a.name.cmp(&b.name));
99
100    DiffResult { added, removed, changed, unchanged_count }
101}
102
103fn diff_struct(old: &StructLayout, new: &StructLayout) -> Option<StructChange> {
104    let size_delta = i64::try_from(new.size)
105        .unwrap_or(i64::MAX)
106        .saturating_sub(i64::try_from(old.size).unwrap_or(i64::MAX));
107    let padding_delta = i64::try_from(new.metrics.padding_bytes)
108        .unwrap_or(i64::MAX)
109        .saturating_sub(i64::try_from(old.metrics.padding_bytes).unwrap_or(i64::MAX));
110
111    let mut member_changes = Vec::new();
112
113    let old_members: HashMap<&str, _> = old.members.iter().map(|m| (m.name.as_str(), m)).collect();
114    let new_members: HashMap<&str, _> = new.members.iter().map(|m| (m.name.as_str(), m)).collect();
115
116    for (name, old_member) in &old_members {
117        if !new_members.contains_key(name) {
118            member_changes.push(MemberChange {
119                kind: MemberChangeKind::Removed,
120                name: name.to_string(),
121                details: format!("offset {:?}, size {:?}", old_member.offset, old_member.size),
122            });
123        }
124    }
125
126    for (name, new_member) in &new_members {
127        match old_members.get(name) {
128            None => {
129                member_changes.push(MemberChange {
130                    kind: MemberChangeKind::Added,
131                    name: name.to_string(),
132                    details: format!("offset {:?}, size {:?}", new_member.offset, new_member.size),
133                });
134            }
135            Some(old_member) => {
136                if old_member.offset != new_member.offset {
137                    member_changes.push(MemberChange {
138                        kind: MemberChangeKind::OffsetChanged,
139                        name: name.to_string(),
140                        details: format!("{:?} -> {:?}", old_member.offset, new_member.offset),
141                    });
142                }
143                if old_member.size != new_member.size {
144                    member_changes.push(MemberChange {
145                        kind: MemberChangeKind::SizeChanged,
146                        name: name.to_string(),
147                        details: format!("{:?} -> {:?}", old_member.size, new_member.size),
148                    });
149                }
150                if old_member.type_name != new_member.type_name {
151                    member_changes.push(MemberChange {
152                        kind: MemberChangeKind::TypeChanged,
153                        name: name.to_string(),
154                        details: format!("{} -> {}", old_member.type_name, new_member.type_name),
155                    });
156                }
157            }
158        }
159    }
160
161    if size_delta == 0 && padding_delta == 0 && member_changes.is_empty() {
162        return None;
163    }
164
165    Some(StructChange {
166        name: old.name.clone(),
167        old_size: old.size,
168        new_size: new.size,
169        size_delta,
170        old_padding: old.metrics.padding_bytes,
171        new_padding: new.metrics.padding_bytes,
172        padding_delta,
173        member_changes,
174    })
175}