Skip to main content

tor_consdiff/
lib.rs

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
49use std::fmt::{Display, Formatter};
50use std::num::NonZeroUsize;
51use std::str::FromStr;
52
53mod err;
54pub use err::Error;
55
56/// Result type used by this crate
57type 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.
61pub fn looks_like_diff(s: &str) -> bool {
62    s.starts_with("network-status-diff-version")
63}
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"))]
71pub fn apply_diff_trivial<'a>(input: &'a str, diff: &'a str) -> Result<DiffResult<'a>> {
72    let mut diff_lines = diff.lines();
73    let (_, d2) = parse_diff_header(&mut diff_lines)?;
74
75    let mut diffable = DiffResult::from_str(input, d2);
76
77    for command in DiffCommandIter::new(diff_lines) {
78        command?.apply_to(&mut diffable)?;
79    }
80
81    Ok(diffable)
82}
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.
89pub fn apply_diff<'a>(
90    input: &'a str,
91    diff: &'a str,
92    check_digest_in: Option<[u8; 32]>,
93) -> Result<DiffResult<'a>> {
94    let mut input = DiffResult::from_str(input, [0; 32]);
95
96    let mut diff_lines = diff.lines();
97    let (d1, d2) = parse_diff_header(&mut diff_lines)?;
98    if let Some(d_want) = check_digest_in {
99        if d1 != d_want {
100            return Err(Error::CantApply("listed digest does not match document"));
101        }
102    }
103
104    let mut output = DiffResult::new(d2);
105
106    for command in DiffCommandIter::new(diff_lines) {
107        command?.apply_transformation(&mut input, &mut output)?;
108    }
109
110    output.push_reversed(&input.lines[..]);
111
112    output.lines.reverse();
113    Ok(output)
114}
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.
118fn parse_diff_header<'a, I>(iter: &mut I) -> Result<([u8; 32], [u8; 32])>
119where
120    I: Iterator<Item = &'a str>,
121{
122    let line1 = iter.next();
123    if line1 != Some("network-status-diff-version 1") {
124        return Err(Error::BadDiff("unrecognized or missing header"));
125    }
126    let line2 = iter.next().ok_or(Error::BadDiff("header truncated"))?;
127    if !line2.starts_with("hash ") {
128        return Err(Error::BadDiff("missing 'hash' line"));
129    }
130    let elts: Vec<_> = line2.split_ascii_whitespace().collect();
131    if elts.len() != 3 {
132        return Err(Error::BadDiff("invalid 'hash' line"));
133    }
134    let d1 = hex::decode(elts[1])?;
135    let d2 = hex::decode(elts[2])?;
136    match (d1.try_into(), d2.try_into()) {
137        (Ok(a), Ok(b)) => Ok((a, b)),
138        _ => Err(Error::BadDiff("wrong digest lengths on 'hash' line")),
139    }
140}
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)]
148enum 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)]
185pub 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)]
195enum 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
202impl FromStr for RangeEnd {
203    type Err = Error;
204    fn from_str(s: &str) -> Result<RangeEnd> {
205        if s == "$" {
206            Ok(RangeEnd::DollarSign)
207        } else {
208            let v: NonZeroUsize = s.parse()?;
209            if v.get() == usize::MAX {
210                return Err(Error::BadDiff("range cannot end at usize::MAX"));
211            }
212            Ok(RangeEnd::Num(v))
213        }
214    }
215}
216
217impl<'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    fn apply_to(&self, target: &mut DiffResult<'a>) -> Result<()> {
224        match self {
225            Self::Delete { low, high } => {
226                target.remove_lines(*low, *high)?;
227            }
228            Self::DeleteToEnd { low } => {
229                target.remove_lines(*low, target.lines.len())?;
230            }
231            Self::Replace { low, high, lines } => {
232                target.remove_lines(*low, *high)?;
233                target.insert_at(*low, lines)?;
234            }
235            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                target.insert_at(*pos + 1, lines)?;
239            }
240        };
241        Ok(())
242    }
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    fn apply_transformation(
263        &self,
264        input: &mut DiffResult<'a>,
265        output: &mut DiffResult<'a>,
266    ) -> Result<()> {
267        if let Some(succ) = self.following_lines() {
268            if let Some(subslice) = input.lines.get(succ - 1..) {
269                // Lines from `succ` onwards are unaffected.  Copy them.
270                output.push_reversed(subslice);
271            } else {
272                // Oops, dubious line number.
273                return Err(Error::CantApply(
274                    "ending line number didn't correspond to document",
275                ));
276            }
277        }
278
279        if let Some(lines) = self.lines() {
280            // These are the lines we're inserting.
281            output.push_reversed(lines);
282        }
283
284        let remove = self.first_removed_line();
285        if remove == 0 || (!self.is_insert() && remove > input.lines.len()) {
286            return Err(Error::CantApply(
287                "starting line number didn't correspond to document",
288            ));
289        }
290        input.lines.truncate(remove - 1);
291
292        Ok(())
293    }
294
295    /// Return the lines that we should add to the output
296    fn lines(&self) -> Option<&[&'a str]> {
297        match self {
298            Self::Replace { lines, .. } | Self::Insert { lines, .. } => Some(lines.as_slice()),
299            _ => None,
300        }
301    }
302
303    /// Return a mutable reference to the vector of lines we should
304    /// add to the output.
305    fn linebuf_mut(&mut self) -> Option<&mut Vec<&'a str>> {
306        match self {
307            Self::Replace { lines, .. } | Self::Insert { lines, .. } => Some(lines),
308            _ => None,
309        }
310    }
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    fn following_lines(&self) -> Option<usize> {
317        match self {
318            Self::Delete { high, .. } | Self::Replace { high, .. } => Some(high + 1),
319            Self::DeleteToEnd { .. } => None,
320            Self::Insert { pos, .. } => Some(pos + 1),
321        }
322    }
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    fn first_removed_line(&self) -> usize {
330        match self {
331            Self::Delete { low, .. } => *low,
332            Self::DeleteToEnd { low } => *low,
333            Self::Replace { low, .. } => *low,
334            Self::Insert { pos, .. } => *pos + 1,
335        }
336    }
337
338    /// Return true if this is an Insert command.
339    fn is_insert(&self) -> bool {
340        matches!(self, Self::Insert { .. })
341    }
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    fn from_line_iterator<I>(iter: &mut I) -> Result<Option<Self>>
346    where
347        I: Iterator<Item = &'a str>,
348    {
349        let command = match iter.next() {
350            Some(s) => s,
351            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        if command.len() < 2 || !command.is_ascii() {
358            return Err(Error::BadDiff("command too short"));
359        }
360
361        let (range, command) = command.split_at(command.len() - 1);
362        let (low, high) = if let Some(comma_pos) = range.find(',') {
363            (
364                range[..comma_pos].parse::<usize>()?,
365                Some(range[comma_pos + 1..].parse::<RangeEnd>()?),
366            )
367        } else {
368            (range.parse::<usize>()?, None)
369        };
370
371        if low == usize::MAX {
372            return Err(Error::BadDiff("range cannot begin at usize::MAX"));
373        }
374
375        match (low, high) {
376            (lo, Some(RangeEnd::Num(hi))) if lo > hi.into() => {
377                return Err(Error::BadDiff("mis-ordered lines in range"));
378            }
379            (_, _) => (),
380        }
381
382        let mut cmd = match (command, low, high) {
383            ("d", low, None) => Self::Delete { low, high: low },
384            ("d", low, Some(RangeEnd::Num(high))) => Self::Delete {
385                low,
386                high: high.into(),
387            },
388            ("d", low, Some(RangeEnd::DollarSign)) => Self::DeleteToEnd { low },
389            ("c", low, None) => Self::Replace {
390                low,
391                high: low,
392                lines: Vec::new(),
393            },
394            ("c", low, Some(RangeEnd::Num(high))) => Self::Replace {
395                low,
396                high: high.into(),
397                lines: Vec::new(),
398            },
399            ("a", low, None) => Self::Insert {
400                pos: low,
401                lines: Vec::new(),
402            },
403            (_, _, _) => return Err(Error::BadDiff("can't parse command line")),
404        };
405
406        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                match iter.next() {
411                    None => return Err(Error::BadDiff("unterminated block to insert")),
412                    Some(".") => break,
413                    Some(line) => linebuf.push(line),
414                }
415            }
416        }
417
418        Ok(Some(cmd))
419    }
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.
427struct DiffCommandIter<'a, I>
428where
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
439impl<'a, I> DiffCommandIter<'a, I>
440where
441    I: Iterator<Item = &'a str>,
442{
443    /// Construct a new DiffCommandIter wrapping `iter`.
444    fn new(iter: I) -> Self {
445        DiffCommandIter {
446            iter,
447            last_cmd_first_removed: None,
448        }
449    }
450}
451
452impl<'a, I> Iterator for DiffCommandIter<'a, I>
453where
454    I: Iterator<Item = &'a str>,
455{
456    type Item = Result<DiffCommand<'a>>;
457    fn next(&mut self) -> Option<Result<DiffCommand<'a>>> {
458        match DiffCommand::from_line_iterator(&mut self.iter) {
459            Err(e) => Some(Err(e)),
460            Ok(None) => None,
461            Ok(Some(c)) => match (self.last_cmd_first_removed, c.following_lines()) {
462                (Some(_), None) => Some(Err(Error::BadDiff("misordered commands"))),
463                (Some(a), Some(b)) if a < b => Some(Err(Error::BadDiff("misordered commands"))),
464                (_, _) => {
465                    self.last_cmd_first_removed = Some(c.first_removed_line());
466                    Some(Ok(c))
467                }
468            },
469        }
470    }
471}
472
473impl<'a> DiffResult<'a> {
474    /// Construct a new DiffResult containing the provided string
475    /// split into lines, and an expected post-transformation digest.
476    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        let lines: Vec<_> = s.lines().collect();
481
482        DiffResult { d_post, lines }
483    }
484
485    /// Return a new empty DiffResult with an expected
486    /// post-transformation digests
487    fn new(d_post: [u8; 32]) -> Self {
488        DiffResult {
489            d_post,
490            lines: Vec::new(),
491        }
492    }
493
494    /// Put every member of `lines` at the end of this DiffResult, in
495    /// reverse order.
496    fn push_reversed(&mut self, lines: &[&'a str]) {
497        self.lines.extend(lines.iter().rev());
498    }
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    fn remove_lines(&mut self, first: usize, last: usize) -> Result<()> {
506        if first > self.lines.len() || last > self.lines.len() || first == 0 || last == 0 {
507            Err(Error::CantApply("line out of range"))
508        } else {
509            let n_to_remove = last - first + 1;
510            if last != self.lines.len() {
511                self.lines[..].copy_within((last).., first - 1);
512            }
513            self.lines.truncate(self.lines.len() - n_to_remove);
514            Ok(())
515        }
516    }
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    fn insert_at(&mut self, pos: usize, lines: &[&'a str]) -> Result<()> {
525        if pos > self.lines.len() + 1 || pos == 0 {
526            Err(Error::CantApply("position out of range"))
527        } else {
528            let orig_len = self.lines.len();
529            self.lines.resize(self.lines.len() + lines.len(), "");
530            self.lines
531                .copy_within(pos - 1..orig_len, pos - 1 + lines.len());
532            self.lines[(pos - 1)..(pos + lines.len() - 1)].copy_from_slice(lines);
533            Ok(())
534        }
535    }
536
537    /// See whether the output of this diff matches the target digest.
538    ///
539    /// If not, return an error.
540    pub fn check_digest(&self) -> Result<()> {
541        use digest::Digest;
542        use tor_llcrypto::d::Sha3_256;
543        let mut d = Sha3_256::new();
544        for line in &self.lines {
545            d.update(line.as_bytes());
546            d.update(b"\n");
547        }
548        if d.finalize() == self.d_post.into() {
549            Ok(())
550        } else {
551            Err(Error::CantApply("Wrong digest after applying diff"))
552        }
553    }
554}
555
556impl<'a> Display for DiffResult<'a> {
557    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
558        for elt in &self.lines {
559            writeln!(f, "{}", elt)?;
560        }
561        Ok(())
562    }
563}
564
565#[cfg(test)]
566mod 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
830hash 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
849hash x y
8505,5d"
851            )
852            .is_err()
853        );
854        assert!(
855            header_from(
856                "network-status-diff-version 1
857hash x y
8585,5d"
859            )
860            .is_err()
861        );
862        assert!(
863            header_from(
864                "network-status-diff-version 1
865hash AA BB
8665,5d"
867            )
868            .is_err()
869        );
870        assert!(
871            header_from(
872                "network-status-diff-version 1
873oh hello there
8745,5d"
875            )
876            .is_err()
877        );
878        assert!(header_from("network-status-diff-version 1
879hash 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}