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