1use std::cmp::Ordering;
2
3#[derive(Debug, PartialEq, Clone)]
4pub enum DiffByteRecord {
5 Add(ByteRecordLineInfo),
6 Modify {
7 delete: ByteRecordLineInfo,
8 add: ByteRecordLineInfo,
9 field_indices: Vec<usize>,
10 },
11 Delete(ByteRecordLineInfo),
12}
13
14impl DiffByteRecord {
15 pub fn line_num(&self) -> LineNum {
16 match self {
17 Self::Add(rli) | Self::Delete(rli) => LineNum::OneSide(rli.line),
18 Self::Modify {
19 delete: deleted,
20 add: added,
21 ..
22 } => LineNum::BothSides {
23 for_deleted: deleted.line,
24 for_added: added.line,
25 },
26 }
27 }
28
29 pub(crate) fn cmp_by_line(&self, other: &DiffByteRecord) -> Ordering {
30 match (self.line_num(), other.line_num()) {
31 (LineNum::OneSide(line_num_a), LineNum::OneSide(line_num_b)) => line_num_a
32 .cmp(&line_num_b)
33 .then(if matches!(self, DiffByteRecord::Delete(..)) {
34 Ordering::Less
35 } else {
36 Ordering::Greater
37 }),
38 (
39 LineNum::OneSide(line_num_a),
40 LineNum::BothSides {
41 for_deleted: modify_line_del,
42 for_added: modify_line_add,
43 },
44 ) => line_num_a
45 .cmp(if modify_line_del < modify_line_add {
46 &modify_line_del
47 } else {
48 &modify_line_add
49 })
50 .then(if matches!(self, DiffByteRecord::Delete(..)) {
51 Ordering::Less
52 } else {
53 Ordering::Greater
54 }),
55 (
56 LineNum::BothSides {
57 for_deleted: modify_line_del,
58 for_added: modify_line_add,
59 },
60 LineNum::OneSide(line_num_b),
61 ) => if modify_line_del < modify_line_add {
62 &modify_line_del
63 } else {
64 &modify_line_add
65 }
66 .cmp(&line_num_b)
67 .then(if matches!(other, DiffByteRecord::Add(..)) {
68 Ordering::Less
69 } else {
70 Ordering::Greater
71 }),
72 (
73 LineNum::BothSides {
74 for_deleted: modify_line_del_a,
75 for_added: modify_line_add_a,
76 },
77 LineNum::BothSides {
78 for_deleted: modify_line_del_b,
79 for_added: modify_line_add_b,
80 },
81 ) => if modify_line_del_a < modify_line_add_a {
82 &modify_line_del_a
83 } else {
84 &modify_line_add_a
85 }
86 .cmp(if modify_line_del_b < modify_line_add_b {
87 &modify_line_del_b
88 } else {
89 &modify_line_add_b
90 })
91 .then(modify_line_del_a.cmp(&modify_line_del_b)),
95 }
96 }
97}
98
99#[cfg(kani)]
100impl kani::Arbitrary for DiffByteRecord {
101 fn any() -> Self {
102 match kani::any() {
103 0 => Self::Add(kani::any()),
104 1 => Self::Delete(kani::any()),
105 _ => Self::Modify {
106 delete: kani::any(),
107 add: kani::any(),
108 field_indices: vec![],
109 },
110 }
111 }
112}
113
114#[cfg_attr(kani, derive(kani::Arbitrary))]
115pub enum LineNum {
116 OneSide(u64),
117 BothSides { for_deleted: u64, for_added: u64 },
118}
119
120#[derive(Debug, PartialEq, Clone)]
121pub struct ByteRecordLineInfo {
122 byte_record: csv::ByteRecord,
123 line: u64,
124}
125
126impl ByteRecordLineInfo {
127 pub fn new(byte_record: csv::ByteRecord, line: u64) -> Self {
128 Self { byte_record, line }
129 }
130
131 pub fn byte_record(&self) -> &csv::ByteRecord {
132 &self.byte_record
133 }
134
135 pub fn into_byte_record(self) -> csv::ByteRecord {
136 self.byte_record
137 }
138
139 pub fn line(&self) -> u64 {
140 self.line
141 }
142}
143
144#[cfg(kani)]
145impl kani::Arbitrary for ByteRecordLineInfo {
146 fn any() -> Self {
147 Self {
148 byte_record: csv::ByteRecord::from(kani::vec::any_vec::<[u8; 1], 3>()), line: kani::any(),
150 }
151 }
152}
153
154#[cfg(kani)]
155mod verification {
156 use super::*;
157
158 #[kani::proof]
159 #[kani::unwind(4)]
160 pub fn check_less_than_line_num_one_side() {
161 let (a, b): (DiffByteRecord, DiffByteRecord) = (kani::any(), kani::any());
162 kani::assume(
163 matches!(a.line_num(), LineNum::OneSide(_))
164 && matches!(b.line_num(), LineNum::OneSide(_)),
165 );
166 match (a.line_num(), b.line_num()) {
167 (LineNum::OneSide(line_a), LineNum::OneSide(line_b)) => {
168 kani::assume(line_a < line_b && line_a > 0 && line_b > 0)
169 }
170 _ => unreachable!("kani::assume should prevent this arm!"),
171 };
172 assert!(a.cmp_by_line(&b) == Ordering::Less);
173 }
174
175 #[kani::proof]
176 #[kani::unwind(4)]
177 pub fn check_greater_than_line_num_one_side() {
178 let (a, b): (DiffByteRecord, DiffByteRecord) = (kani::any(), kani::any());
179 kani::assume(
180 matches!(a.line_num(), LineNum::OneSide(_))
181 && matches!(b.line_num(), LineNum::OneSide(_)),
182 );
183 match (a.line_num(), b.line_num()) {
184 (LineNum::OneSide(line_a), LineNum::OneSide(line_b)) => {
185 kani::assume(line_a > line_b && line_a > 0 && line_b > 0)
186 }
187 _ => unreachable!("kani::assume should prevent this arm!"),
188 };
189 assert!(a.cmp_by_line(&b) == Ordering::Greater);
190 }
191
192 #[kani::proof]
193 #[kani::unwind(4)]
194 pub fn check_never_compare_to_equal() {
195 let (a, b): (DiffByteRecord, DiffByteRecord) = (kani::any(), kani::any());
196
197 kani_assume_certain_lines_cannot_be_equal(&a, &b);
198
199 assert!(a.cmp_by_line(&b) != Ordering::Equal);
200 }
201
202 fn kani_assume_certain_lines_cannot_be_equal(a: &DiffByteRecord, b: &DiffByteRecord) {
203 match (a, b) {
204 (DiffByteRecord::Add(line_info_a), DiffByteRecord::Add(line_info_b))
205 | (DiffByteRecord::Delete(line_info_a), DiffByteRecord::Delete(line_info_b)) => {
206 kani::assume(line_info_a.line() != line_info_b.line());
207 }
208 (
209 DiffByteRecord::Add(byte_record_line_info_add),
210 DiffByteRecord::Modify {
211 delete: _,
212 add,
213 field_indices: _,
214 },
215 )
216 | (
217 DiffByteRecord::Modify {
218 delete: _,
219 add,
220 field_indices: _,
221 },
222 DiffByteRecord::Add(byte_record_line_info_add),
223 ) => {
224 kani::assume(byte_record_line_info_add.line() != add.line());
225 }
226 (
227 DiffByteRecord::Modify {
228 delete: delete_a,
229 add: add_a,
230 ..
231 },
232 DiffByteRecord::Modify {
233 delete: delete_b,
234 add: add_b,
235 ..
236 },
237 ) => {
238 kani::assume(delete_a.line() != delete_b.line() && add_a.line() != add_b.line());
239 }
240 (
241 DiffByteRecord::Modify {
242 delete,
243 add: _,
244 field_indices: _,
245 },
246 DiffByteRecord::Delete(line_info_delete),
247 )
248 | (
249 DiffByteRecord::Delete(line_info_delete),
250 DiffByteRecord::Modify {
251 delete,
252 add: _,
253 field_indices: _,
254 },
255 ) => {
256 kani::assume(delete.line() != line_info_delete.line());
257 }
258 _ => {
259 }
261 }
262 }
263
264 #[kani::proof]
265 #[kani::unwind(4)]
266 pub fn check_comparison_one_side_with_both_sides() {
267 let (a, b): (DiffByteRecord, DiffByteRecord) = (kani::any(), kani::any());
268 kani::assume(matches!(
269 (a.line_num(), b.line_num()),
270 (LineNum::OneSide(_), LineNum::BothSides { .. })
271 | (LineNum::BothSides { .. }, LineNum::OneSide(_))
272 ));
273 match (a.line_num(), b.line_num()) {
274 (
275 LineNum::OneSide(line_a),
276 LineNum::BothSides {
277 for_deleted: line_both_del,
278 for_added: line_both_add,
279 },
280 ) => {
281 kani::assume(line_a > 0 && line_both_del > 0 && line_both_add > 0);
282 match line_a.cmp(if line_both_del < line_both_add {
283 &line_both_del
284 } else {
285 &line_both_add
286 }) {
287 o @ Ordering::Less | o @ Ordering::Greater => assert!(a.cmp_by_line(&b) == o),
288 Ordering::Equal => assert!(if matches!(a, DiffByteRecord::Delete(_)) {
289 a.cmp_by_line(&b) == Ordering::Less
290 } else {
291 a.cmp_by_line(&b) == Ordering::Greater
292 }),
293 }
294 }
295 (
296 LineNum::BothSides {
297 for_deleted: line_both_del,
298 for_added: line_both_add,
299 },
300 LineNum::OneSide(line_b),
301 ) => {
302 kani::assume(line_b > 0 && line_both_del > 0 && line_both_add > 0);
303 match line_b.cmp(if line_both_del < line_both_add {
304 &line_both_del
305 } else {
306 &line_both_add
307 }) {
308 o @ Ordering::Less | o @ Ordering::Greater => assert!(b.cmp_by_line(&a) == o),
309 Ordering::Equal => assert!(if matches!(b, DiffByteRecord::Add(_)) {
310 a.cmp_by_line(&b) == Ordering::Less
311 } else {
312 a.cmp_by_line(&b) == Ordering::Greater
313 }),
314 }
315 }
316 _ => unreachable!("kani::assume should prevent this arm"),
317 };
318 }
319
320 #[kani::proof]
321 #[kani::unwind(5)]
322 pub fn check_comparison_both_sides_with_both_sides() {
323 let (a, b): (DiffByteRecord, DiffByteRecord) = (kani::any(), kani::any());
324 kani::assume(matches!(
325 (a.line_num(), b.line_num()),
326 (LineNum::BothSides { .. }, LineNum::BothSides { .. })
327 ));
328 match (a.line_num(), b.line_num()) {
329 (
330 LineNum::BothSides {
331 for_deleted: line_both_del_a,
332 for_added: line_both_add_a,
333 },
334 LineNum::BothSides {
335 for_deleted: line_both_del_b,
336 for_added: line_both_add_b,
337 },
338 ) => {
339 kani::assume(
340 [
341 line_both_add_a,
342 line_both_del_a,
343 line_both_add_b,
344 line_both_del_b,
345 ]
346 .into_iter()
347 .all(|x| x > 0)
348 && [
349 (line_both_add_a, line_both_add_b),
350 (line_both_del_a, line_both_del_b),
351 ]
352 .into_iter()
353 .all(|(a, b)| a != b),
354 );
355 match if line_both_add_a < line_both_del_a {
356 line_both_add_a
357 } else {
358 line_both_del_a
359 }
360 .cmp(if line_both_add_b < line_both_del_b {
361 &line_both_add_b
362 } else {
363 &line_both_del_b
364 }) {
365 o @ Ordering::Less | o @ Ordering::Greater => {
366 assert!(a.cmp_by_line(&b) == o)
367 }
368 Ordering::Equal => {
369 assert!(line_both_del_a.cmp(&line_both_del_b) == a.cmp_by_line(&b))
370 }
371 }
372 }
373 _ => unreachable!("kani::assume should prevent this arm"),
374 };
375 }
376}