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