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        // Regressions: size increased or padding increased
55        self.changed.iter().any(|c| c.size_delta > 0 || c.padding_delta > 0)
56    }
57}
58
59pub fn diff_layouts(old: &[StructLayout], new: &[StructLayout]) -> DiffResult {
60    let old_map: HashMap<&str, &StructLayout> = old.iter().map(|s| (s.name.as_str(), s)).collect();
61    let new_map: HashMap<&str, &StructLayout> = new.iter().map(|s| (s.name.as_str(), s)).collect();
62
63    let mut added = Vec::new();
64    let mut removed = Vec::new();
65    let mut changed = Vec::new();
66    let mut unchanged_count = 0;
67
68    // Find removed structs
69    for (name, old_struct) in &old_map {
70        if !new_map.contains_key(name) {
71            removed.push(StructSummary {
72                name: name.to_string(),
73                size: old_struct.size,
74                padding_bytes: old_struct.metrics.padding_bytes,
75            });
76        }
77    }
78
79    // Find added and changed structs
80    for (name, new_struct) in &new_map {
81        match old_map.get(name) {
82            None => {
83                added.push(StructSummary {
84                    name: name.to_string(),
85                    size: new_struct.size,
86                    padding_bytes: new_struct.metrics.padding_bytes,
87                });
88            }
89            Some(old_struct) => {
90                if let Some(change) = diff_struct(old_struct, new_struct) {
91                    changed.push(change);
92                } else {
93                    unchanged_count += 1;
94                }
95            }
96        }
97    }
98
99    // Sort for consistent output
100    added.sort_by(|a, b| a.name.cmp(&b.name));
101    removed.sort_by(|a, b| a.name.cmp(&b.name));
102    changed.sort_by(|a, b| a.name.cmp(&b.name));
103
104    DiffResult { added, removed, changed, unchanged_count }
105}
106
107fn diff_struct(old: &StructLayout, new: &StructLayout) -> Option<StructChange> {
108    // Use saturating conversion to avoid overflow on extremely large u64 values
109    let size_delta = i64::try_from(new.size)
110        .unwrap_or(i64::MAX)
111        .saturating_sub(i64::try_from(old.size).unwrap_or(i64::MAX));
112    let padding_delta = i64::try_from(new.metrics.padding_bytes)
113        .unwrap_or(i64::MAX)
114        .saturating_sub(i64::try_from(old.metrics.padding_bytes).unwrap_or(i64::MAX));
115
116    let mut member_changes = Vec::new();
117
118    let old_members: HashMap<&str, _> = old.members.iter().map(|m| (m.name.as_str(), m)).collect();
119    let new_members: HashMap<&str, _> = new.members.iter().map(|m| (m.name.as_str(), m)).collect();
120
121    // Check for removed members
122    for (name, old_member) in &old_members {
123        if !new_members.contains_key(name) {
124            member_changes.push(MemberChange {
125                kind: MemberChangeKind::Removed,
126                name: name.to_string(),
127                details: format!("offset {:?}, size {:?}", old_member.offset, old_member.size),
128            });
129        }
130    }
131
132    // Check for added and changed members
133    for (name, new_member) in &new_members {
134        match old_members.get(name) {
135            None => {
136                member_changes.push(MemberChange {
137                    kind: MemberChangeKind::Added,
138                    name: name.to_string(),
139                    details: format!("offset {:?}, size {:?}", new_member.offset, new_member.size),
140                });
141            }
142            Some(old_member) => {
143                if old_member.offset != new_member.offset {
144                    member_changes.push(MemberChange {
145                        kind: MemberChangeKind::OffsetChanged,
146                        name: name.to_string(),
147                        details: format!("{:?} -> {:?}", old_member.offset, new_member.offset),
148                    });
149                }
150                if old_member.size != new_member.size {
151                    member_changes.push(MemberChange {
152                        kind: MemberChangeKind::SizeChanged,
153                        name: name.to_string(),
154                        details: format!("{:?} -> {:?}", old_member.size, new_member.size),
155                    });
156                }
157                if old_member.type_name != new_member.type_name {
158                    member_changes.push(MemberChange {
159                        kind: MemberChangeKind::TypeChanged,
160                        name: name.to_string(),
161                        details: format!("{} -> {}", old_member.type_name, new_member.type_name),
162                    });
163                }
164            }
165        }
166    }
167
168    if size_delta == 0 && padding_delta == 0 && member_changes.is_empty() {
169        return None;
170    }
171
172    Some(StructChange {
173        name: old.name.clone(),
174        old_size: old.size,
175        new_size: new.size,
176        size_delta,
177        old_padding: old.metrics.padding_bytes,
178        new_padding: new.metrics.padding_bytes,
179        padding_delta,
180        member_changes,
181    })
182}