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)] use std::fmt::{Display, Formatter};
47use std::num::NonZeroUsize;
48use std::str::FromStr;
49
50mod err;
51pub use err::Error;
52
53type Result<T> = std::result::Result<T, Error>;
55
56pub fn looks_like_diff(s: &str) -> bool {
59 s.starts_with("network-status-diff-version")
60}
61
62#[cfg(any(test, feature = "slow-diff-apply"))]
68pub fn apply_diff_trivial<'a>(input: &'a str, diff: &'a str) -> Result<DiffResult<'a>> {
69 let mut diff_lines = diff.lines();
70 let (_, d2) = parse_diff_header(&mut diff_lines)?;
71
72 let mut diffable = DiffResult::from_str(input, d2);
73
74 for command in DiffCommandIter::new(diff_lines) {
75 command?.apply_to(&mut diffable)?;
76 }
77
78 Ok(diffable)
79}
80
81pub fn apply_diff<'a>(
87 input: &'a str,
88 diff: &'a str,
89 check_digest_in: Option<[u8; 32]>,
90) -> Result<DiffResult<'a>> {
91 let mut input = DiffResult::from_str(input, [0; 32]);
92
93 let mut diff_lines = diff.lines();
94 let (d1, d2) = parse_diff_header(&mut diff_lines)?;
95 if let Some(d_want) = check_digest_in {
96 if d1 != d_want {
97 return Err(Error::CantApply("listed digest does not match document"));
98 }
99 }
100
101 let mut output = DiffResult::new(d2);
102
103 for command in DiffCommandIter::new(diff_lines) {
104 command?.apply_transformation(&mut input, &mut output)?;
105 }
106
107 output.push_reversed(&input.lines[..]);
108
109 output.lines.reverse();
110 Ok(output)
111}
112
113fn parse_diff_header<'a, I>(iter: &mut I) -> Result<([u8; 32], [u8; 32])>
116where
117 I: Iterator<Item = &'a str>,
118{
119 let line1 = iter.next();
120 if line1 != Some("network-status-diff-version 1") {
121 return Err(Error::BadDiff("unrecognized or missing header"));
122 }
123 let line2 = iter.next().ok_or(Error::BadDiff("header truncated"))?;
124 if !line2.starts_with("hash ") {
125 return Err(Error::BadDiff("missing 'hash' line"));
126 }
127 let elts: Vec<_> = line2.split_ascii_whitespace().collect();
128 if elts.len() != 3 {
129 return Err(Error::BadDiff("invalid 'hash' line"));
130 }
131 let d1 = hex::decode(elts[1])?;
132 let d2 = hex::decode(elts[2])?;
133 match (d1.try_into(), d2.try_into()) {
134 (Ok(a), Ok(b)) => Ok((a, b)),
135 _ => Err(Error::BadDiff("wrong digest lengths on 'hash' line")),
136 }
137}
138
139#[derive(Clone, Debug)]
145enum DiffCommand<'a> {
146 Delete {
148 low: usize,
150 high: usize,
152 },
153 DeleteToEnd {
155 low: usize,
157 },
158 Replace {
161 low: usize,
163 high: usize,
165 lines: Vec<&'a str>,
167 },
168 Insert {
170 pos: usize,
172 lines: Vec<&'a str>,
174 },
175}
176
177#[derive(Clone, Debug)]
182pub struct DiffResult<'a> {
183 d_post: [u8; 32],
185 lines: Vec<&'a str>,
187}
188
189#[derive(Clone, Copy, Debug)]
192enum RangeEnd {
193 Num(NonZeroUsize),
195 DollarSign,
197}
198
199impl FromStr for RangeEnd {
200 type Err = Error;
201 fn from_str(s: &str) -> Result<RangeEnd> {
202 if s == "$" {
203 Ok(RangeEnd::DollarSign)
204 } else {
205 let v: NonZeroUsize = s.parse()?;
206 if v.get() == usize::MAX {
207 return Err(Error::BadDiff("range cannot end at usize::MAX"));
208 }
209 Ok(RangeEnd::Num(v))
210 }
211 }
212}
213
214impl<'a> DiffCommand<'a> {
215 #[cfg(any(test, feature = "slow-diff-apply"))]
220 fn apply_to(&self, target: &mut DiffResult<'a>) -> Result<()> {
221 match self {
222 Self::Delete { low, high } => {
223 target.remove_lines(*low, *high)?;
224 }
225 Self::DeleteToEnd { low } => {
226 target.remove_lines(*low, target.lines.len())?;
227 }
228 Self::Replace { low, high, lines } => {
229 target.remove_lines(*low, *high)?;
230 target.insert_at(*low, lines)?;
231 }
232 Self::Insert { pos, lines } => {
233 target.insert_at(*pos + 1, lines)?;
236 }
237 };
238 Ok(())
239 }
240
241 fn apply_transformation(
260 &self,
261 input: &mut DiffResult<'a>,
262 output: &mut DiffResult<'a>,
263 ) -> Result<()> {
264 if let Some(succ) = self.following_lines() {
265 if let Some(subslice) = input.lines.get(succ - 1..) {
266 output.push_reversed(subslice);
268 } else {
269 return Err(Error::CantApply(
271 "ending line number didn't correspond to document",
272 ));
273 }
274 }
275
276 if let Some(lines) = self.lines() {
277 output.push_reversed(lines);
279 }
280
281 let remove = self.first_removed_line();
282 if remove == 0 || (!self.is_insert() && remove > input.lines.len()) {
283 return Err(Error::CantApply(
284 "starting line number didn't correspond to document",
285 ));
286 }
287 input.lines.truncate(remove - 1);
288
289 Ok(())
290 }
291
292 fn lines(&self) -> Option<&[&'a str]> {
294 match self {
295 Self::Replace { lines, .. } | Self::Insert { lines, .. } => Some(lines.as_slice()),
296 _ => None,
297 }
298 }
299
300 fn linebuf_mut(&mut self) -> Option<&mut Vec<&'a str>> {
303 match self {
304 Self::Replace { ref mut lines, .. } | Self::Insert { ref mut lines, .. } => Some(lines),
305 _ => None,
306 }
307 }
308
309 fn following_lines(&self) -> Option<usize> {
314 match self {
315 Self::Delete { high, .. } | Self::Replace { high, .. } => Some(high + 1),
316 Self::DeleteToEnd { .. } => None,
317 Self::Insert { pos, .. } => Some(pos + 1),
318 }
319 }
320
321 fn first_removed_line(&self) -> usize {
327 match self {
328 Self::Delete { low, .. } => *low,
329 Self::DeleteToEnd { low } => *low,
330 Self::Replace { low, .. } => *low,
331 Self::Insert { pos, .. } => *pos + 1,
332 }
333 }
334
335 fn is_insert(&self) -> bool {
337 matches!(self, Self::Insert { .. })
338 }
339
340 fn from_line_iterator<I>(iter: &mut I) -> Result<Option<Self>>
343 where
344 I: Iterator<Item = &'a str>,
345 {
346 let command = match iter.next() {
347 Some(s) => s,
348 None => return Ok(None),
349 };
350
351 if command.len() < 2 || !command.is_ascii() {
355 return Err(Error::BadDiff("command too short"));
356 }
357
358 let (range, command) = command.split_at(command.len() - 1);
359 let (low, high) = if let Some(comma_pos) = range.find(',') {
360 (
361 range[..comma_pos].parse::<usize>()?,
362 Some(range[comma_pos + 1..].parse::<RangeEnd>()?),
363 )
364 } else {
365 (range.parse::<usize>()?, None)
366 };
367
368 if low == usize::MAX {
369 return Err(Error::BadDiff("range cannot begin at usize::MAX"));
370 }
371
372 match (low, high) {
373 (lo, Some(RangeEnd::Num(hi))) if lo > hi.into() => {
374 return Err(Error::BadDiff("mis-ordered lines in range"))
375 }
376 (_, _) => (),
377 }
378
379 let mut cmd = match (command, low, high) {
380 ("d", low, None) => Self::Delete { low, high: low },
381 ("d", low, Some(RangeEnd::Num(high))) => Self::Delete {
382 low,
383 high: high.into(),
384 },
385 ("d", low, Some(RangeEnd::DollarSign)) => Self::DeleteToEnd { low },
386 ("c", low, None) => Self::Replace {
387 low,
388 high: low,
389 lines: Vec::new(),
390 },
391 ("c", low, Some(RangeEnd::Num(high))) => Self::Replace {
392 low,
393 high: high.into(),
394 lines: Vec::new(),
395 },
396 ("a", low, None) => Self::Insert {
397 pos: low,
398 lines: Vec::new(),
399 },
400 (_, _, _) => return Err(Error::BadDiff("can't parse command line")),
401 };
402
403 if let Some(ref mut linebuf) = cmd.linebuf_mut() {
404 loop {
407 match iter.next() {
408 None => return Err(Error::BadDiff("unterminated block to insert")),
409 Some(".") => break,
410 Some(line) => linebuf.push(line),
411 }
412 }
413 }
414
415 Ok(Some(cmd))
416 }
417}
418
419struct DiffCommandIter<'a, I>
425where
426 I: Iterator<Item = &'a str>,
427{
428 iter: I,
430
431 last_cmd_first_removed: Option<usize>,
434}
435
436impl<'a, I> DiffCommandIter<'a, I>
437where
438 I: Iterator<Item = &'a str>,
439{
440 fn new(iter: I) -> Self {
442 DiffCommandIter {
443 iter,
444 last_cmd_first_removed: None,
445 }
446 }
447}
448
449impl<'a, I> Iterator for DiffCommandIter<'a, I>
450where
451 I: Iterator<Item = &'a str>,
452{
453 type Item = Result<DiffCommand<'a>>;
454 fn next(&mut self) -> Option<Result<DiffCommand<'a>>> {
455 match DiffCommand::from_line_iterator(&mut self.iter) {
456 Err(e) => Some(Err(e)),
457 Ok(None) => None,
458 Ok(Some(c)) => match (self.last_cmd_first_removed, c.following_lines()) {
459 (Some(_), None) => Some(Err(Error::BadDiff("misordered commands"))),
460 (Some(a), Some(b)) if a < b => Some(Err(Error::BadDiff("misordered commands"))),
461 (_, _) => {
462 self.last_cmd_first_removed = Some(c.first_removed_line());
463 Some(Ok(c))
464 }
465 },
466 }
467 }
468}
469
470impl<'a> DiffResult<'a> {
471 fn from_str(s: &'a str, d_post: [u8; 32]) -> Self {
474 let lines: Vec<_> = s.lines().collect();
478
479 DiffResult { d_post, lines }
480 }
481
482 fn new(d_post: [u8; 32]) -> Self {
485 DiffResult {
486 d_post,
487 lines: Vec::new(),
488 }
489 }
490
491 fn push_reversed(&mut self, lines: &[&'a str]) {
494 self.lines.extend(lines.iter().rev());
495 }
496
497 #[cfg(any(test, feature = "slow-diff-apply"))]
502 fn remove_lines(&mut self, first: usize, last: usize) -> Result<()> {
503 if first > self.lines.len() || last > self.lines.len() || first == 0 || last == 0 {
504 Err(Error::CantApply("line out of range"))
505 } else {
506 let n_to_remove = last - first + 1;
507 if last != self.lines.len() {
508 self.lines[..].copy_within((last).., first - 1);
509 }
510 self.lines.truncate(self.lines.len() - n_to_remove);
511 Ok(())
512 }
513 }
514
515 #[cfg(any(test, feature = "slow-diff-apply"))]
521 fn insert_at(&mut self, pos: usize, lines: &[&'a str]) -> Result<()> {
522 if pos > self.lines.len() + 1 || pos == 0 {
523 Err(Error::CantApply("position out of range"))
524 } else {
525 let orig_len = self.lines.len();
526 self.lines.resize(self.lines.len() + lines.len(), "");
527 self.lines
528 .copy_within(pos - 1..orig_len, pos - 1 + lines.len());
529 self.lines[(pos - 1)..(pos + lines.len() - 1)].copy_from_slice(lines);
530 Ok(())
531 }
532 }
533
534 pub fn check_digest(&self) -> Result<()> {
538 use digest::Digest;
539 use tor_llcrypto::d::Sha3_256;
540 let mut d = Sha3_256::new();
541 for line in &self.lines {
542 d.update(line.as_bytes());
543 d.update(b"\n");
544 }
545 if d.finalize() == self.d_post.into() {
546 Ok(())
547 } else {
548 Err(Error::CantApply("Wrong digest after applying diff"))
549 }
550 }
551}
552
553impl<'a> Display for DiffResult<'a> {
554 fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
555 for elt in &self.lines {
556 writeln!(f, "{}", elt)?;
557 }
558 Ok(())
559 }
560}
561
562#[cfg(test)]
563mod test {
564 #![allow(clippy::bool_assert_comparison)]
566 #![allow(clippy::clone_on_copy)]
567 #![allow(clippy::dbg_macro)]
568 #![allow(clippy::mixed_attributes_style)]
569 #![allow(clippy::print_stderr)]
570 #![allow(clippy::print_stdout)]
571 #![allow(clippy::single_char_pattern)]
572 #![allow(clippy::unwrap_used)]
573 #![allow(clippy::unchecked_duration_subtraction)]
574 #![allow(clippy::useless_vec)]
575 #![allow(clippy::needless_pass_by_value)]
576 use super::*;
578
579 #[test]
580 fn remove() -> Result<()> {
581 let example = DiffResult::from_str("1\n2\n3\n4\n5\n6\n7\n8\n9\n", [0; 32]);
582
583 let mut d = example.clone();
584 d.remove_lines(5, 7)?;
585 assert_eq!(d.to_string(), "1\n2\n3\n4\n8\n9\n");
586
587 let mut d = example.clone();
588 d.remove_lines(1, 9)?;
589 assert_eq!(d.to_string(), "");
590
591 let mut d = example.clone();
592 d.remove_lines(1, 1)?;
593 assert_eq!(d.to_string(), "2\n3\n4\n5\n6\n7\n8\n9\n");
594
595 let mut d = example.clone();
596 d.remove_lines(6, 9)?;
597 assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n");
598
599 let mut d = example.clone();
600 assert!(d.remove_lines(6, 10).is_err());
601 assert!(d.remove_lines(0, 1).is_err());
602 assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n9\n");
603
604 Ok(())
605 }
606
607 #[test]
608 fn insert() -> Result<()> {
609 let example = DiffResult::from_str("1\n2\n3\n4\n5\n", [0; 32]);
610 let mut d = example.clone();
611 d.insert_at(3, &["hello", "world"])?;
612 assert_eq!(d.to_string(), "1\n2\nhello\nworld\n3\n4\n5\n");
613
614 let mut d = example.clone();
615 d.insert_at(6, &["hello", "world"])?;
616 assert_eq!(d.to_string(), "1\n2\n3\n4\n5\nhello\nworld\n");
617
618 let mut d = example.clone();
619 assert!(d.insert_at(0, &["hello", "world"]).is_err());
620 assert!(d.insert_at(7, &["hello", "world"]).is_err());
621 Ok(())
622 }
623
624 #[test]
625 fn push_reversed() {
626 let mut d = DiffResult::new([0; 32]);
627 d.push_reversed(&["7", "8", "9"]);
628 assert_eq!(d.to_string(), "9\n8\n7\n");
629 d.push_reversed(&["world", "hello", ""]);
630 assert_eq!(d.to_string(), "9\n8\n7\n\nhello\nworld\n");
631 }
632
633 #[test]
634 fn apply_command_simple() {
635 let example = DiffResult::from_str("a\nb\nc\nd\ne\nf\n", [0; 32]);
636
637 let mut d = example.clone();
638 assert_eq!(d.to_string(), "a\nb\nc\nd\ne\nf\n".to_string());
639 assert!(DiffCommand::DeleteToEnd { low: 5 }.apply_to(&mut d).is_ok());
640 assert_eq!(d.to_string(), "a\nb\nc\nd\n".to_string());
641
642 let mut d = example.clone();
643 assert!(DiffCommand::Delete { low: 3, high: 5 }
644 .apply_to(&mut d)
645 .is_ok());
646 assert_eq!(d.to_string(), "a\nb\nf\n".to_string());
647
648 let mut d = example.clone();
649 assert!(DiffCommand::Replace {
650 low: 3,
651 high: 5,
652 lines: vec!["hello", "world"]
653 }
654 .apply_to(&mut d)
655 .is_ok());
656 assert_eq!(d.to_string(), "a\nb\nhello\nworld\nf\n".to_string());
657
658 let mut d = example.clone();
659 assert!(DiffCommand::Insert {
660 pos: 3,
661 lines: vec!["hello", "world"]
662 }
663 .apply_to(&mut d)
664 .is_ok());
665 assert_eq!(
666 d.to_string(),
667 "a\nb\nc\nhello\nworld\nd\ne\nf\n".to_string()
668 );
669 }
670
671 #[test]
672 fn parse_command() -> Result<()> {
673 fn parse(s: &str) -> Result<DiffCommand<'_>> {
674 let mut iter = s.lines();
675 let cmd = DiffCommand::from_line_iterator(&mut iter)?;
676 let cmd2 = DiffCommand::from_line_iterator(&mut iter)?;
677 if cmd2.is_some() {
678 panic!("Unexpected second command");
679 }
680 Ok(cmd.unwrap())
681 }
682
683 fn parse_err(s: &str) {
684 let mut iter = s.lines();
685 let cmd = DiffCommand::from_line_iterator(&mut iter);
686 assert!(matches!(cmd, Err(Error::BadDiff(_))));
687 }
688
689 let p = parse("3,8d\n")?;
690 assert!(matches!(p, DiffCommand::Delete { low: 3, high: 8 }));
691 let p = parse("3d\n")?;
692 assert!(matches!(p, DiffCommand::Delete { low: 3, high: 3 }));
693 let p = parse("100,$d\n")?;
694 assert!(matches!(p, DiffCommand::DeleteToEnd { low: 100 }));
695
696 let p = parse("30,40c\nHello\nWorld\n.\n")?;
697 assert!(matches!(
698 p,
699 DiffCommand::Replace {
700 low: 30,
701 high: 40,
702 ..
703 }
704 ));
705 assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
706 let p = parse("30c\nHello\nWorld\n.\n")?;
707 assert!(matches!(
708 p,
709 DiffCommand::Replace {
710 low: 30,
711 high: 30,
712 ..
713 }
714 ));
715 assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
716
717 let p = parse("999a\nHello\nWorld\n.\n")?;
718 assert!(matches!(p, DiffCommand::Insert { pos: 999, .. }));
719 assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
720 let p = parse("0a\nHello\nWorld\n.\n")?;
721 assert!(matches!(p, DiffCommand::Insert { pos: 0, .. }));
722 assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
723
724 parse_err("hello world");
725 parse_err("\n\n");
726 parse_err("$,5d");
727 parse_err("5,6,8d");
728 parse_err("8,5d");
729 parse_err("6");
730 parse_err("d");
731 parse_err("-10d");
732 parse_err("4,$c\na\n.");
733 parse_err("foo");
734 parse_err("5,10p");
735 parse_err("18446744073709551615a");
736 parse_err("1,18446744073709551615d");
737
738 Ok(())
739 }
740
741 #[test]
742 fn apply_transformation() -> Result<()> {
743 let example = DiffResult::from_str("1\n2\n3\n4\n5\n6\n7\n8\n9\n", [0; 32]);
744 let empty = DiffResult::new([1; 32]);
745
746 let mut inp = example.clone();
747 let mut out = empty.clone();
748 DiffCommand::DeleteToEnd { low: 5 }.apply_transformation(&mut inp, &mut out)?;
749 assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
750 assert_eq!(out.to_string(), "");
751
752 let mut inp = example.clone();
753 let mut out = empty.clone();
754 DiffCommand::DeleteToEnd { low: 9 }.apply_transformation(&mut inp, &mut out)?;
755 assert_eq!(inp.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n");
756 assert_eq!(out.to_string(), "");
757
758 let mut inp = example.clone();
759 let mut out = empty.clone();
760 DiffCommand::Delete { low: 3, high: 5 }.apply_transformation(&mut inp, &mut out)?;
761 assert_eq!(inp.to_string(), "1\n2\n");
762 assert_eq!(out.to_string(), "9\n8\n7\n6\n");
763
764 let mut inp = example.clone();
765 let mut out = empty.clone();
766 DiffCommand::Replace {
767 low: 5,
768 high: 6,
769 lines: vec!["oh hey", "there"],
770 }
771 .apply_transformation(&mut inp, &mut out)?;
772 assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
773 assert_eq!(out.to_string(), "9\n8\n7\nthere\noh hey\n");
774
775 let mut inp = example.clone();
776 let mut out = empty.clone();
777 DiffCommand::Insert {
778 pos: 3,
779 lines: vec!["oh hey", "there"],
780 }
781 .apply_transformation(&mut inp, &mut out)?;
782 assert_eq!(inp.to_string(), "1\n2\n3\n");
783 assert_eq!(out.to_string(), "9\n8\n7\n6\n5\n4\nthere\noh hey\n");
784 DiffCommand::Insert {
785 pos: 0,
786 lines: vec!["boom!"],
787 }
788 .apply_transformation(&mut inp, &mut out)?;
789 assert_eq!(inp.to_string(), "");
790 assert_eq!(
791 out.to_string(),
792 "9\n8\n7\n6\n5\n4\nthere\noh hey\n3\n2\n1\nboom!\n"
793 );
794
795 let mut inp = example.clone();
796 let mut out = empty.clone();
797 let r = DiffCommand::Delete {
798 low: 100,
799 high: 200,
800 }
801 .apply_transformation(&mut inp, &mut out);
802 assert!(r.is_err());
803 let r = DiffCommand::Delete { low: 5, high: 200 }.apply_transformation(&mut inp, &mut out);
804 assert!(r.is_err());
805 let r = DiffCommand::Delete { low: 0, high: 1 }.apply_transformation(&mut inp, &mut out);
806 assert!(r.is_err());
807 let r = DiffCommand::DeleteToEnd { low: 10 }.apply_transformation(&mut inp, &mut out);
808 assert!(r.is_err());
809 Ok(())
810 }
811
812 #[test]
813 fn header() -> Result<()> {
814 fn header_from(s: &str) -> Result<([u8; 32], [u8; 32])> {
815 let mut iter = s.lines();
816 parse_diff_header(&mut iter)
817 }
818
819 let (a,b) = header_from(
820 "network-status-diff-version 1
821hash B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663 F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB"
822 )?;
823
824 assert_eq!(
825 &a[..],
826 hex::decode("B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663")?
827 );
828 assert_eq!(
829 &b[..],
830 hex::decode("F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB")?
831 );
832
833 assert!(header_from("network-status-diff-version 2\n").is_err());
834 assert!(header_from("").is_err());
835 assert!(header_from("5,$d\n1,2d\n").is_err());
836 assert!(header_from("network-status-diff-version 1\n").is_err());
837 assert!(header_from(
838 "network-status-diff-version 1
839hash x y
8405,5d"
841 )
842 .is_err());
843 assert!(header_from(
844 "network-status-diff-version 1
845hash x y
8465,5d"
847 )
848 .is_err());
849 assert!(header_from(
850 "network-status-diff-version 1
851hash AA BB
8525,5d"
853 )
854 .is_err());
855 assert!(header_from(
856 "network-status-diff-version 1
857oh hello there
8585,5d"
859 )
860 .is_err());
861 assert!(header_from("network-status-diff-version 1
862hash B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663 F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB extra").is_err());
863
864 Ok(())
865 }
866
867 #[test]
868 fn apply_simple() {
869 let pre = include_str!("../testdata/consensus1.txt");
870 let diff = include_str!("../testdata/diff1.txt");
871 let post = include_str!("../testdata/consensus2.txt");
872
873 let result = apply_diff_trivial(pre, diff).unwrap();
874 assert!(result.check_digest().is_ok());
875 assert_eq!(result.to_string(), post);
876 }
877
878 #[test]
879 fn sort_order() -> Result<()> {
880 fn cmds(s: &str) -> Result<Vec<DiffCommand<'_>>> {
881 let mut out = Vec::new();
882 for cmd in DiffCommandIter::new(s.lines()) {
883 out.push(cmd?);
884 }
885 Ok(out)
886 }
887
888 let _ = cmds("6,9d\n5,5d\n")?;
889 assert!(cmds("5,5d\n6,9d\n").is_err());
890 assert!(cmds("5,5d\n6,6d\n").is_err());
891 assert!(cmds("5,5d\n5,6d\n").is_err());
892
893 Ok(())
894 }
895}