1
#![cfg_attr(docsrs, feature(doc_cfg))]
2
#![doc = include_str!("../README.md")]
3
// @@ begin lint list maintained by maint/add_warning @@
4
#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5
#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6
#![warn(missing_docs)]
7
#![warn(noop_method_call)]
8
#![warn(unreachable_pub)]
9
#![warn(clippy::all)]
10
#![deny(clippy::await_holding_lock)]
11
#![deny(clippy::cargo_common_metadata)]
12
#![deny(clippy::cast_lossless)]
13
#![deny(clippy::checked_conversions)]
14
#![warn(clippy::cognitive_complexity)]
15
#![deny(clippy::debug_assert_with_mut_call)]
16
#![deny(clippy::exhaustive_enums)]
17
#![deny(clippy::exhaustive_structs)]
18
#![deny(clippy::expl_impl_clone_on_copy)]
19
#![deny(clippy::fallible_impl_from)]
20
#![deny(clippy::implicit_clone)]
21
#![deny(clippy::large_stack_arrays)]
22
#![warn(clippy::manual_ok_or)]
23
#![deny(clippy::missing_docs_in_private_items)]
24
#![warn(clippy::needless_borrow)]
25
#![warn(clippy::needless_pass_by_value)]
26
#![warn(clippy::option_option)]
27
#![deny(clippy::print_stderr)]
28
#![deny(clippy::print_stdout)]
29
#![warn(clippy::rc_buffer)]
30
#![deny(clippy::ref_option_ref)]
31
#![warn(clippy::semicolon_if_nothing_returned)]
32
#![warn(clippy::trait_duplication_in_bounds)]
33
#![deny(clippy::unchecked_time_subtraction)]
34
#![deny(clippy::unnecessary_wraps)]
35
#![warn(clippy::unseparated_literal_suffix)]
36
#![deny(clippy::unwrap_used)]
37
#![deny(clippy::mod_module_files)]
38
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
39
#![allow(clippy::uninlined_format_args)]
40
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43
#![allow(clippy::needless_lifetimes)] // See arti#1765
44
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
45
#![deny(clippy::unused_async)]
46
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
47

            
48
use std::fmt::{Display, Formatter};
49
use std::num::NonZeroUsize;
50
use std::str::FromStr;
51

            
52
mod err;
53
pub use err::Error;
54

            
55
/// Result type used by this crate
56
type Result<T> = std::result::Result<T, Error>;
57

            
58
/// Return true if `s` looks more like a consensus diff than some other kind
59
/// of document.
60
148
pub fn looks_like_diff(s: &str) -> bool {
61
148
    s.starts_with("network-status-diff-version")
62
148
}
63

            
64
/// Apply a given diff to an input text, and return the result from applying
65
/// that diff.
66
///
67
/// This is a slow version, for testing and correctness checking.  It uses
68
/// an O(n) operation to apply diffs, and therefore runs in O(n^2) time.
69
#[cfg(any(test, feature = "slow-diff-apply"))]
70
2
pub fn apply_diff_trivial<'a>(input: &'a str, diff: &'a str) -> Result<DiffResult<'a>> {
71
2
    let mut diff_lines = diff.lines();
72
2
    let (_, d2) = parse_diff_header(&mut diff_lines)?;
73

            
74
2
    let mut diffable = DiffResult::from_str(input, d2);
75

            
76
24
    for command in DiffCommandIter::new(diff_lines) {
77
24
        command?.apply_to(&mut diffable)?;
78
    }
79

            
80
2
    Ok(diffable)
81
2
}
82

            
83
/// Apply a given diff to an input text, and return the result from applying
84
/// that diff.
85
///
86
/// If `check_digest_in` is provided, require the diff to say that it
87
/// applies to a document with the provided digest.
88
111
pub fn apply_diff<'a>(
89
111
    input: &'a str,
90
111
    diff: &'a str,
91
111
    check_digest_in: Option<[u8; 32]>,
92
111
) -> Result<DiffResult<'a>> {
93
111
    let mut input = DiffResult::from_str(input, [0; 32]);
94

            
95
111
    let mut diff_lines = diff.lines();
96
111
    let (d1, d2) = parse_diff_header(&mut diff_lines)?;
97
111
    if let Some(d_want) = check_digest_in {
98
74
        if d1 != d_want {
99
            return Err(Error::CantApply("listed digest does not match document"));
100
74
        }
101
37
    }
102

            
103
111
    let mut output = DiffResult::new(d2);
104

            
105
518
    for command in DiffCommandIter::new(diff_lines) {
106
518
        command?.apply_transformation(&mut input, &mut output)?;
107
    }
108

            
109
111
    output.push_reversed(&input.lines[..]);
110

            
111
111
    output.lines.reverse();
112
111
    Ok(output)
113
111
}
114

            
115
/// Given a line iterator, check to make sure the first two lines are
116
/// a valid diff header as specified in dir-spec.txt.
117
133
fn parse_diff_header<'a, I>(iter: &mut I) -> Result<([u8; 32], [u8; 32])>
118
133
where
119
133
    I: Iterator<Item = &'a str>,
120
{
121
133
    let line1 = iter.next();
122
133
    if line1 != Some("network-status-diff-version 1") {
123
6
        return Err(Error::BadDiff("unrecognized or missing header"));
124
127
    }
125
127
    let line2 = iter.next().ok_or(Error::BadDiff("header truncated"))?;
126
125
    if !line2.starts_with("hash ") {
127
2
        return Err(Error::BadDiff("missing 'hash' line"));
128
123
    }
129
123
    let elts: Vec<_> = line2.split_ascii_whitespace().collect();
130
123
    if elts.len() != 3 {
131
2
        return Err(Error::BadDiff("invalid 'hash' line"));
132
121
    }
133
121
    let d1 = hex::decode(elts[1])?;
134
117
    let d2 = hex::decode(elts[2])?;
135
117
    match (d1.try_into(), d2.try_into()) {
136
115
        (Ok(a), Ok(b)) => Ok((a, b)),
137
2
        _ => Err(Error::BadDiff("wrong digest lengths on 'hash' line")),
138
    }
139
133
}
140

            
141
/// A command that can appear in a diff.  Each command tells us to
142
/// remove zero or more lines, and insert zero or more lines in their
143
/// place.
144
///
145
/// Commands refer to lines by 1-indexed line number.
146
#[derive(Clone, Debug)]
147
enum DiffCommand<'a> {
148
    /// Remove the lines from low through high, inclusive.
149
    Delete {
150
        /// The first line to remove
151
        low: usize,
152
        /// The last line to remove
153
        high: usize,
154
    },
155
    /// Remove the lines from low through the end of the file, inclusive.
156
    DeleteToEnd {
157
        /// The first line to remove
158
        low: usize,
159
    },
160
    /// Replace the lines from low through high, inclusive, with the
161
    /// lines in 'lines'.
162
    Replace {
163
        /// The first line to replace
164
        low: usize,
165
        /// The last line to replace
166
        high: usize,
167
        /// The text to insert instead
168
        lines: Vec<&'a str>,
169
    },
170
    /// Insert the provided 'lines' after the line with index 'pos'.
171
    Insert {
172
        /// The position after which to insert the text
173
        pos: usize,
174
        /// The text to insert
175
        lines: Vec<&'a str>,
176
    },
177
}
178

            
179
/// The result of applying one or more diff commands to an input string.
180
///
181
/// It refers to lines from the diff and the input by reference, to
182
/// avoid copying.
183
#[derive(Clone, Debug)]
184
pub struct DiffResult<'a> {
185
    /// An expected digest of the output, after it has been assembled.
186
    d_post: [u8; 32],
187
    /// The lines in the output.
188
    lines: Vec<&'a str>,
189
}
190

            
191
/// A possible value for the end of a range.  It can be either a line number,
192
/// or a dollar sign indicating "end of file".
193
#[derive(Clone, Copy, Debug)]
194
enum RangeEnd {
195
    /// A line number in the file.
196
    Num(NonZeroUsize),
197
    /// A dollar sign, indicating "end of file" in a delete command.
198
    DollarSign,
199
}
200

            
201
impl FromStr for RangeEnd {
202
    type Err = Error;
203
344
    fn from_str(s: &str) -> Result<RangeEnd> {
204
344
        if s == "$" {
205
43
            Ok(RangeEnd::DollarSign)
206
        } else {
207
301
            let v: NonZeroUsize = s.parse()?;
208
299
            if v.get() == usize::MAX {
209
2
                return Err(Error::BadDiff("range cannot end at usize::MAX"));
210
297
            }
211
297
            Ok(RangeEnd::Num(v))
212
        }
213
344
    }
214
}
215

            
216
impl<'a> DiffCommand<'a> {
217
    /// Transform 'target' according to the this command.
218
    ///
219
    /// Because DiffResult internally uses a vector of line, this
220
    /// implementation is potentially O(n) in the size of the input.
221
    #[cfg(any(test, feature = "slow-diff-apply"))]
222
32
    fn apply_to(&self, target: &mut DiffResult<'a>) -> Result<()> {
223
32
        match self {
224
8
            Self::Delete { low, high } => {
225
8
                target.remove_lines(*low, *high)?;
226
            }
227
4
            Self::DeleteToEnd { low } => {
228
4
                target.remove_lines(*low, target.lines.len())?;
229
            }
230
16
            Self::Replace { low, high, lines } => {
231
16
                target.remove_lines(*low, *high)?;
232
16
                target.insert_at(*low, lines)?;
233
            }
234
4
            Self::Insert { pos, lines } => {
235
                // This '+1' seems off, but it's what the spec says. I wonder
236
                // if the spec is wrong.
237
4
                target.insert_at(*pos + 1, lines)?;
238
            }
239
        };
240
32
        Ok(())
241
32
    }
242

            
243
    /// Apply this command to 'input', moving lines into 'output'.
244
    ///
245
    /// This is a more efficient algorithm, but it requires that the
246
    /// diff commands are sorted in reverse order by line
247
    /// number. (Fortunately, the Tor ed diff format guarantees this.)
248
    ///
249
    /// Before calling this method, input and output must contain the
250
    /// results of having applied the previous command in the diff.
251
    /// (When no commands have been applied, input starts out as the
252
    /// original text, and output starts out empty.)
253
    ///
254
    /// This method applies the command by copying unaffected lines
255
    /// from the _end_ of input into output, adding any lines inserted
256
    /// by this command, and finally deleting any affected lines from
257
    /// input.
258
    ///
259
    /// We build the `output` value in reverse order, and then put it
260
    /// back to normal before giving it to the user.
261
538
    fn apply_transformation(
262
538
        &self,
263
538
        input: &mut DiffResult<'a>,
264
538
        output: &mut DiffResult<'a>,
265
538
    ) -> Result<()> {
266
538
        if let Some(succ) = self.following_lines() {
267
495
            if let Some(subslice) = input.lines.get(succ - 1..) {
268
491
                // Lines from `succ` onwards are unaffected.  Copy them.
269
491
                output.push_reversed(subslice);
270
491
            } else {
271
                // Oops, dubious line number.
272
4
                return Err(Error::CantApply(
273
4
                    "ending line number didn't correspond to document",
274
4
                ));
275
            }
276
43
        }
277

            
278
534
        if let Some(lines) = self.lines() {
279
376
            // These are the lines we're inserting.
280
376
            output.push_reversed(lines);
281
380
        }
282

            
283
534
        let remove = self.first_removed_line();
284
534
        if remove == 0 || (!self.is_insert() && remove > input.lines.len()) {
285
4
            return Err(Error::CantApply(
286
4
                "starting line number didn't correspond to document",
287
4
            ));
288
530
        }
289
530
        input.lines.truncate(remove - 1);
290

            
291
530
        Ok(())
292
538
    }
293

            
294
    /// Return the lines that we should add to the output
295
542
    fn lines(&self) -> Option<&[&'a str]> {
296
542
        match self {
297
384
            Self::Replace { lines, .. } | Self::Insert { lines, .. } => Some(lines.as_slice()),
298
158
            _ => None,
299
        }
300
542
    }
301

            
302
    /// Return a mutable reference to the vector of lines we should
303
    /// add to the output.
304
572
    fn linebuf_mut(&mut self) -> Option<&mut Vec<&'a str>> {
305
572
        match self {
306
394
            Self::Replace { lines, .. } | Self::Insert { lines, .. } => Some(lines),
307
178
            _ => None,
308
        }
309
572
    }
310

            
311
    /// Return the (1-indexed) line number of the first line in the
312
    /// input that comes _after_ this command, and is not affected by it.
313
    ///
314
    /// We use this line number to know which lines we should copy.
315
1096
    fn following_lines(&self) -> Option<usize> {
316
1096
        match self {
317
934
            Self::Delete { high, .. } | Self::Replace { high, .. } => Some(high + 1),
318
82
            Self::DeleteToEnd { .. } => None,
319
80
            Self::Insert { pos, .. } => Some(pos + 1),
320
        }
321
1096
    }
322

            
323
    /// Return the (1-indexed) line number of the first line that we
324
    /// should clear from the input when processing this command.
325
    ///
326
    /// This can be the same as following_lines(), if we shouldn't
327
    /// actually remove any lines.
328
1086
    fn first_removed_line(&self) -> usize {
329
1086
        match self {
330
242
            Self::Delete { low, .. } => *low,
331
82
            Self::DeleteToEnd { low } => *low,
332
682
            Self::Replace { low, .. } => *low,
333
80
            Self::Insert { pos, .. } => *pos + 1,
334
        }
335
1086
    }
336

            
337
    /// Return true if this is an Insert command.
338
532
    fn is_insert(&self) -> bool {
339
532
        matches!(self, Self::Insert { .. })
340
532
    }
341

            
342
    /// Extract a single command from a line iterator that yields lines
343
    /// of the diffs.  Return None if we're at the end of the iterator.
344
727
    fn from_line_iterator<I>(iter: &mut I) -> Result<Option<Self>>
345
727
    where
346
727
        I: Iterator<Item = &'a str>,
347
    {
348
727
        let command = match iter.next() {
349
598
            Some(s) => s,
350
129
            None => return Ok(None),
351
        };
352

            
353
        // `command` can be of these forms: `Rc`, `Rd`, `N,$d`, and `Na`,
354
        // where R is a range of form `N,N`, and where N is a line number.
355

            
356
598
        if command.len() < 2 || !command.is_ascii() {
357
6
            return Err(Error::BadDiff("command too short"));
358
592
        }
359

            
360
592
        let (range, command) = command.split_at(command.len() - 1);
361
592
        let (low, high) = if let Some(comma_pos) = range.find(',') {
362
            (
363
346
                range[..comma_pos].parse::<usize>()?,
364
344
                Some(range[comma_pos + 1..].parse::<RangeEnd>()?),
365
            )
366
        } else {
367
246
            (range.parse::<usize>()?, None)
368
        };
369

            
370
580
        if low == usize::MAX {
371
2
            return Err(Error::BadDiff("range cannot begin at usize::MAX"));
372
578
        }
373

            
374
578
        match (low, high) {
375
297
            (lo, Some(RangeEnd::Num(hi))) if lo > hi.into() => {
376
2
                return Err(Error::BadDiff("mis-ordered lines in range"));
377
            }
378
576
            (_, _) => (),
379
        }
380

            
381
576
        let mut cmd = match (command, low, high) {
382
576
            ("d", low, None) => Self::Delete { low, high: low },
383
135
            ("d", low, Some(RangeEnd::Num(high))) => Self::Delete {
384
135
                low,
385
135
                high: high.into(),
386
135
            },
387
41
            ("d", low, Some(RangeEnd::DollarSign)) => Self::DeleteToEnd { low },
388
398
            ("c", low, None) => Self::Replace {
389
193
                low,
390
193
                high: low,
391
193
                lines: Vec::new(),
392
193
            },
393
158
            ("c", low, Some(RangeEnd::Num(high))) => Self::Replace {
394
158
                low,
395
158
                high: high.into(),
396
158
                lines: Vec::new(),
397
158
            },
398
45
            ("a", low, None) => Self::Insert {
399
43
                pos: low,
400
43
                lines: Vec::new(),
401
43
            },
402
4
            (_, _, _) => return Err(Error::BadDiff("can't parse command line")),
403
        };
404

            
405
572
        if let Some(ref mut linebuf) = cmd.linebuf_mut() {
406
            // The 'c' and 'a' commands take a series of lines followed by a
407
            // line containing a period.
408
            loop {
409
1966
                match iter.next() {
410
                    None => return Err(Error::BadDiff("unterminated block to insert")),
411
1966
                    Some(".") => break,
412
1572
                    Some(line) => linebuf.push(line),
413
                }
414
            }
415
178
        }
416

            
417
572
        Ok(Some(cmd))
418
727
    }
419
}
420

            
421
/// Iterator that wraps a line iterator and returns a sequence of
422
/// `Result<DiffCommand>`.
423
///
424
/// This iterator forces the commands to affect the file in reverse order,
425
/// so that we can use the O(n) algorithm for applying these diffs.
426
struct DiffCommandIter<'a, I>
427
where
428
    I: Iterator<Item = &'a str>,
429
{
430
    /// The underlying iterator.
431
    iter: I,
432

            
433
    /// The 'first removed line' of the last-parsed command; used to ensure
434
    /// that commands appear in reverse order.
435
    last_cmd_first_removed: Option<usize>,
436
}
437

            
438
impl<'a, I> DiffCommandIter<'a, I>
439
where
440
    I: Iterator<Item = &'a str>,
441
{
442
    /// Construct a new DiffCommandIter wrapping `iter`.
443
121
    fn new(iter: I) -> Self {
444
121
        DiffCommandIter {
445
121
            iter,
446
121
            last_cmd_first_removed: None,
447
121
        }
448
121
    }
449
}
450

            
451
impl<'a, I> Iterator for DiffCommandIter<'a, I>
452
where
453
    I: Iterator<Item = &'a str>,
454
{
455
    type Item = Result<DiffCommand<'a>>;
456
673
    fn next(&mut self) -> Option<Result<DiffCommand<'a>>> {
457
673
        match DiffCommand::from_line_iterator(&mut self.iter) {
458
            Err(e) => Some(Err(e)),
459
115
            Ok(None) => None,
460
558
            Ok(Some(c)) => match (self.last_cmd_first_removed, c.following_lines()) {
461
                (Some(_), None) => Some(Err(Error::BadDiff("misordered commands"))),
462
437
                (Some(a), Some(b)) if a < b => Some(Err(Error::BadDiff("misordered commands"))),
463
                (_, _) => {
464
552
                    self.last_cmd_first_removed = Some(c.first_removed_line());
465
552
                    Some(Ok(c))
466
                }
467
            },
468
        }
469
673
    }
470
}
471

            
472
impl<'a> DiffResult<'a> {
473
    /// Construct a new DiffResult containing the provided string
474
    /// split into lines, and an expected post-transformation digest.
475
121
    fn from_str(s: &'a str, d_post: [u8; 32]) -> Self {
476
        // As per the [netdoc syntax], newlines should be discarded and ignored.
477
        //
478
        // [netdoc syntax]: https://spec.torproject.org/dir-spec/netdoc.html#netdoc-syntax
479
121
        let lines: Vec<_> = s.lines().collect();
480

            
481
121
        DiffResult { d_post, lines }
482
121
    }
483

            
484
    /// Return a new empty DiffResult with an expected
485
    /// post-transformation digests
486
115
    fn new(d_post: [u8; 32]) -> Self {
487
115
        DiffResult {
488
115
            d_post,
489
115
            lines: Vec::new(),
490
115
        }
491
115
    }
492

            
493
    /// Put every member of `lines` at the end of this DiffResult, in
494
    /// reverse order.
495
982
    fn push_reversed(&mut self, lines: &[&'a str]) {
496
982
        self.lines.extend(lines.iter().rev());
497
982
    }
498

            
499
    /// Remove the 1-indexed lines from `first` through `last` inclusive.
500
    ///
501
    /// This has to move elements around within the vector, and so it
502
    /// is potentially O(n) in its length.
503
    #[cfg(any(test, feature = "slow-diff-apply"))]
504
40
    fn remove_lines(&mut self, first: usize, last: usize) -> Result<()> {
505
40
        if first > self.lines.len() || last > self.lines.len() || first == 0 || last == 0 {
506
4
            Err(Error::CantApply("line out of range"))
507
        } else {
508
36
            let n_to_remove = last - first + 1;
509
36
            if last != self.lines.len() {
510
28
                self.lines[..].copy_within((last).., first - 1);
511
28
            }
512
36
            self.lines.truncate(self.lines.len() - n_to_remove);
513
36
            Ok(())
514
        }
515
40
    }
516

            
517
    /// Insert the provided `lines` so that they appear at 1-indexed
518
    /// position `pos`.
519
    ///
520
    /// This has to move elements around within the vector, and so it
521
    /// is potentially O(n) in its length.
522
    #[cfg(any(test, feature = "slow-diff-apply"))]
523
28
    fn insert_at(&mut self, pos: usize, lines: &[&'a str]) -> Result<()> {
524
28
        if pos > self.lines.len() + 1 || pos == 0 {
525
4
            Err(Error::CantApply("position out of range"))
526
        } else {
527
24
            let orig_len = self.lines.len();
528
24
            self.lines.resize(self.lines.len() + lines.len(), "");
529
24
            self.lines
530
24
                .copy_within(pos - 1..orig_len, pos - 1 + lines.len());
531
24
            self.lines[(pos - 1)..(pos + lines.len() - 1)].copy_from_slice(lines);
532
24
            Ok(())
533
        }
534
28
    }
535

            
536
    /// See whether the output of this diff matches the target digest.
537
    ///
538
    /// If not, return an error.
539
76
    pub fn check_digest(&self) -> Result<()> {
540
        use digest::Digest;
541
        use tor_llcrypto::d::Sha3_256;
542
76
        let mut d = Sha3_256::new();
543
438
        for line in &self.lines {
544
362
            d.update(line.as_bytes());
545
362
            d.update(b"\n");
546
362
        }
547
76
        if d.finalize() == self.d_post.into() {
548
39
            Ok(())
549
        } else {
550
37
            Err(Error::CantApply("Wrong digest after applying diff"))
551
        }
552
76
    }
553
}
554

            
555
impl<'a> Display for DiffResult<'a> {
556
128
    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
557
3223
        for elt in &self.lines {
558
3095
            writeln!(f, "{}", elt)?;
559
        }
560
128
        Ok(())
561
128
    }
562
}
563

            
564
#[cfg(test)]
565
mod test {
566
    // @@ begin test lint list maintained by maint/add_warning @@
567
    #![allow(clippy::bool_assert_comparison)]
568
    #![allow(clippy::clone_on_copy)]
569
    #![allow(clippy::dbg_macro)]
570
    #![allow(clippy::mixed_attributes_style)]
571
    #![allow(clippy::print_stderr)]
572
    #![allow(clippy::print_stdout)]
573
    #![allow(clippy::single_char_pattern)]
574
    #![allow(clippy::unwrap_used)]
575
    #![allow(clippy::unchecked_time_subtraction)]
576
    #![allow(clippy::useless_vec)]
577
    #![allow(clippy::needless_pass_by_value)]
578
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
579
    use super::*;
580

            
581
    #[test]
582
    fn remove() -> Result<()> {
583
        let example = DiffResult::from_str("1\n2\n3\n4\n5\n6\n7\n8\n9\n", [0; 32]);
584

            
585
        let mut d = example.clone();
586
        d.remove_lines(5, 7)?;
587
        assert_eq!(d.to_string(), "1\n2\n3\n4\n8\n9\n");
588

            
589
        let mut d = example.clone();
590
        d.remove_lines(1, 9)?;
591
        assert_eq!(d.to_string(), "");
592

            
593
        let mut d = example.clone();
594
        d.remove_lines(1, 1)?;
595
        assert_eq!(d.to_string(), "2\n3\n4\n5\n6\n7\n8\n9\n");
596

            
597
        let mut d = example.clone();
598
        d.remove_lines(6, 9)?;
599
        assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n");
600

            
601
        let mut d = example.clone();
602
        assert!(d.remove_lines(6, 10).is_err());
603
        assert!(d.remove_lines(0, 1).is_err());
604
        assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n9\n");
605

            
606
        Ok(())
607
    }
608

            
609
    #[test]
610
    fn insert() -> Result<()> {
611
        let example = DiffResult::from_str("1\n2\n3\n4\n5\n", [0; 32]);
612
        let mut d = example.clone();
613
        d.insert_at(3, &["hello", "world"])?;
614
        assert_eq!(d.to_string(), "1\n2\nhello\nworld\n3\n4\n5\n");
615

            
616
        let mut d = example.clone();
617
        d.insert_at(6, &["hello", "world"])?;
618
        assert_eq!(d.to_string(), "1\n2\n3\n4\n5\nhello\nworld\n");
619

            
620
        let mut d = example.clone();
621
        assert!(d.insert_at(0, &["hello", "world"]).is_err());
622
        assert!(d.insert_at(7, &["hello", "world"]).is_err());
623
        Ok(())
624
    }
625

            
626
    #[test]
627
    fn push_reversed() {
628
        let mut d = DiffResult::new([0; 32]);
629
        d.push_reversed(&["7", "8", "9"]);
630
        assert_eq!(d.to_string(), "9\n8\n7\n");
631
        d.push_reversed(&["world", "hello", ""]);
632
        assert_eq!(d.to_string(), "9\n8\n7\n\nhello\nworld\n");
633
    }
634

            
635
    #[test]
636
    fn apply_command_simple() {
637
        let example = DiffResult::from_str("a\nb\nc\nd\ne\nf\n", [0; 32]);
638

            
639
        let mut d = example.clone();
640
        assert_eq!(d.to_string(), "a\nb\nc\nd\ne\nf\n".to_string());
641
        assert!(DiffCommand::DeleteToEnd { low: 5 }.apply_to(&mut d).is_ok());
642
        assert_eq!(d.to_string(), "a\nb\nc\nd\n".to_string());
643

            
644
        let mut d = example.clone();
645
        assert!(
646
            DiffCommand::Delete { low: 3, high: 5 }
647
                .apply_to(&mut d)
648
                .is_ok()
649
        );
650
        assert_eq!(d.to_string(), "a\nb\nf\n".to_string());
651

            
652
        let mut d = example.clone();
653
        assert!(
654
            DiffCommand::Replace {
655
                low: 3,
656
                high: 5,
657
                lines: vec!["hello", "world"]
658
            }
659
            .apply_to(&mut d)
660
            .is_ok()
661
        );
662
        assert_eq!(d.to_string(), "a\nb\nhello\nworld\nf\n".to_string());
663

            
664
        let mut d = example.clone();
665
        assert!(
666
            DiffCommand::Insert {
667
                pos: 3,
668
                lines: vec!["hello", "world"]
669
            }
670
            .apply_to(&mut d)
671
            .is_ok()
672
        );
673
        assert_eq!(
674
            d.to_string(),
675
            "a\nb\nc\nhello\nworld\nd\ne\nf\n".to_string()
676
        );
677
    }
678

            
679
    #[test]
680
    fn parse_command() -> Result<()> {
681
        fn parse(s: &str) -> Result<DiffCommand<'_>> {
682
            let mut iter = s.lines();
683
            let cmd = DiffCommand::from_line_iterator(&mut iter)?;
684
            let cmd2 = DiffCommand::from_line_iterator(&mut iter)?;
685
            if cmd2.is_some() {
686
                panic!("Unexpected second command");
687
            }
688
            Ok(cmd.unwrap())
689
        }
690

            
691
        fn parse_err(s: &str) {
692
            let mut iter = s.lines();
693
            let cmd = DiffCommand::from_line_iterator(&mut iter);
694
            assert!(matches!(cmd, Err(Error::BadDiff(_))));
695
        }
696

            
697
        let p = parse("3,8d\n")?;
698
        assert!(matches!(p, DiffCommand::Delete { low: 3, high: 8 }));
699
        let p = parse("3d\n")?;
700
        assert!(matches!(p, DiffCommand::Delete { low: 3, high: 3 }));
701
        let p = parse("100,$d\n")?;
702
        assert!(matches!(p, DiffCommand::DeleteToEnd { low: 100 }));
703

            
704
        let p = parse("30,40c\nHello\nWorld\n.\n")?;
705
        assert!(matches!(
706
            p,
707
            DiffCommand::Replace {
708
                low: 30,
709
                high: 40,
710
                ..
711
            }
712
        ));
713
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
714
        let p = parse("30c\nHello\nWorld\n.\n")?;
715
        assert!(matches!(
716
            p,
717
            DiffCommand::Replace {
718
                low: 30,
719
                high: 30,
720
                ..
721
            }
722
        ));
723
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
724

            
725
        let p = parse("999a\nHello\nWorld\n.\n")?;
726
        assert!(matches!(p, DiffCommand::Insert { pos: 999, .. }));
727
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
728
        let p = parse("0a\nHello\nWorld\n.\n")?;
729
        assert!(matches!(p, DiffCommand::Insert { pos: 0, .. }));
730
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
731

            
732
        parse_err("hello world");
733
        parse_err("\n\n");
734
        parse_err("$,5d");
735
        parse_err("5,6,8d");
736
        parse_err("8,5d");
737
        parse_err("6");
738
        parse_err("d");
739
        parse_err("-10d");
740
        parse_err("4,$c\na\n.");
741
        parse_err("foo");
742
        parse_err("5,10p");
743
        parse_err("18446744073709551615a");
744
        parse_err("1,18446744073709551615d");
745

            
746
        Ok(())
747
    }
748

            
749
    #[test]
750
    fn apply_transformation() -> Result<()> {
751
        let example = DiffResult::from_str("1\n2\n3\n4\n5\n6\n7\n8\n9\n", [0; 32]);
752
        let empty = DiffResult::new([1; 32]);
753

            
754
        let mut inp = example.clone();
755
        let mut out = empty.clone();
756
        DiffCommand::DeleteToEnd { low: 5 }.apply_transformation(&mut inp, &mut out)?;
757
        assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
758
        assert_eq!(out.to_string(), "");
759

            
760
        let mut inp = example.clone();
761
        let mut out = empty.clone();
762
        DiffCommand::DeleteToEnd { low: 9 }.apply_transformation(&mut inp, &mut out)?;
763
        assert_eq!(inp.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n");
764
        assert_eq!(out.to_string(), "");
765

            
766
        let mut inp = example.clone();
767
        let mut out = empty.clone();
768
        DiffCommand::Delete { low: 3, high: 5 }.apply_transformation(&mut inp, &mut out)?;
769
        assert_eq!(inp.to_string(), "1\n2\n");
770
        assert_eq!(out.to_string(), "9\n8\n7\n6\n");
771

            
772
        let mut inp = example.clone();
773
        let mut out = empty.clone();
774
        DiffCommand::Replace {
775
            low: 5,
776
            high: 6,
777
            lines: vec!["oh hey", "there"],
778
        }
779
        .apply_transformation(&mut inp, &mut out)?;
780
        assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
781
        assert_eq!(out.to_string(), "9\n8\n7\nthere\noh hey\n");
782

            
783
        let mut inp = example.clone();
784
        let mut out = empty.clone();
785
        DiffCommand::Insert {
786
            pos: 3,
787
            lines: vec!["oh hey", "there"],
788
        }
789
        .apply_transformation(&mut inp, &mut out)?;
790
        assert_eq!(inp.to_string(), "1\n2\n3\n");
791
        assert_eq!(out.to_string(), "9\n8\n7\n6\n5\n4\nthere\noh hey\n");
792
        DiffCommand::Insert {
793
            pos: 0,
794
            lines: vec!["boom!"],
795
        }
796
        .apply_transformation(&mut inp, &mut out)?;
797
        assert_eq!(inp.to_string(), "");
798
        assert_eq!(
799
            out.to_string(),
800
            "9\n8\n7\n6\n5\n4\nthere\noh hey\n3\n2\n1\nboom!\n"
801
        );
802

            
803
        let mut inp = example.clone();
804
        let mut out = empty.clone();
805
        let r = DiffCommand::Delete {
806
            low: 100,
807
            high: 200,
808
        }
809
        .apply_transformation(&mut inp, &mut out);
810
        assert!(r.is_err());
811
        let r = DiffCommand::Delete { low: 5, high: 200 }.apply_transformation(&mut inp, &mut out);
812
        assert!(r.is_err());
813
        let r = DiffCommand::Delete { low: 0, high: 1 }.apply_transformation(&mut inp, &mut out);
814
        assert!(r.is_err());
815
        let r = DiffCommand::DeleteToEnd { low: 10 }.apply_transformation(&mut inp, &mut out);
816
        assert!(r.is_err());
817
        Ok(())
818
    }
819

            
820
    #[test]
821
    fn header() -> Result<()> {
822
        fn header_from(s: &str) -> Result<([u8; 32], [u8; 32])> {
823
            let mut iter = s.lines();
824
            parse_diff_header(&mut iter)
825
        }
826

            
827
        let (a,b) = header_from(
828
            "network-status-diff-version 1
829
hash B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663 F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB"
830
        )?;
831

            
832
        assert_eq!(
833
            &a[..],
834
            hex::decode("B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663")?
835
        );
836
        assert_eq!(
837
            &b[..],
838
            hex::decode("F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB")?
839
        );
840

            
841
        assert!(header_from("network-status-diff-version 2\n").is_err());
842
        assert!(header_from("").is_err());
843
        assert!(header_from("5,$d\n1,2d\n").is_err());
844
        assert!(header_from("network-status-diff-version 1\n").is_err());
845
        assert!(
846
            header_from(
847
                "network-status-diff-version 1
848
hash x y
849
5,5d"
850
            )
851
            .is_err()
852
        );
853
        assert!(
854
            header_from(
855
                "network-status-diff-version 1
856
hash x y
857
5,5d"
858
            )
859
            .is_err()
860
        );
861
        assert!(
862
            header_from(
863
                "network-status-diff-version 1
864
hash AA BB
865
5,5d"
866
            )
867
            .is_err()
868
        );
869
        assert!(
870
            header_from(
871
                "network-status-diff-version 1
872
oh hello there
873
5,5d"
874
            )
875
            .is_err()
876
        );
877
        assert!(header_from("network-status-diff-version 1
878
hash B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663 F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB extra").is_err());
879

            
880
        Ok(())
881
    }
882

            
883
    #[test]
884
    fn apply_simple() {
885
        let pre = include_str!("../testdata/consensus1.txt");
886
        let diff = include_str!("../testdata/diff1.txt");
887
        let post = include_str!("../testdata/consensus2.txt");
888

            
889
        let result = apply_diff_trivial(pre, diff).unwrap();
890
        assert!(result.check_digest().is_ok());
891
        assert_eq!(result.to_string(), post);
892
    }
893

            
894
    #[test]
895
    fn sort_order() -> Result<()> {
896
        fn cmds(s: &str) -> Result<Vec<DiffCommand<'_>>> {
897
            let mut out = Vec::new();
898
            for cmd in DiffCommandIter::new(s.lines()) {
899
                out.push(cmd?);
900
            }
901
            Ok(out)
902
        }
903

            
904
        let _ = cmds("6,9d\n5,5d\n")?;
905
        assert!(cmds("5,5d\n6,9d\n").is_err());
906
        assert!(cmds("5,5d\n6,6d\n").is_err());
907
        assert!(cmds("5,5d\n5,6d\n").is_err());
908

            
909
        Ok(())
910
    }
911
}