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