csv_diff/
diff_row.rs

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            // this will _never_ compare to `Ordering::Equal`, because
92            // one and the same line on one side of the comparison
93            // can't be in two different `DiffByteRecord`s
94            .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>()), //csv::ByteRecord::default(),
149            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                // no assumptions for the other cases
260            }
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}