1
1
//! `tor-consdiff`: Restricted ed diff and patch formats for Tor.
2
//!
3
//! # Overview
4
//!
5
//! This crate is part of
6
//! [Arti](https://gitlab.torproject.org/tpo/core/arti/), a project to
7
//! implement [Tor](https://www.torproject.org/) in Rust.
8
//! Tor uses a restricted version of the "ed-style" diff format to
9
//! record the difference between a pair of consensus documents, so that
10
//! clients can download only the changes since the last document they
11
//! have.
12
//!
13
//! This crate provides a function to apply one of these diffs to an older
14
//! consensus document, to get a newer one.
15
//!
16
//! TODO: Eventually, when we add relay support, we will need to generate
17
//! these diffs as well as consume them.
18

            
19
#![deny(missing_docs)]
20
#![warn(noop_method_call)]
21
#![deny(unreachable_pub)]
22
#![warn(clippy::all)]
23
#![deny(clippy::await_holding_lock)]
24
#![deny(clippy::cargo_common_metadata)]
25
#![deny(clippy::cast_lossless)]
26
#![deny(clippy::checked_conversions)]
27
#![warn(clippy::cognitive_complexity)]
28
#![deny(clippy::debug_assert_with_mut_call)]
29
#![deny(clippy::exhaustive_enums)]
30
#![deny(clippy::exhaustive_structs)]
31
#![deny(clippy::expl_impl_clone_on_copy)]
32
#![deny(clippy::fallible_impl_from)]
33
#![deny(clippy::implicit_clone)]
34
#![deny(clippy::large_stack_arrays)]
35
#![warn(clippy::manual_ok_or)]
36
#![deny(clippy::missing_docs_in_private_items)]
37
#![deny(clippy::missing_panics_doc)]
38
#![warn(clippy::needless_borrow)]
39
#![warn(clippy::needless_pass_by_value)]
40
#![warn(clippy::option_option)]
41
#![warn(clippy::rc_buffer)]
42
#![deny(clippy::ref_option_ref)]
43
#![warn(clippy::semicolon_if_nothing_returned)]
44
#![warn(clippy::trait_duplication_in_bounds)]
45
#![deny(clippy::unnecessary_wraps)]
46
#![warn(clippy::unseparated_literal_suffix)]
47
#![deny(clippy::unwrap_used)]
48

            
49
use std::convert::TryInto;
50
use std::fmt::{Display, Formatter};
51
use std::num::NonZeroUsize;
52
use std::str::FromStr;
53

            
54
mod err;
55
pub use err::Error;
56

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

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

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

            
76
1
    let mut diffable = DiffResult::from_str(input, d2);
77

            
78
12
    for command in DiffCommandIter::new(diff_lines) {
79
12
        command?.apply_to(&mut diffable)?;
80
    }
81

            
82
1
    Ok(diffable)
83
1
}
84

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

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

            
105
36
    let mut output = DiffResult::new(d2);
106

            
107
168
    for command in DiffCommandIter::new(diff_lines) {
108
168
        command?.apply_transformation(&mut input, &mut output)?;
109
    }
110

            
111
36
    output.push_reversed(&input.lines[..]);
112
36

            
113
36
    output.lines.reverse();
114
36
    Ok(output)
115
36
}
116

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

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

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

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

            
203
impl FromStr for RangeEnd {
204
    type Err = Error;
205
120
    fn from_str(s: &str) -> Result<RangeEnd> {
206
120
        if s == "$" {
207
15
            Ok(RangeEnd::DollarSign)
208
        } else {
209
105
            let v: NonZeroUsize = s.parse()?;
210
104
            if v.get() == std::usize::MAX {
211
1
                return Err(Error::BadDiff("range end cannot at usize::MAX"));
212
103
            }
213
103
            Ok(RangeEnd::Num(v))
214
        }
215
120
    }
216
}
217

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

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

            
280
176
        if let Some(lines) = self.lines() {
281
123
            // These are the lines we're inserting.
282
123
            output.push_reversed(lines);
283
125
        }
284

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

            
293
174
        Ok(())
294
178
    }
295

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

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

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

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

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

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

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

            
358
208
        if command.len() < 2 || !command.is_ascii() {
359
3
            return Err(Error::BadDiff("command too short"));
360
205
        }
361
205

            
362
205
        let (range, command) = command.split_at(command.len() - 1);
363
205
        let (low, high) = if let Some(comma_pos) = range.find(',') {
364
            (
365
121
                range[..comma_pos].parse::<usize>()?,
366
120
                Some(range[comma_pos + 1..].parse::<RangeEnd>()?),
367
            )
368
        } else {
369
84
            (range.parse::<usize>()?, None)
370
        };
371

            
372
199
        if low == std::usize::MAX {
373
1
            return Err(Error::BadDiff("range cannot begin at usize::MAX"));
374
198
        }
375
198

            
376
198
        match (low, high) {
377
103
            (lo, Some(RangeEnd::Num(hi))) if lo > hi.into() => {
378
1
                return Err(Error::BadDiff("mis-ordered lines in range"))
379
            }
380
197
            (_, _) => (),
381
        }
382

            
383
197
        let mut cmd = match (command, low, high) {
384
197
            ("d", low, None) => Self::Delete { low, high: low },
385
48
            ("d", low, Some(RangeEnd::Num(high))) => Self::Delete {
386
48
                low,
387
48
                high: high.into(),
388
48
            },
389
14
            ("d", low, Some(RangeEnd::DollarSign)) => Self::DeleteToEnd { low },
390
134
            ("c", low, None) => Self::Replace {
391
64
                low,
392
64
                high: low,
393
64
                lines: Vec::new(),
394
64
            },
395
53
            ("c", low, Some(RangeEnd::Num(high))) => Self::Replace {
396
53
                low,
397
53
                high: high.into(),
398
53
                lines: Vec::new(),
399
53
            },
400
17
            ("a", low, None) => Self::Insert {
401
15
                pos: low,
402
15
                lines: Vec::new(),
403
15
            },
404
2
            (_, _, _) => return Err(Error::BadDiff("can't parse command line")),
405
        };
406

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

            
419
195
        Ok(Some(cmd))
420
253
    }
421
}
422

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

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

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

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

            
474
impl<'a> DiffResult<'a> {
475
    /// Construct a new DiffResult containing the provided string
476
    /// split into lines, and an expected post-transformation digests.
477
41
    fn from_str(s: &'a str, d_post: [u8; 32]) -> Self {
478
41
        // I'd like to use str::split_inclusive here, but that isn't stable yet
479
41
        // as of rust 1.48.
480
41

            
481
41
        let lines: Vec<_> = s.lines().collect();
482
41

            
483
41
        DiffResult { d_post, lines }
484
41
    }
485

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

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

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

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

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

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

            
566
#[cfg(test)]
567
mod test {
568
    #![allow(clippy::unwrap_used)]
569
    use super::*;
570

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

            
575
        let mut d = example.clone();
576
        d.remove_lines(5, 7)?;
577
        assert_eq!(d.to_string(), "1\n2\n3\n4\n8\n9\n");
578

            
579
        let mut d = example.clone();
580
        d.remove_lines(1, 9)?;
581
        assert_eq!(d.to_string(), "");
582

            
583
        let mut d = example.clone();
584
        d.remove_lines(1, 1)?;
585
        assert_eq!(d.to_string(), "2\n3\n4\n5\n6\n7\n8\n9\n");
586

            
587
        let mut d = example.clone();
588
        d.remove_lines(6, 9)?;
589
        assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n");
590

            
591
        let mut d = example.clone();
592
        assert!(d.remove_lines(6, 10).is_err());
593
        assert!(d.remove_lines(0, 1).is_err());
594
        assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n9\n");
595

            
596
        Ok(())
597
    }
598

            
599
    #[test]
600
    fn insert() -> Result<()> {
601
        let example = DiffResult::from_str("1\n2\n3\n4\n5\n", [0; 32]);
602
        let mut d = example.clone();
603
        d.insert_at(3, &["hello", "world"])?;
604
        assert_eq!(d.to_string(), "1\n2\nhello\nworld\n3\n4\n5\n");
605

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

            
610
        let mut d = example.clone();
611
        assert!(d.insert_at(0, &["hello", "world"]).is_err());
612
        assert!(d.insert_at(7, &["hello", "world"]).is_err());
613
        Ok(())
614
    }
615

            
616
    #[test]
617
    fn push_reversed() {
618
        let mut d = DiffResult::new([0; 32]);
619
        d.push_reversed(&["7", "8", "9"]);
620
        assert_eq!(d.to_string(), "9\n8\n7\n");
621
        d.push_reversed(&["world", "hello", ""]);
622
        assert_eq!(d.to_string(), "9\n8\n7\n\nhello\nworld\n");
623
    }
624

            
625
    #[test]
626
    fn apply_command_simple() {
627
        let example = DiffResult::from_str("a\nb\nc\nd\ne\nf\n", [0; 32]);
628

            
629
        let mut d = example.clone();
630
        assert_eq!(d.to_string(), "a\nb\nc\nd\ne\nf\n".to_string());
631
        assert!(DiffCommand::DeleteToEnd { low: 5 }.apply_to(&mut d).is_ok());
632
        assert_eq!(d.to_string(), "a\nb\nc\nd\n".to_string());
633

            
634
        let mut d = example.clone();
635
        assert!(DiffCommand::Delete { low: 3, high: 5 }
636
            .apply_to(&mut d)
637
            .is_ok());
638
        assert_eq!(d.to_string(), "a\nb\nf\n".to_string());
639

            
640
        let mut d = example.clone();
641
        assert!(DiffCommand::Replace {
642
            low: 3,
643
            high: 5,
644
            lines: vec!["hello", "world"]
645
        }
646
        .apply_to(&mut d)
647
        .is_ok());
648
        assert_eq!(d.to_string(), "a\nb\nhello\nworld\nf\n".to_string());
649

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

            
663
    #[test]
664
    fn parse_command() -> Result<()> {
665
        fn parse(s: &str) -> Result<DiffCommand<'_>> {
666
            let mut iter = s.lines();
667
            let cmd = DiffCommand::from_line_iterator(&mut iter)?;
668
            let cmd2 = DiffCommand::from_line_iterator(&mut iter)?;
669
            if cmd2.is_some() {
670
                panic!("Unexpected second command");
671
            }
672
            Ok(cmd.unwrap())
673
        }
674

            
675
        fn parse_err(s: &str) {
676
            let mut iter = s.lines();
677
            let cmd = DiffCommand::from_line_iterator(&mut iter);
678
            assert!(matches!(cmd, Err(Error::BadDiff(_))));
679
        }
680

            
681
        let p = parse("3,8d\n")?;
682
        assert!(matches!(p, DiffCommand::Delete { low: 3, high: 8 }));
683
        let p = parse("3d\n")?;
684
        assert!(matches!(p, DiffCommand::Delete { low: 3, high: 3 }));
685
        let p = parse("100,$d\n")?;
686
        assert!(matches!(p, DiffCommand::DeleteToEnd { low: 100 }));
687

            
688
        let p = parse("30,40c\nHello\nWorld\n.\n")?;
689
        assert!(matches!(
690
            p,
691
            DiffCommand::Replace {
692
                low: 30,
693
                high: 40,
694
                ..
695
            }
696
        ));
697
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
698
        let p = parse("30c\nHello\nWorld\n.\n")?;
699
        assert!(matches!(
700
            p,
701
            DiffCommand::Replace {
702
                low: 30,
703
                high: 30,
704
                ..
705
            }
706
        ));
707
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
708

            
709
        let p = parse("999a\nHello\nWorld\n.\n")?;
710
        assert!(matches!(p, DiffCommand::Insert { pos: 999, .. }));
711
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
712
        let p = parse("0a\nHello\nWorld\n.\n")?;
713
        assert!(matches!(p, DiffCommand::Insert { pos: 0, .. }));
714
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
715

            
716
        parse_err("hello world");
717
        parse_err("\n\n");
718
        parse_err("$,5d");
719
        parse_err("5,6,8d");
720
        parse_err("8,5d");
721
        parse_err("6");
722
        parse_err("d");
723
        parse_err("-10d");
724
        parse_err("4,$c\na\n.");
725
        parse_err("foo");
726
        parse_err("5,10p");
727
        parse_err("18446744073709551615a");
728
        parse_err("1,18446744073709551615d");
729

            
730
        Ok(())
731
    }
732

            
733
    #[test]
734
    fn apply_transformation() -> Result<()> {
735
        let example = DiffResult::from_str("1\n2\n3\n4\n5\n6\n7\n8\n9\n", [0; 32]);
736
        let empty = DiffResult::new([1; 32]);
737

            
738
        let mut inp = example.clone();
739
        let mut out = empty.clone();
740
        DiffCommand::DeleteToEnd { low: 5 }.apply_transformation(&mut inp, &mut out)?;
741
        assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
742
        assert_eq!(out.to_string(), "");
743

            
744
        let mut inp = example.clone();
745
        let mut out = empty.clone();
746
        DiffCommand::DeleteToEnd { low: 9 }.apply_transformation(&mut inp, &mut out)?;
747
        assert_eq!(inp.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n");
748
        assert_eq!(out.to_string(), "");
749

            
750
        let mut inp = example.clone();
751
        let mut out = empty.clone();
752
        DiffCommand::Delete { low: 3, high: 5 }.apply_transformation(&mut inp, &mut out)?;
753
        assert_eq!(inp.to_string(), "1\n2\n");
754
        assert_eq!(out.to_string(), "9\n8\n7\n6\n");
755

            
756
        let mut inp = example.clone();
757
        let mut out = empty.clone();
758
        DiffCommand::Replace {
759
            low: 5,
760
            high: 6,
761
            lines: vec!["oh hey", "there"],
762
        }
763
        .apply_transformation(&mut inp, &mut out)?;
764
        assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
765
        assert_eq!(out.to_string(), "9\n8\n7\nthere\noh hey\n");
766

            
767
        let mut inp = example.clone();
768
        let mut out = empty.clone();
769
        DiffCommand::Insert {
770
            pos: 3,
771
            lines: vec!["oh hey", "there"],
772
        }
773
        .apply_transformation(&mut inp, &mut out)?;
774
        assert_eq!(inp.to_string(), "1\n2\n3\n");
775
        assert_eq!(out.to_string(), "9\n8\n7\n6\n5\n4\nthere\noh hey\n");
776
        DiffCommand::Insert {
777
            pos: 0,
778
            lines: vec!["boom!"],
779
        }
780
        .apply_transformation(&mut inp, &mut out)?;
781
        assert_eq!(inp.to_string(), "");
782
        assert_eq!(
783
            out.to_string(),
784
            "9\n8\n7\n6\n5\n4\nthere\noh hey\n3\n2\n1\nboom!\n"
785
        );
786

            
787
        let mut inp = example.clone();
788
        let mut out = empty.clone();
789
        let r = DiffCommand::Delete {
790
            low: 100,
791
            high: 200,
792
        }
793
        .apply_transformation(&mut inp, &mut out);
794
        assert!(r.is_err());
795
        let r = DiffCommand::Delete { low: 5, high: 200 }.apply_transformation(&mut inp, &mut out);
796
        assert!(r.is_err());
797
        let r = DiffCommand::Delete { low: 0, high: 1 }.apply_transformation(&mut inp, &mut out);
798
        assert!(r.is_err());
799
        let r = DiffCommand::DeleteToEnd { low: 10 }.apply_transformation(&mut inp, &mut out);
800
        assert!(r.is_err());
801
        Ok(())
802
    }
803

            
804
    #[test]
805
    fn header() -> Result<()> {
806
        fn header_from(s: &str) -> Result<([u8; 32], [u8; 32])> {
807
            let mut iter = s.lines();
808
            parse_diff_header(&mut iter)
809
        }
810

            
811
        let (a,b) = header_from(
812
            "network-status-diff-version 1
813
hash B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663 F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB"
814
        )?;
815

            
816
        assert_eq!(
817
            &a[..],
818
            hex::decode("B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663")?
819
        );
820
        assert_eq!(
821
            &b[..],
822
            hex::decode("F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB")?
823
        );
824

            
825
        assert!(header_from("network-status-diff-version 2\n").is_err());
826
        assert!(header_from("").is_err());
827
        assert!(header_from("5,$d\n1,2d\n").is_err());
828
        assert!(header_from("network-status-diff-version 1\n").is_err());
829
        assert!(header_from(
830
            "network-status-diff-version 1
831
hash x y
832
5,5d"
833
        )
834
        .is_err());
835
        assert!(header_from(
836
            "network-status-diff-version 1
837
hash x y
838
5,5d"
839
        )
840
        .is_err());
841
        assert!(header_from(
842
            "network-status-diff-version 1
843
hash AA BB
844
5,5d"
845
        )
846
        .is_err());
847
        assert!(header_from(
848
            "network-status-diff-version 1
849
oh hello there
850
5,5d"
851
        )
852
        .is_err());
853
        assert!(header_from("network-status-diff-version 1
854
hash B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663 F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB extra").is_err());
855

            
856
        Ok(())
857
    }
858

            
859
    #[test]
860
    fn apply_simple() {
861
        let pre = include_str!("../testdata/consensus1.txt");
862
        let diff = include_str!("../testdata/diff1.txt");
863
        let post = include_str!("../testdata/consensus2.txt");
864

            
865
        let result = apply_diff_trivial(pre, diff).unwrap();
866
        assert!(result.check_digest().is_ok());
867
        assert_eq!(result.to_string(), post);
868
    }
869

            
870
    #[test]
871
    fn sort_order() -> Result<()> {
872
        fn cmds(s: &str) -> Result<Vec<DiffCommand<'_>>> {
873
            let mut out = Vec::new();
874
            for cmd in DiffCommandIter::new(s.lines()) {
875
                out.push(cmd?);
876
            }
877
            Ok(out)
878
        }
879

            
880
        let _ = cmds("6,9d\n5,5d\n")?;
881
        assert!(cmds("5,5d\n6,9d\n").is_err());
882
        assert!(cmds("5,5d\n6,6d\n").is_err());
883
        assert!(cmds("5,5d\n5,6d\n").is_err());
884

            
885
        Ok(())
886
    }
887
}