1#![cfg_attr(docsrs, feature(doc_auto_cfg, 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_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)] #![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)] use std::fmt::{Display, Formatter};
48use std::num::NonZeroUsize;
49use std::str::FromStr;
50
51mod err;
52pub use err::Error;
53
54type Result<T> = std::result::Result<T, Error>;
56
57pub fn looks_like_diff(s: &str) -> bool {
60 s.starts_with("network-status-diff-version")
61}
62
63#[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
82pub 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
114fn 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#[derive(Clone, Debug)]
146enum DiffCommand<'a> {
147 Delete {
149 low: usize,
151 high: usize,
153 },
154 DeleteToEnd {
156 low: usize,
158 },
159 Replace {
162 low: usize,
164 high: usize,
166 lines: Vec<&'a str>,
168 },
169 Insert {
171 pos: usize,
173 lines: Vec<&'a str>,
175 },
176}
177
178#[derive(Clone, Debug)]
183pub struct DiffResult<'a> {
184 d_post: [u8; 32],
186 lines: Vec<&'a str>,
188}
189
190#[derive(Clone, Copy, Debug)]
193enum RangeEnd {
194 Num(NonZeroUsize),
196 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 #[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 target.insert_at(*pos + 1, lines)?;
237 }
238 };
239 Ok(())
240 }
241
242 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 output.push_reversed(subslice);
269 } else {
270 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 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 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 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 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 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 fn is_insert(&self) -> bool {
338 matches!(self, Self::Insert { .. })
339 }
340
341 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 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 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
420struct DiffCommandIter<'a, I>
426where
427 I: Iterator<Item = &'a str>,
428{
429 iter: I,
431
432 last_cmd_first_removed: Option<usize>,
435}
436
437impl<'a, I> DiffCommandIter<'a, I>
438where
439 I: Iterator<Item = &'a str>,
440{
441 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 fn from_str(s: &'a str, d_post: [u8; 32]) -> Self {
475 let lines: Vec<_> = s.lines().collect();
479
480 DiffResult { d_post, lines }
481 }
482
483 fn new(d_post: [u8; 32]) -> Self {
486 DiffResult {
487 d_post,
488 lines: Vec::new(),
489 }
490 }
491
492 fn push_reversed(&mut self, lines: &[&'a str]) {
495 self.lines.extend(lines.iter().rev());
496 }
497
498 #[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 #[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 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 #![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 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}