tor_consdiff/
lib.rs

1#![cfg_attr(docsrs, feature(doc_auto_cfg, 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_duration_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//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
45
46use std::fmt::{Display, Formatter};
47use std::num::NonZeroUsize;
48use std::str::FromStr;
49
50mod err;
51pub use err::Error;
52
53/// Result type used by this crate
54type Result<T> = std::result::Result<T, Error>;
55
56/// Return true if `s` looks more like a consensus diff than some other kind
57/// of document.
58pub fn looks_like_diff(s: &str) -> bool {
59    s.starts_with("network-status-diff-version")
60}
61
62/// Apply a given diff to an input text, and return the result from applying
63/// that diff.
64///
65/// This is a slow version, for testing and correctness checking.  It uses
66/// an O(n) operation to apply diffs, and therefore runs in O(n^2) time.
67#[cfg(any(test, feature = "slow-diff-apply"))]
68pub fn apply_diff_trivial<'a>(input: &'a str, diff: &'a str) -> Result<DiffResult<'a>> {
69    let mut diff_lines = diff.lines();
70    let (_, d2) = parse_diff_header(&mut diff_lines)?;
71
72    let mut diffable = DiffResult::from_str(input, d2);
73
74    for command in DiffCommandIter::new(diff_lines) {
75        command?.apply_to(&mut diffable)?;
76    }
77
78    Ok(diffable)
79}
80
81/// Apply a given diff to an input text, and return the result from applying
82/// that diff.
83///
84/// If `check_digest_in` is provided, require the diff to say that it
85/// applies to a document with the provided digest.
86pub fn apply_diff<'a>(
87    input: &'a str,
88    diff: &'a str,
89    check_digest_in: Option<[u8; 32]>,
90) -> Result<DiffResult<'a>> {
91    let mut input = DiffResult::from_str(input, [0; 32]);
92
93    let mut diff_lines = diff.lines();
94    let (d1, d2) = parse_diff_header(&mut diff_lines)?;
95    if let Some(d_want) = check_digest_in {
96        if d1 != d_want {
97            return Err(Error::CantApply("listed digest does not match document"));
98        }
99    }
100
101    let mut output = DiffResult::new(d2);
102
103    for command in DiffCommandIter::new(diff_lines) {
104        command?.apply_transformation(&mut input, &mut output)?;
105    }
106
107    output.push_reversed(&input.lines[..]);
108
109    output.lines.reverse();
110    Ok(output)
111}
112
113/// Given a line iterator, check to make sure the first two lines are
114/// a valid diff header as specified in dir-spec.txt.
115fn parse_diff_header<'a, I>(iter: &mut I) -> Result<([u8; 32], [u8; 32])>
116where
117    I: Iterator<Item = &'a str>,
118{
119    let line1 = iter.next();
120    if line1 != Some("network-status-diff-version 1") {
121        return Err(Error::BadDiff("unrecognized or missing header"));
122    }
123    let line2 = iter.next().ok_or(Error::BadDiff("header truncated"))?;
124    if !line2.starts_with("hash ") {
125        return Err(Error::BadDiff("missing 'hash' line"));
126    }
127    let elts: Vec<_> = line2.split_ascii_whitespace().collect();
128    if elts.len() != 3 {
129        return Err(Error::BadDiff("invalid 'hash' line"));
130    }
131    let d1 = hex::decode(elts[1])?;
132    let d2 = hex::decode(elts[2])?;
133    match (d1.try_into(), d2.try_into()) {
134        (Ok(a), Ok(b)) => Ok((a, b)),
135        _ => Err(Error::BadDiff("wrong digest lengths on 'hash' line")),
136    }
137}
138
139/// A command that can appear in a diff.  Each command tells us to
140/// remove zero or more lines, and insert zero or more lines in their
141/// place.
142///
143/// Commands refer to lines by 1-indexed line number.
144#[derive(Clone, Debug)]
145enum DiffCommand<'a> {
146    /// Remove the lines from low through high, inclusive.
147    Delete {
148        /// The first line to remove
149        low: usize,
150        /// The last line to remove
151        high: usize,
152    },
153    /// Remove the lines from low through the end of the file, inclusive.
154    DeleteToEnd {
155        /// The first line to remove
156        low: usize,
157    },
158    /// Replace the lines from low through high, inclusive, with the
159    /// lines in 'lines'.
160    Replace {
161        /// The first line to replace
162        low: usize,
163        /// The last line to replace
164        high: usize,
165        /// The text to insert instead
166        lines: Vec<&'a str>,
167    },
168    /// Insert the provided 'lines' after the line with index 'pos'.
169    Insert {
170        /// The position after which to insert the text
171        pos: usize,
172        /// The text to insert
173        lines: Vec<&'a str>,
174    },
175}
176
177/// The result of applying one or more diff commands to an input string.
178///
179/// It refers to lines from the diff and the input by reference, to
180/// avoid copying.
181#[derive(Clone, Debug)]
182pub struct DiffResult<'a> {
183    /// An expected digest of the output, after it has been assembled.
184    d_post: [u8; 32],
185    /// The lines in the output.
186    lines: Vec<&'a str>,
187}
188
189/// A possible value for the end of a range.  It can be either a line number,
190/// or a dollar sign indicating "end of file".
191#[derive(Clone, Copy, Debug)]
192enum RangeEnd {
193    /// A line number in the file.
194    Num(NonZeroUsize),
195    /// A dollar sign, indicating "end of file" in a delete command.
196    DollarSign,
197}
198
199impl FromStr for RangeEnd {
200    type Err = Error;
201    fn from_str(s: &str) -> Result<RangeEnd> {
202        if s == "$" {
203            Ok(RangeEnd::DollarSign)
204        } else {
205            let v: NonZeroUsize = s.parse()?;
206            if v.get() == usize::MAX {
207                return Err(Error::BadDiff("range cannot end at usize::MAX"));
208            }
209            Ok(RangeEnd::Num(v))
210        }
211    }
212}
213
214impl<'a> DiffCommand<'a> {
215    /// Transform 'target' according to the this command.
216    ///
217    /// Because DiffResult internally uses a vector of line, this
218    /// implementation is potentially O(n) in the size of the input.
219    #[cfg(any(test, feature = "slow-diff-apply"))]
220    fn apply_to(&self, target: &mut DiffResult<'a>) -> Result<()> {
221        match self {
222            Self::Delete { low, high } => {
223                target.remove_lines(*low, *high)?;
224            }
225            Self::DeleteToEnd { low } => {
226                target.remove_lines(*low, target.lines.len())?;
227            }
228            Self::Replace { low, high, lines } => {
229                target.remove_lines(*low, *high)?;
230                target.insert_at(*low, lines)?;
231            }
232            Self::Insert { pos, lines } => {
233                // This '+1' seems off, but it's what the spec says. I wonder
234                // if the spec is wrong.
235                target.insert_at(*pos + 1, lines)?;
236            }
237        };
238        Ok(())
239    }
240
241    /// Apply this command to 'input', moving lines into 'output'.
242    ///
243    /// This is a more efficient algorithm, but it requires that the
244    /// diff commands are sorted in reverse order by line
245    /// number. (Fortunately, the Tor ed diff format guarantees this.)
246    ///
247    /// Before calling this method, input and output must contain the
248    /// results of having applied the previous command in the diff.
249    /// (When no commands have been applied, input starts out as the
250    /// original text, and output starts out empty.)
251    ///
252    /// This method applies the command by copying unaffected lines
253    /// from the _end_ of input into output, adding any lines inserted
254    /// by this command, and finally deleting any affected lines from
255    /// input.
256    ///
257    /// We build the `output` value in reverse order, and then put it
258    /// back to normal before giving it to the user.
259    fn apply_transformation(
260        &self,
261        input: &mut DiffResult<'a>,
262        output: &mut DiffResult<'a>,
263    ) -> Result<()> {
264        if let Some(succ) = self.following_lines() {
265            if let Some(subslice) = input.lines.get(succ - 1..) {
266                // Lines from `succ` onwards are unaffected.  Copy them.
267                output.push_reversed(subslice);
268            } else {
269                // Oops, dubious line number.
270                return Err(Error::CantApply(
271                    "ending line number didn't correspond to document",
272                ));
273            }
274        }
275
276        if let Some(lines) = self.lines() {
277            // These are the lines we're inserting.
278            output.push_reversed(lines);
279        }
280
281        let remove = self.first_removed_line();
282        if remove == 0 || (!self.is_insert() && remove > input.lines.len()) {
283            return Err(Error::CantApply(
284                "starting line number didn't correspond to document",
285            ));
286        }
287        input.lines.truncate(remove - 1);
288
289        Ok(())
290    }
291
292    /// Return the lines that we should add to the output
293    fn lines(&self) -> Option<&[&'a str]> {
294        match self {
295            Self::Replace { lines, .. } | Self::Insert { lines, .. } => Some(lines.as_slice()),
296            _ => None,
297        }
298    }
299
300    /// Return a mutable reference to the vector of lines we should
301    /// add to the output.
302    fn linebuf_mut(&mut self) -> Option<&mut Vec<&'a str>> {
303        match self {
304            Self::Replace { ref mut lines, .. } | Self::Insert { ref mut lines, .. } => Some(lines),
305            _ => None,
306        }
307    }
308
309    /// Return the (1-indexed) line number of the first line in the
310    /// input that comes _after_ this command, and is not affected by it.
311    ///
312    /// We use this line number to know which lines we should copy.
313    fn following_lines(&self) -> Option<usize> {
314        match self {
315            Self::Delete { high, .. } | Self::Replace { high, .. } => Some(high + 1),
316            Self::DeleteToEnd { .. } => None,
317            Self::Insert { pos, .. } => Some(pos + 1),
318        }
319    }
320
321    /// Return the (1-indexed) line number of the first line that we
322    /// should clear from the input when processing this command.
323    ///
324    /// This can be the same as following_lines(), if we shouldn't
325    /// actually remove any lines.
326    fn first_removed_line(&self) -> usize {
327        match self {
328            Self::Delete { low, .. } => *low,
329            Self::DeleteToEnd { low } => *low,
330            Self::Replace { low, .. } => *low,
331            Self::Insert { pos, .. } => *pos + 1,
332        }
333    }
334
335    /// Return true if this is an Insert command.
336    fn is_insert(&self) -> bool {
337        matches!(self, Self::Insert { .. })
338    }
339
340    /// Extract a single command from a line iterator that yields lines
341    /// of the diffs.  Return None if we're at the end of the iterator.
342    fn from_line_iterator<I>(iter: &mut I) -> Result<Option<Self>>
343    where
344        I: Iterator<Item = &'a str>,
345    {
346        let command = match iter.next() {
347            Some(s) => s,
348            None => return Ok(None),
349        };
350
351        // `command` can be of these forms: `Rc`, `Rd`, `N,$d`, and `Na`,
352        // where R is a range of form `N,N`, and where N is a line number.
353
354        if command.len() < 2 || !command.is_ascii() {
355            return Err(Error::BadDiff("command too short"));
356        }
357
358        let (range, command) = command.split_at(command.len() - 1);
359        let (low, high) = if let Some(comma_pos) = range.find(',') {
360            (
361                range[..comma_pos].parse::<usize>()?,
362                Some(range[comma_pos + 1..].parse::<RangeEnd>()?),
363            )
364        } else {
365            (range.parse::<usize>()?, None)
366        };
367
368        if low == usize::MAX {
369            return Err(Error::BadDiff("range cannot begin at usize::MAX"));
370        }
371
372        match (low, high) {
373            (lo, Some(RangeEnd::Num(hi))) if lo > hi.into() => {
374                return Err(Error::BadDiff("mis-ordered lines in range"))
375            }
376            (_, _) => (),
377        }
378
379        let mut cmd = match (command, low, high) {
380            ("d", low, None) => Self::Delete { low, high: low },
381            ("d", low, Some(RangeEnd::Num(high))) => Self::Delete {
382                low,
383                high: high.into(),
384            },
385            ("d", low, Some(RangeEnd::DollarSign)) => Self::DeleteToEnd { low },
386            ("c", low, None) => Self::Replace {
387                low,
388                high: low,
389                lines: Vec::new(),
390            },
391            ("c", low, Some(RangeEnd::Num(high))) => Self::Replace {
392                low,
393                high: high.into(),
394                lines: Vec::new(),
395            },
396            ("a", low, None) => Self::Insert {
397                pos: low,
398                lines: Vec::new(),
399            },
400            (_, _, _) => return Err(Error::BadDiff("can't parse command line")),
401        };
402
403        if let Some(ref mut linebuf) = cmd.linebuf_mut() {
404            // The 'c' and 'a' commands take a series of lines followed by a
405            // line containing a period.
406            loop {
407                match iter.next() {
408                    None => return Err(Error::BadDiff("unterminated block to insert")),
409                    Some(".") => break,
410                    Some(line) => linebuf.push(line),
411                }
412            }
413        }
414
415        Ok(Some(cmd))
416    }
417}
418
419/// Iterator that wraps a line iterator and returns a sequence of
420/// `Result<DiffCommand>`.
421///
422/// This iterator forces the commands to affect the file in reverse order,
423/// so that we can use the O(n) algorithm for applying these diffs.
424struct DiffCommandIter<'a, I>
425where
426    I: Iterator<Item = &'a str>,
427{
428    /// The underlying iterator.
429    iter: I,
430
431    /// The 'first removed line' of the last-parsed command; used to ensure
432    /// that commands appear in reverse order.
433    last_cmd_first_removed: Option<usize>,
434}
435
436impl<'a, I> DiffCommandIter<'a, I>
437where
438    I: Iterator<Item = &'a str>,
439{
440    /// Construct a new DiffCommandIter wrapping `iter`.
441    fn new(iter: I) -> Self {
442        DiffCommandIter {
443            iter,
444            last_cmd_first_removed: None,
445        }
446    }
447}
448
449impl<'a, I> Iterator for DiffCommandIter<'a, I>
450where
451    I: Iterator<Item = &'a str>,
452{
453    type Item = Result<DiffCommand<'a>>;
454    fn next(&mut self) -> Option<Result<DiffCommand<'a>>> {
455        match DiffCommand::from_line_iterator(&mut self.iter) {
456            Err(e) => Some(Err(e)),
457            Ok(None) => None,
458            Ok(Some(c)) => match (self.last_cmd_first_removed, c.following_lines()) {
459                (Some(_), None) => Some(Err(Error::BadDiff("misordered commands"))),
460                (Some(a), Some(b)) if a < b => Some(Err(Error::BadDiff("misordered commands"))),
461                (_, _) => {
462                    self.last_cmd_first_removed = Some(c.first_removed_line());
463                    Some(Ok(c))
464                }
465            },
466        }
467    }
468}
469
470impl<'a> DiffResult<'a> {
471    /// Construct a new DiffResult containing the provided string
472    /// split into lines, and an expected post-transformation digest.
473    fn from_str(s: &'a str, d_post: [u8; 32]) -> Self {
474        // As per the [netdoc syntax], newlines should be discarded and ignored.
475        //
476        // [netdoc syntax]: https://spec.torproject.org/dir-spec/netdoc.html#netdoc-syntax
477        let lines: Vec<_> = s.lines().collect();
478
479        DiffResult { d_post, lines }
480    }
481
482    /// Return a new empty DiffResult with an expected
483    /// post-transformation digests
484    fn new(d_post: [u8; 32]) -> Self {
485        DiffResult {
486            d_post,
487            lines: Vec::new(),
488        }
489    }
490
491    /// Put every member of `lines` at the end of this DiffResult, in
492    /// reverse order.
493    fn push_reversed(&mut self, lines: &[&'a str]) {
494        self.lines.extend(lines.iter().rev());
495    }
496
497    /// Remove the 1-indexed lines from `first` through `last` inclusive.
498    ///
499    /// This has to move elements around within the vector, and so it
500    /// is potentially O(n) in its length.
501    #[cfg(any(test, feature = "slow-diff-apply"))]
502    fn remove_lines(&mut self, first: usize, last: usize) -> Result<()> {
503        if first > self.lines.len() || last > self.lines.len() || first == 0 || last == 0 {
504            Err(Error::CantApply("line out of range"))
505        } else {
506            let n_to_remove = last - first + 1;
507            if last != self.lines.len() {
508                self.lines[..].copy_within((last).., first - 1);
509            }
510            self.lines.truncate(self.lines.len() - n_to_remove);
511            Ok(())
512        }
513    }
514
515    /// Insert the provided `lines` so that they appear at 1-indexed
516    /// position `pos`.
517    ///
518    /// This has to move elements around within the vector, and so it
519    /// is potentially O(n) in its length.
520    #[cfg(any(test, feature = "slow-diff-apply"))]
521    fn insert_at(&mut self, pos: usize, lines: &[&'a str]) -> Result<()> {
522        if pos > self.lines.len() + 1 || pos == 0 {
523            Err(Error::CantApply("position out of range"))
524        } else {
525            let orig_len = self.lines.len();
526            self.lines.resize(self.lines.len() + lines.len(), "");
527            self.lines
528                .copy_within(pos - 1..orig_len, pos - 1 + lines.len());
529            self.lines[(pos - 1)..(pos + lines.len() - 1)].copy_from_slice(lines);
530            Ok(())
531        }
532    }
533
534    /// See whether the output of this diff matches the target digest.
535    ///
536    /// If not, return an error.
537    pub fn check_digest(&self) -> Result<()> {
538        use digest::Digest;
539        use tor_llcrypto::d::Sha3_256;
540        let mut d = Sha3_256::new();
541        for line in &self.lines {
542            d.update(line.as_bytes());
543            d.update(b"\n");
544        }
545        if d.finalize() == self.d_post.into() {
546            Ok(())
547        } else {
548            Err(Error::CantApply("Wrong digest after applying diff"))
549        }
550    }
551}
552
553impl<'a> Display for DiffResult<'a> {
554    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
555        for elt in &self.lines {
556            writeln!(f, "{}", elt)?;
557        }
558        Ok(())
559    }
560}
561
562#[cfg(test)]
563mod test {
564    // @@ begin test lint list maintained by maint/add_warning @@
565    #![allow(clippy::bool_assert_comparison)]
566    #![allow(clippy::clone_on_copy)]
567    #![allow(clippy::dbg_macro)]
568    #![allow(clippy::mixed_attributes_style)]
569    #![allow(clippy::print_stderr)]
570    #![allow(clippy::print_stdout)]
571    #![allow(clippy::single_char_pattern)]
572    #![allow(clippy::unwrap_used)]
573    #![allow(clippy::unchecked_duration_subtraction)]
574    #![allow(clippy::useless_vec)]
575    #![allow(clippy::needless_pass_by_value)]
576    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
577    use super::*;
578
579    #[test]
580    fn remove() -> Result<()> {
581        let example = DiffResult::from_str("1\n2\n3\n4\n5\n6\n7\n8\n9\n", [0; 32]);
582
583        let mut d = example.clone();
584        d.remove_lines(5, 7)?;
585        assert_eq!(d.to_string(), "1\n2\n3\n4\n8\n9\n");
586
587        let mut d = example.clone();
588        d.remove_lines(1, 9)?;
589        assert_eq!(d.to_string(), "");
590
591        let mut d = example.clone();
592        d.remove_lines(1, 1)?;
593        assert_eq!(d.to_string(), "2\n3\n4\n5\n6\n7\n8\n9\n");
594
595        let mut d = example.clone();
596        d.remove_lines(6, 9)?;
597        assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n");
598
599        let mut d = example.clone();
600        assert!(d.remove_lines(6, 10).is_err());
601        assert!(d.remove_lines(0, 1).is_err());
602        assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n9\n");
603
604        Ok(())
605    }
606
607    #[test]
608    fn insert() -> Result<()> {
609        let example = DiffResult::from_str("1\n2\n3\n4\n5\n", [0; 32]);
610        let mut d = example.clone();
611        d.insert_at(3, &["hello", "world"])?;
612        assert_eq!(d.to_string(), "1\n2\nhello\nworld\n3\n4\n5\n");
613
614        let mut d = example.clone();
615        d.insert_at(6, &["hello", "world"])?;
616        assert_eq!(d.to_string(), "1\n2\n3\n4\n5\nhello\nworld\n");
617
618        let mut d = example.clone();
619        assert!(d.insert_at(0, &["hello", "world"]).is_err());
620        assert!(d.insert_at(7, &["hello", "world"]).is_err());
621        Ok(())
622    }
623
624    #[test]
625    fn push_reversed() {
626        let mut d = DiffResult::new([0; 32]);
627        d.push_reversed(&["7", "8", "9"]);
628        assert_eq!(d.to_string(), "9\n8\n7\n");
629        d.push_reversed(&["world", "hello", ""]);
630        assert_eq!(d.to_string(), "9\n8\n7\n\nhello\nworld\n");
631    }
632
633    #[test]
634    fn apply_command_simple() {
635        let example = DiffResult::from_str("a\nb\nc\nd\ne\nf\n", [0; 32]);
636
637        let mut d = example.clone();
638        assert_eq!(d.to_string(), "a\nb\nc\nd\ne\nf\n".to_string());
639        assert!(DiffCommand::DeleteToEnd { low: 5 }.apply_to(&mut d).is_ok());
640        assert_eq!(d.to_string(), "a\nb\nc\nd\n".to_string());
641
642        let mut d = example.clone();
643        assert!(DiffCommand::Delete { low: 3, high: 5 }
644            .apply_to(&mut d)
645            .is_ok());
646        assert_eq!(d.to_string(), "a\nb\nf\n".to_string());
647
648        let mut d = example.clone();
649        assert!(DiffCommand::Replace {
650            low: 3,
651            high: 5,
652            lines: vec!["hello", "world"]
653        }
654        .apply_to(&mut d)
655        .is_ok());
656        assert_eq!(d.to_string(), "a\nb\nhello\nworld\nf\n".to_string());
657
658        let mut d = example.clone();
659        assert!(DiffCommand::Insert {
660            pos: 3,
661            lines: vec!["hello", "world"]
662        }
663        .apply_to(&mut d)
664        .is_ok());
665        assert_eq!(
666            d.to_string(),
667            "a\nb\nc\nhello\nworld\nd\ne\nf\n".to_string()
668        );
669    }
670
671    #[test]
672    fn parse_command() -> Result<()> {
673        fn parse(s: &str) -> Result<DiffCommand<'_>> {
674            let mut iter = s.lines();
675            let cmd = DiffCommand::from_line_iterator(&mut iter)?;
676            let cmd2 = DiffCommand::from_line_iterator(&mut iter)?;
677            if cmd2.is_some() {
678                panic!("Unexpected second command");
679            }
680            Ok(cmd.unwrap())
681        }
682
683        fn parse_err(s: &str) {
684            let mut iter = s.lines();
685            let cmd = DiffCommand::from_line_iterator(&mut iter);
686            assert!(matches!(cmd, Err(Error::BadDiff(_))));
687        }
688
689        let p = parse("3,8d\n")?;
690        assert!(matches!(p, DiffCommand::Delete { low: 3, high: 8 }));
691        let p = parse("3d\n")?;
692        assert!(matches!(p, DiffCommand::Delete { low: 3, high: 3 }));
693        let p = parse("100,$d\n")?;
694        assert!(matches!(p, DiffCommand::DeleteToEnd { low: 100 }));
695
696        let p = parse("30,40c\nHello\nWorld\n.\n")?;
697        assert!(matches!(
698            p,
699            DiffCommand::Replace {
700                low: 30,
701                high: 40,
702                ..
703            }
704        ));
705        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
706        let p = parse("30c\nHello\nWorld\n.\n")?;
707        assert!(matches!(
708            p,
709            DiffCommand::Replace {
710                low: 30,
711                high: 30,
712                ..
713            }
714        ));
715        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
716
717        let p = parse("999a\nHello\nWorld\n.\n")?;
718        assert!(matches!(p, DiffCommand::Insert { pos: 999, .. }));
719        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
720        let p = parse("0a\nHello\nWorld\n.\n")?;
721        assert!(matches!(p, DiffCommand::Insert { pos: 0, .. }));
722        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
723
724        parse_err("hello world");
725        parse_err("\n\n");
726        parse_err("$,5d");
727        parse_err("5,6,8d");
728        parse_err("8,5d");
729        parse_err("6");
730        parse_err("d");
731        parse_err("-10d");
732        parse_err("4,$c\na\n.");
733        parse_err("foo");
734        parse_err("5,10p");
735        parse_err("18446744073709551615a");
736        parse_err("1,18446744073709551615d");
737
738        Ok(())
739    }
740
741    #[test]
742    fn apply_transformation() -> Result<()> {
743        let example = DiffResult::from_str("1\n2\n3\n4\n5\n6\n7\n8\n9\n", [0; 32]);
744        let empty = DiffResult::new([1; 32]);
745
746        let mut inp = example.clone();
747        let mut out = empty.clone();
748        DiffCommand::DeleteToEnd { low: 5 }.apply_transformation(&mut inp, &mut out)?;
749        assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
750        assert_eq!(out.to_string(), "");
751
752        let mut inp = example.clone();
753        let mut out = empty.clone();
754        DiffCommand::DeleteToEnd { low: 9 }.apply_transformation(&mut inp, &mut out)?;
755        assert_eq!(inp.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n");
756        assert_eq!(out.to_string(), "");
757
758        let mut inp = example.clone();
759        let mut out = empty.clone();
760        DiffCommand::Delete { low: 3, high: 5 }.apply_transformation(&mut inp, &mut out)?;
761        assert_eq!(inp.to_string(), "1\n2\n");
762        assert_eq!(out.to_string(), "9\n8\n7\n6\n");
763
764        let mut inp = example.clone();
765        let mut out = empty.clone();
766        DiffCommand::Replace {
767            low: 5,
768            high: 6,
769            lines: vec!["oh hey", "there"],
770        }
771        .apply_transformation(&mut inp, &mut out)?;
772        assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
773        assert_eq!(out.to_string(), "9\n8\n7\nthere\noh hey\n");
774
775        let mut inp = example.clone();
776        let mut out = empty.clone();
777        DiffCommand::Insert {
778            pos: 3,
779            lines: vec!["oh hey", "there"],
780        }
781        .apply_transformation(&mut inp, &mut out)?;
782        assert_eq!(inp.to_string(), "1\n2\n3\n");
783        assert_eq!(out.to_string(), "9\n8\n7\n6\n5\n4\nthere\noh hey\n");
784        DiffCommand::Insert {
785            pos: 0,
786            lines: vec!["boom!"],
787        }
788        .apply_transformation(&mut inp, &mut out)?;
789        assert_eq!(inp.to_string(), "");
790        assert_eq!(
791            out.to_string(),
792            "9\n8\n7\n6\n5\n4\nthere\noh hey\n3\n2\n1\nboom!\n"
793        );
794
795        let mut inp = example.clone();
796        let mut out = empty.clone();
797        let r = DiffCommand::Delete {
798            low: 100,
799            high: 200,
800        }
801        .apply_transformation(&mut inp, &mut out);
802        assert!(r.is_err());
803        let r = DiffCommand::Delete { low: 5, high: 200 }.apply_transformation(&mut inp, &mut out);
804        assert!(r.is_err());
805        let r = DiffCommand::Delete { low: 0, high: 1 }.apply_transformation(&mut inp, &mut out);
806        assert!(r.is_err());
807        let r = DiffCommand::DeleteToEnd { low: 10 }.apply_transformation(&mut inp, &mut out);
808        assert!(r.is_err());
809        Ok(())
810    }
811
812    #[test]
813    fn header() -> Result<()> {
814        fn header_from(s: &str) -> Result<([u8; 32], [u8; 32])> {
815            let mut iter = s.lines();
816            parse_diff_header(&mut iter)
817        }
818
819        let (a,b) = header_from(
820            "network-status-diff-version 1
821hash B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663 F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB"
822        )?;
823
824        assert_eq!(
825            &a[..],
826            hex::decode("B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663")?
827        );
828        assert_eq!(
829            &b[..],
830            hex::decode("F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB")?
831        );
832
833        assert!(header_from("network-status-diff-version 2\n").is_err());
834        assert!(header_from("").is_err());
835        assert!(header_from("5,$d\n1,2d\n").is_err());
836        assert!(header_from("network-status-diff-version 1\n").is_err());
837        assert!(header_from(
838            "network-status-diff-version 1
839hash x y
8405,5d"
841        )
842        .is_err());
843        assert!(header_from(
844            "network-status-diff-version 1
845hash x y
8465,5d"
847        )
848        .is_err());
849        assert!(header_from(
850            "network-status-diff-version 1
851hash AA BB
8525,5d"
853        )
854        .is_err());
855        assert!(header_from(
856            "network-status-diff-version 1
857oh hello there
8585,5d"
859        )
860        .is_err());
861        assert!(header_from("network-status-diff-version 1
862hash B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663 F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB extra").is_err());
863
864        Ok(())
865    }
866
867    #[test]
868    fn apply_simple() {
869        let pre = include_str!("../testdata/consensus1.txt");
870        let diff = include_str!("../testdata/diff1.txt");
871        let post = include_str!("../testdata/consensus2.txt");
872
873        let result = apply_diff_trivial(pre, diff).unwrap();
874        assert!(result.check_digest().is_ok());
875        assert_eq!(result.to_string(), post);
876    }
877
878    #[test]
879    fn sort_order() -> Result<()> {
880        fn cmds(s: &str) -> Result<Vec<DiffCommand<'_>>> {
881            let mut out = Vec::new();
882            for cmd in DiffCommandIter::new(s.lines()) {
883                out.push(cmd?);
884            }
885            Ok(out)
886        }
887
888        let _ = cmds("6,9d\n5,5d\n")?;
889        assert!(cmds("5,5d\n6,9d\n").is_err());
890        assert!(cmds("5,5d\n6,6d\n").is_err());
891        assert!(cmds("5,5d\n5,6d\n").is_err());
892
893        Ok(())
894    }
895}