1use serde::{Deserialize, Serialize};
2
3use super::layout::{Provenance, StorageEntry, StorageLayout, StorageType};
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
6pub enum DiffStatus {
7 Same,
8 Added,
9 Removed,
10 TypeChanged,
11 PackingChanged,
12}
13
14#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
15pub enum SeverityGrade {
16 Ok,
17 Risk,
18 Break,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct DiffEntry {
23 pub slot: u128,
24 pub old: Option<StorageEntry>,
25 pub new: Option<StorageEntry>,
26 pub status: DiffStatus,
27 pub grade: SeverityGrade,
28 pub provenance_old: Option<Provenance>,
29 pub provenance_new: Option<Provenance>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33pub struct Summary {
34 pub added: usize,
35 pub removed: usize,
36 pub type_changed: usize,
37 pub packing_changed: usize,
38 pub same: usize,
39 pub max_grade: SeverityGrade,
40}
41
42fn grade_for(status: &DiffStatus) -> SeverityGrade {
43 match status {
44 DiffStatus::Same => SeverityGrade::Ok,
45 DiffStatus::Added => SeverityGrade::Ok,
46 DiffStatus::Removed => SeverityGrade::Break,
47 DiffStatus::TypeChanged => SeverityGrade::Break,
48 DiffStatus::PackingChanged => SeverityGrade::Risk,
49 }
50}
51
52fn is_same_type(a: &StorageType, b: &StorageType) -> bool {
53 a == b
54}
55
56fn packing_key(e: &StorageEntry) -> (Option<u8>, Option<u8>) {
57 (e.offset, e.size)
58}
59
60pub fn diff_layouts(old: &StorageLayout, new: &StorageLayout) -> (Vec<DiffEntry>, Summary) {
61 use std::collections::BTreeMap;
65
66 let mut old_map: BTreeMap<u128, Vec<&StorageEntry>> = BTreeMap::new();
67 for e in &old.entries {
68 old_map.entry(e.slot).or_default().push(e);
69 }
70
71 let mut new_map: BTreeMap<u128, Vec<&StorageEntry>> = BTreeMap::new();
72 for e in &new.entries {
73 new_map.entry(e.slot).or_default().push(e);
74 }
75
76 let mut all_slots: BTreeMap<u128, ()> = BTreeMap::new();
77 for s in old_map.keys() {
78 all_slots.insert(*s, ());
79 }
80 for s in new_map.keys() {
81 all_slots.insert(*s, ());
82 }
83
84 let mut diffs: Vec<DiffEntry> = Vec::new();
85 let mut summary = Summary {
86 added: 0,
87 removed: 0,
88 type_changed: 0,
89 packing_changed: 0,
90 same: 0,
91 max_grade: SeverityGrade::Ok,
92 };
93
94 for slot in all_slots.keys().copied() {
95 let old_entries = old_map.get(&slot).cloned().unwrap_or_default();
96 let new_entries = new_map.get(&slot).cloned().unwrap_or_default();
97
98 let status = match (old_entries.is_empty(), new_entries.is_empty()) {
99 (true, false) => DiffStatus::Added,
100 (false, true) => DiffStatus::Removed,
101 (true, true) => continue,
102 (false, false) => {
103 let mut type_changed = false;
107 let mut packing_changed = false;
108
109 let min_len = old_entries.len().min(new_entries.len());
110 for i in 0..min_len {
111 if !is_same_type(&old_entries[i].r#type, &new_entries[i].r#type) {
112 type_changed = true;
113 break;
114 }
115 if packing_key(old_entries[i]) != packing_key(new_entries[i]) {
116 packing_changed = true;
117 }
118 }
119 if !type_changed {
120 if old_entries.len() != new_entries.len() {
121 packing_changed = true;
122 }
123 }
124
125 if type_changed {
126 DiffStatus::TypeChanged
127 } else if packing_changed {
128 DiffStatus::PackingChanged
129 } else {
130 DiffStatus::Same
131 }
132 }
133 };
134
135 let grade = grade_for(&status);
136 summary.max_grade = summary.max_grade.max(grade);
137 match status {
138 DiffStatus::Same => summary.same += 1,
139 DiffStatus::Added => summary.added += 1,
140 DiffStatus::Removed => summary.removed += 1,
141 DiffStatus::TypeChanged => summary.type_changed += 1,
142 DiffStatus::PackingChanged => summary.packing_changed += 1,
143 }
144
145 let old_rep = old_entries.first().cloned().cloned();
146 let new_rep = new_entries.first().cloned().cloned();
147
148 diffs.push(DiffEntry {
149 slot,
150 old: old_rep.clone(),
151 new: new_rep.clone(),
152 status,
153 grade,
154 provenance_old: old_rep.as_ref().map(|e| e.provenance),
155 provenance_new: new_rep.as_ref().map(|e| e.provenance),
156 });
157 }
158
159 (diffs, summary)
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 fn entry(
167 slot: u128,
168 ty: StorageType,
169 offset: Option<u8>,
170 size: Option<u8>,
171 prov: Provenance,
172 ) -> StorageEntry {
173 StorageEntry {
174 slot,
175 offset,
176 size,
177 r#type: ty,
178 label: None,
179 provenance: prov,
180 }
181 }
182
183 #[test]
184 fn added_and_removed() {
185 let old = StorageLayout {
186 entries: vec![entry(
187 0,
188 StorageType::Unknown,
189 None,
190 None,
191 Provenance::HeuristicTrace,
192 )],
193 };
194 let new = StorageLayout {
195 entries: vec![entry(
196 1,
197 StorageType::Unknown,
198 None,
199 None,
200 Provenance::HeuristicTrace,
201 )],
202 };
203 let (_diffs, summary) = diff_layouts(&old, &new);
204 assert_eq!(summary.added, 1);
205 assert_eq!(summary.removed, 1);
206 assert_eq!(summary.max_grade, SeverityGrade::Break);
207 }
208
209 #[test]
210 fn type_changed_breaks() {
211 let old = StorageLayout {
212 entries: vec![entry(
213 0,
214 StorageType::Uint { bits: 256 },
215 None,
216 None,
217 Provenance::CompilerMetadata,
218 )],
219 };
220 let new = StorageLayout {
221 entries: vec![entry(
222 0,
223 StorageType::Address,
224 None,
225 None,
226 Provenance::CompilerMetadata,
227 )],
228 };
229 let (_diffs, summary) = diff_layouts(&old, &new);
230 assert_eq!(summary.type_changed, 1);
231 assert_eq!(summary.max_grade, SeverityGrade::Break);
232 }
233
234 #[test]
235 fn packing_changed_risk() {
236 let old = StorageLayout {
237 entries: vec![entry(
238 0,
239 StorageType::Uint { bits: 256 },
240 Some(0),
241 Some(32),
242 Provenance::CompilerMetadata,
243 )],
244 };
245 let new = StorageLayout {
246 entries: vec![entry(
247 0,
248 StorageType::Uint { bits: 256 },
249 Some(1),
250 Some(31),
251 Provenance::CompilerMetadata,
252 )],
253 };
254 let (_diffs, summary) = diff_layouts(&old, &new);
255 assert_eq!(summary.packing_changed, 1);
256 assert_eq!(summary.max_grade, SeverityGrade::Risk);
257 }
258
259 #[test]
260 fn same_ok() {
261 let e = entry(
262 2,
263 StorageType::Bool,
264 None,
265 None,
266 Provenance::CompilerMetadata,
267 );
268 let old = StorageLayout {
269 entries: vec![e.clone()],
270 };
271 let new = StorageLayout { entries: vec![e] };
272 let (_diffs, summary) = diff_layouts(&old, &new);
273 assert_eq!(summary.same, 1);
274 assert_eq!(summary.max_grade, SeverityGrade::Ok);
275 }
276}