1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![allow(renamed_and_removed_lints)] #![allow(unknown_lints)] #![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)] #![allow(clippy::uninlined_format_args)]
40#![allow(clippy::significant_drop_in_scrutinee)] #![allow(clippy::result_large_err)] #![allow(clippy::needless_raw_string_hashes)] #![allow(clippy::needless_lifetimes)] #![allow(mismatched_lifetime_syntaxes)] #![allow(clippy::collapsible_if)] #![deny(clippy::unused_async)]
47use std::fmt::{Display, Formatter};
50use std::num::NonZeroUsize;
51use std::str::FromStr;
52
53mod err;
54pub use err::Error;
55
56type Result<T> = std::result::Result<T, Error>;
58
59pub fn looks_like_diff(s: &str) -> bool {
62 s.starts_with("network-status-diff-version")
63}
64
65#[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
84pub 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
116fn 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#[derive(Clone, Debug)]
148enum DiffCommand<'a> {
149 Delete {
151 low: usize,
153 high: usize,
155 },
156 DeleteToEnd {
158 low: usize,
160 },
161 Replace {
164 low: usize,
166 high: usize,
168 lines: Vec<&'a str>,
170 },
171 Insert {
173 pos: usize,
175 lines: Vec<&'a str>,
177 },
178}
179
180#[derive(Clone, Debug)]
185pub struct DiffResult<'a> {
186 d_post: [u8; 32],
188 lines: Vec<&'a str>,
190}
191
192#[derive(Clone, Copy, Debug)]
195enum RangeEnd {
196 Num(NonZeroUsize),
198 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 #[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 target.insert_at(*pos + 1, lines)?;
239 }
240 };
241 Ok(())
242 }
243
244 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 output.push_reversed(subslice);
271 } else {
272 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 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 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 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 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 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 fn is_insert(&self) -> bool {
340 matches!(self, Self::Insert { .. })
341 }
342
343 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 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 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
422struct DiffCommandIter<'a, I>
428where
429 I: Iterator<Item = &'a str>,
430{
431 iter: I,
433
434 last_cmd_first_removed: Option<usize>,
437}
438
439impl<'a, I> DiffCommandIter<'a, I>
440where
441 I: Iterator<Item = &'a str>,
442{
443 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 fn from_str(s: &'a str, d_post: [u8; 32]) -> Self {
477 let lines: Vec<_> = s.lines().collect();
481
482 DiffResult { d_post, lines }
483 }
484
485 fn new(d_post: [u8; 32]) -> Self {
488 DiffResult {
489 d_post,
490 lines: Vec::new(),
491 }
492 }
493
494 fn push_reversed(&mut self, lines: &[&'a str]) {
497 self.lines.extend(lines.iter().rev());
498 }
499
500 #[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 #[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 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 #![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 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}