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
#![allow(clippy::collapsible_if)] // See arti#2342
46
#![deny(clippy::unused_async)]
47
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
48

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

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

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

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

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

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

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

            
81
2
    Ok(diffable)
82
2
}
83

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
292
530
        Ok(())
293
538
    }
294

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
607
        Ok(())
608
    }
609

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
747
        Ok(())
748
    }
749

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

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

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

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

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

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

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

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

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

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

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

            
881
        Ok(())
882
    }
883

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

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

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

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

            
910
        Ok(())
911
    }
912
}