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, Write};
50use std::num::NonZeroUsize;
51use std::str::FromStr;
52
53mod err;
54use digest::Digest;
55pub use err::Error;
56use imara_diff::{Algorithm, Diff, Hunk, InternedInput};
57use tor_error::internal;
58use tor_netdoc::parse2::{ErrorProblem, ItemStream, KeywordRef, ParseError, ParseInput};
59
60use crate::err::GenEdDiffError;
61
62type Result<T> = std::result::Result<T, Error>;
64
65const DIRECTORY_SIGNATURE_KEYWORD: KeywordRef = KeywordRef::new_const("directory-signature");
68
69const CONSENSUS_SIGNED_SHA3_256_HASH_TAIL: &str = "directory-signature ";
71
72static_assertions::const_assert!(std::mem::size_of::<usize>() >= std::mem::size_of::<u32>());
74
75pub fn gen_cons_diff(base: &str, target: &str) -> Result<String> {
94 let (base_signed, _) = split_directory_signatures(base)?;
96 let base_lines = base_signed.chars().filter(|c| *c == '\n').count() + 1;
97
98 let base_signed_hash = hex::encode_upper({
100 let mut h = tor_llcrypto::d::Sha3_256::new();
101 h.update(base_signed);
102 h.update(CONSENSUS_SIGNED_SHA3_256_HASH_TAIL);
103 h.finalize()
104 });
105 let target_hash = hex::encode_upper(tor_llcrypto::d::Sha3_256::digest(target.as_bytes()));
106
107 let ed_diff = gen_ed_diff(base_signed, target).map_err(|e| match e {
109 GenEdDiffError::MissingUnixLineEnding { lno } => Error::InvalidInput(ParseError::new(
110 ErrorProblem::OtherBadDocument("line does not end with '\\n'"),
111 "consdiff",
112 "",
113 lno,
114 None,
115 )),
116 GenEdDiffError::ContainsDotLine { lno } => Error::InvalidInput(ParseError::new(
117 ErrorProblem::OtherBadDocument("contains dotline"),
118 "consdiff",
119 "",
120 lno,
121 None,
122 )),
123 GenEdDiffError::Write(_) => internal!("string write was not infallible?").into(),
124 })?;
125
126 let result = format!(
127 "network-status-diff-version 1\n\
128 hash {base_signed_hash} {target_hash}\n\
129 {base_lines},$d\n\
130 {ed_diff}"
131 );
132
133 let check = apply_diff(base, &result, None).map_err(|_| internal!("apply call failed"))?;
135 if check.to_string() != target {
136 Err(internal!("result does not match?"))?;
137 }
138
139 Ok(result)
140}
141
142fn split_directory_signatures(input: &str) -> Result<(&str, &str)> {
144 let parse_input = ParseInput::new(input, "");
145 let mut items = ItemStream::new(&parse_input)?;
146
147 loop {
149 let item = items
154 .peek_keyword()
155 .map_err(|e| ParseError::new(e, "consdiff", "", items.lno_for_error(), None))?;
156
157 match item {
158 Some(DIRECTORY_SIGNATURE_KEYWORD) => {
159 let offset = items.byte_position();
160 return Ok((&input[..offset], &input[offset..]));
161 }
162 Some(_) => {
163 let _ = items.next();
165 }
166 None => {
167 return Err(Error::InvalidInput(ParseError::new(
169 ErrorProblem::MissingItem {
170 keyword: DIRECTORY_SIGNATURE_KEYWORD.as_str(),
171 },
172 "consdiff",
173 "",
174 items.lno_for_error(),
175 None,
176 )));
177 }
178 }
179 }
180}
181
182fn gen_ed_diff(base: &str, target: &str) -> std::result::Result<String, GenEdDiffError> {
187 let mut result = String::new();
188
189 let input = InternedInput::new(base, target);
192 let mut diff = Diff::compute(Algorithm::Myers, &input);
193 diff.postprocess_lines(&input);
194
195 let hunks = diff.hunks().collect::<Vec<_>>();
197 for hunk in hunks.into_iter().rev() {
198 let hunk_type = HunkType::determine(&hunk);
200 match hunk_type {
201 HunkType::Append => writeln!(result, "{}{hunk_type}", hunk.before.start)?,
203 HunkType::Delete | HunkType::Change => {
204 if hunk.before.start + 1 == hunk.before.end {
205 writeln!(result, "{}{hunk_type}", hunk.before.start + 1)?;
207 } else {
208 writeln!(
211 result,
212 "{},{}{hunk_type}",
213 hunk.before.start + 1,
214 hunk.before.end
215 )?;
216 }
217 }
218 }
219
220 match hunk_type {
222 HunkType::Append | HunkType::Change => {
223 let range = (hunk.after.start)..(hunk.after.end);
224 let tlines = range
225 .map(|idx| {
226 let idx = usize::try_from(idx).expect("32-bit static assertion violated?");
227 input.interner[input.after[idx]]
228 })
229 .collect::<Vec<_>>();
230
231 for (lno, line) in tlines.iter().copied().enumerate() {
232 if line.ends_with("\r\n") || !line.ends_with("\n") {
234 return Err(GenEdDiffError::MissingUnixLineEnding { lno: lno + 1 });
236 }
237
238 if line.trim_end() == "." {
246 return Err(GenEdDiffError::ContainsDotLine { lno: lno + 1 });
248 }
249
250 write!(result, "{line}")?;
252 }
253
254 writeln!(result, ".")?;
256 }
257 HunkType::Delete => {}
258 }
259 }
260
261 Ok(result)
262}
263
264#[derive(Clone, Copy, Debug, derive_more::Display)]
266enum HunkType {
267 #[display("a")]
269 Append,
270 #[display("d")]
272 Delete,
273 #[display("c")]
275 Change,
276}
277
278impl HunkType {
279 fn determine(hunk: &Hunk) -> Self {
281 if hunk.is_pure_insertion() {
282 Self::Append
283 } else if hunk.is_pure_removal() {
284 Self::Delete
285 } else {
286 Self::Change
287 }
288 }
289}
290
291pub fn looks_like_diff(s: &str) -> bool {
294 s.starts_with("network-status-diff-version")
295}
296
297#[cfg(any(test, feature = "slow-diff-apply"))]
303pub fn apply_diff_trivial<'a>(input: &'a str, diff: &'a str) -> Result<DiffResult<'a>> {
304 let mut diff_lines = diff.lines();
305 let (_, d2) = parse_diff_header(&mut diff_lines)?;
306
307 let mut diffable = DiffResult::from_str(input, d2);
308
309 for command in DiffCommandIter::new(diff_lines) {
310 command?.apply_to(&mut diffable)?;
311 }
312
313 Ok(diffable)
314}
315
316pub fn apply_diff<'a>(
322 input: &'a str,
323 diff: &'a str,
324 check_digest_in: Option<[u8; 32]>,
325) -> Result<DiffResult<'a>> {
326 let mut input = DiffResult::from_str(input, [0; 32]);
327
328 let mut diff_lines = diff.lines();
329 let (d1, d2) = parse_diff_header(&mut diff_lines)?;
330 if let Some(d_want) = check_digest_in {
331 if d1 != d_want {
332 return Err(Error::CantApply("listed digest does not match document"));
333 }
334 }
335
336 let mut output = DiffResult::new(d2);
337
338 for command in DiffCommandIter::new(diff_lines) {
339 command?.apply_transformation(&mut input, &mut output)?;
340 }
341
342 output.push_reversed(&input.lines[..]);
343
344 output.lines.reverse();
345 Ok(output)
346}
347
348fn parse_diff_header<'a, I>(iter: &mut I) -> Result<([u8; 32], [u8; 32])>
351where
352 I: Iterator<Item = &'a str>,
353{
354 let line1 = iter.next();
355 if line1 != Some("network-status-diff-version 1") {
356 return Err(Error::BadDiff("unrecognized or missing header"));
357 }
358 let line2 = iter.next().ok_or(Error::BadDiff("header truncated"))?;
359 if !line2.starts_with("hash ") {
360 return Err(Error::BadDiff("missing 'hash' line"));
361 }
362 let elts: Vec<_> = line2.split_ascii_whitespace().collect();
363 if elts.len() != 3 {
364 return Err(Error::BadDiff("invalid 'hash' line"));
365 }
366 let d1 = hex::decode(elts[1])?;
367 let d2 = hex::decode(elts[2])?;
368 match (d1.try_into(), d2.try_into()) {
369 (Ok(a), Ok(b)) => Ok((a, b)),
370 _ => Err(Error::BadDiff("wrong digest lengths on 'hash' line")),
371 }
372}
373
374#[derive(Clone, Debug)]
380enum DiffCommand<'a> {
381 Delete {
383 low: usize,
385 high: usize,
387 },
388 DeleteToEnd {
390 low: usize,
392 },
393 Replace {
396 low: usize,
398 high: usize,
400 lines: Vec<&'a str>,
402 },
403 Insert {
405 pos: usize,
407 lines: Vec<&'a str>,
409 },
410}
411
412#[derive(Clone, Debug)]
417pub struct DiffResult<'a> {
418 d_post: [u8; 32],
420 lines: Vec<&'a str>,
422}
423
424#[derive(Clone, Copy, Debug)]
427enum RangeEnd {
428 Num(NonZeroUsize),
430 DollarSign,
432}
433
434impl FromStr for RangeEnd {
435 type Err = Error;
436 fn from_str(s: &str) -> Result<RangeEnd> {
437 if s == "$" {
438 Ok(RangeEnd::DollarSign)
439 } else {
440 let v: NonZeroUsize = s.parse()?;
441 if v.get() == usize::MAX {
442 return Err(Error::BadDiff("range cannot end at usize::MAX"));
443 }
444 Ok(RangeEnd::Num(v))
445 }
446 }
447}
448
449impl<'a> DiffCommand<'a> {
450 #[cfg(any(test, feature = "slow-diff-apply"))]
455 fn apply_to(&self, target: &mut DiffResult<'a>) -> Result<()> {
456 match self {
457 Self::Delete { low, high } => {
458 target.remove_lines(*low, *high)?;
459 }
460 Self::DeleteToEnd { low } => {
461 target.remove_lines(*low, target.lines.len())?;
462 }
463 Self::Replace { low, high, lines } => {
464 target.remove_lines(*low, *high)?;
465 target.insert_at(*low, lines)?;
466 }
467 Self::Insert { pos, lines } => {
468 target.insert_at(*pos + 1, lines)?;
471 }
472 };
473 Ok(())
474 }
475
476 fn apply_transformation(
495 &self,
496 input: &mut DiffResult<'a>,
497 output: &mut DiffResult<'a>,
498 ) -> Result<()> {
499 if let Some(succ) = self.following_lines() {
500 if let Some(subslice) = input.lines.get(succ - 1..) {
501 output.push_reversed(subslice);
503 } else {
504 return Err(Error::CantApply(
506 "ending line number didn't correspond to document",
507 ));
508 }
509 }
510
511 if let Some(lines) = self.lines() {
512 output.push_reversed(lines);
514 }
515
516 let remove = self.first_removed_line();
517 if remove == 0 || (!self.is_insert() && remove > input.lines.len()) {
518 return Err(Error::CantApply(
519 "starting line number didn't correspond to document",
520 ));
521 }
522 input.lines.truncate(remove - 1);
523
524 Ok(())
525 }
526
527 fn lines(&self) -> Option<&[&'a str]> {
529 match self {
530 Self::Replace { lines, .. } | Self::Insert { lines, .. } => Some(lines.as_slice()),
531 _ => None,
532 }
533 }
534
535 fn linebuf_mut(&mut self) -> Option<&mut Vec<&'a str>> {
538 match self {
539 Self::Replace { lines, .. } | Self::Insert { lines, .. } => Some(lines),
540 _ => None,
541 }
542 }
543
544 fn following_lines(&self) -> Option<usize> {
549 match self {
550 Self::Delete { high, .. } | Self::Replace { high, .. } => Some(high + 1),
551 Self::DeleteToEnd { .. } => None,
552 Self::Insert { pos, .. } => Some(pos + 1),
553 }
554 }
555
556 fn first_removed_line(&self) -> usize {
562 match self {
563 Self::Delete { low, .. } => *low,
564 Self::DeleteToEnd { low } => *low,
565 Self::Replace { low, .. } => *low,
566 Self::Insert { pos, .. } => *pos + 1,
567 }
568 }
569
570 fn is_insert(&self) -> bool {
572 matches!(self, Self::Insert { .. })
573 }
574
575 fn from_line_iterator<I>(iter: &mut I) -> Result<Option<Self>>
578 where
579 I: Iterator<Item = &'a str>,
580 {
581 let command = match iter.next() {
582 Some(s) => s,
583 None => return Ok(None),
584 };
585
586 if command.len() < 2 || !command.is_ascii() {
590 return Err(Error::BadDiff("command too short"));
591 }
592
593 let (range, command) = command.split_at(command.len() - 1);
594 let (low, high) = if let Some(comma_pos) = range.find(',') {
595 (
596 range[..comma_pos].parse::<usize>()?,
597 Some(range[comma_pos + 1..].parse::<RangeEnd>()?),
598 )
599 } else {
600 (range.parse::<usize>()?, None)
601 };
602
603 if low == usize::MAX {
604 return Err(Error::BadDiff("range cannot begin at usize::MAX"));
605 }
606
607 match (low, high) {
608 (lo, Some(RangeEnd::Num(hi))) if lo > hi.into() => {
609 return Err(Error::BadDiff("mis-ordered lines in range"));
610 }
611 (_, _) => (),
612 }
613
614 let mut cmd = match (command, low, high) {
615 ("d", low, None) => Self::Delete { low, high: low },
616 ("d", low, Some(RangeEnd::Num(high))) => Self::Delete {
617 low,
618 high: high.into(),
619 },
620 ("d", low, Some(RangeEnd::DollarSign)) => Self::DeleteToEnd { low },
621 ("c", low, None) => Self::Replace {
622 low,
623 high: low,
624 lines: Vec::new(),
625 },
626 ("c", low, Some(RangeEnd::Num(high))) => Self::Replace {
627 low,
628 high: high.into(),
629 lines: Vec::new(),
630 },
631 ("a", low, None) => Self::Insert {
632 pos: low,
633 lines: Vec::new(),
634 },
635 (_, _, _) => return Err(Error::BadDiff("can't parse command line")),
636 };
637
638 if let Some(ref mut linebuf) = cmd.linebuf_mut() {
639 loop {
642 match iter.next() {
643 None => return Err(Error::BadDiff("unterminated block to insert")),
644 Some(".") => break,
645 Some(line) => linebuf.push(line),
646 }
647 }
648 }
649
650 Ok(Some(cmd))
651 }
652}
653
654struct DiffCommandIter<'a, I>
660where
661 I: Iterator<Item = &'a str>,
662{
663 iter: I,
665
666 last_cmd_first_removed: Option<usize>,
669}
670
671impl<'a, I> DiffCommandIter<'a, I>
672where
673 I: Iterator<Item = &'a str>,
674{
675 fn new(iter: I) -> Self {
677 DiffCommandIter {
678 iter,
679 last_cmd_first_removed: None,
680 }
681 }
682}
683
684impl<'a, I> Iterator for DiffCommandIter<'a, I>
685where
686 I: Iterator<Item = &'a str>,
687{
688 type Item = Result<DiffCommand<'a>>;
689 fn next(&mut self) -> Option<Result<DiffCommand<'a>>> {
690 match DiffCommand::from_line_iterator(&mut self.iter) {
691 Err(e) => Some(Err(e)),
692 Ok(None) => None,
693 Ok(Some(c)) => match (self.last_cmd_first_removed, c.following_lines()) {
694 (Some(_), None) => Some(Err(Error::BadDiff("misordered commands"))),
695 (Some(a), Some(b)) if a < b => Some(Err(Error::BadDiff("misordered commands"))),
696 (_, _) => {
697 self.last_cmd_first_removed = Some(c.first_removed_line());
698 Some(Ok(c))
699 }
700 },
701 }
702 }
703}
704
705impl<'a> DiffResult<'a> {
706 fn from_str(s: &'a str, d_post: [u8; 32]) -> Self {
709 let lines: Vec<_> = s.lines().collect();
713
714 DiffResult { d_post, lines }
715 }
716
717 fn new(d_post: [u8; 32]) -> Self {
720 DiffResult {
721 d_post,
722 lines: Vec::new(),
723 }
724 }
725
726 fn push_reversed(&mut self, lines: &[&'a str]) {
729 self.lines.extend(lines.iter().rev());
730 }
731
732 #[cfg(any(test, feature = "slow-diff-apply"))]
737 fn remove_lines(&mut self, first: usize, last: usize) -> Result<()> {
738 if first > self.lines.len() || last > self.lines.len() || first == 0 || last == 0 {
739 Err(Error::CantApply("line out of range"))
740 } else {
741 let n_to_remove = last - first + 1;
742 if last != self.lines.len() {
743 self.lines[..].copy_within((last).., first - 1);
744 }
745 self.lines.truncate(self.lines.len() - n_to_remove);
746 Ok(())
747 }
748 }
749
750 #[cfg(any(test, feature = "slow-diff-apply"))]
756 fn insert_at(&mut self, pos: usize, lines: &[&'a str]) -> Result<()> {
757 if pos > self.lines.len() + 1 || pos == 0 {
758 Err(Error::CantApply("position out of range"))
759 } else {
760 let orig_len = self.lines.len();
761 self.lines.resize(self.lines.len() + lines.len(), "");
762 self.lines
763 .copy_within(pos - 1..orig_len, pos - 1 + lines.len());
764 self.lines[(pos - 1)..(pos + lines.len() - 1)].copy_from_slice(lines);
765 Ok(())
766 }
767 }
768
769 pub fn check_digest(&self) -> Result<()> {
773 use digest::Digest;
774 use tor_llcrypto::d::Sha3_256;
775 let mut d = Sha3_256::new();
776 for line in &self.lines {
777 d.update(line.as_bytes());
778 d.update(b"\n");
779 }
780 if d.finalize() == self.d_post.into() {
781 Ok(())
782 } else {
783 Err(Error::CantApply("Wrong digest after applying diff"))
784 }
785 }
786}
787
788impl<'a> Display for DiffResult<'a> {
789 fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
790 for elt in &self.lines {
791 writeln!(f, "{}", elt)?;
792 }
793 Ok(())
794 }
795}
796
797#[cfg(test)]
798mod test {
799 #![allow(clippy::bool_assert_comparison)]
801 #![allow(clippy::clone_on_copy)]
802 #![allow(clippy::dbg_macro)]
803 #![allow(clippy::mixed_attributes_style)]
804 #![allow(clippy::print_stderr)]
805 #![allow(clippy::print_stdout)]
806 #![allow(clippy::single_char_pattern)]
807 #![allow(clippy::unwrap_used)]
808 #![allow(clippy::unchecked_time_subtraction)]
809 #![allow(clippy::useless_vec)]
810 #![allow(clippy::needless_pass_by_value)]
811 use rand::seq::IndexedRandom;
814 use tor_basic_utils::test_rng::testing_rng;
815
816 use super::*;
817
818 #[test]
819 fn remove() -> Result<()> {
820 let example = DiffResult::from_str("1\n2\n3\n4\n5\n6\n7\n8\n9\n", [0; 32]);
821
822 let mut d = example.clone();
823 d.remove_lines(5, 7)?;
824 assert_eq!(d.to_string(), "1\n2\n3\n4\n8\n9\n");
825
826 let mut d = example.clone();
827 d.remove_lines(1, 9)?;
828 assert_eq!(d.to_string(), "");
829
830 let mut d = example.clone();
831 d.remove_lines(1, 1)?;
832 assert_eq!(d.to_string(), "2\n3\n4\n5\n6\n7\n8\n9\n");
833
834 let mut d = example.clone();
835 d.remove_lines(6, 9)?;
836 assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n");
837
838 let mut d = example.clone();
839 assert!(d.remove_lines(6, 10).is_err());
840 assert!(d.remove_lines(0, 1).is_err());
841 assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n9\n");
842
843 Ok(())
844 }
845
846 #[test]
847 fn insert() -> Result<()> {
848 let example = DiffResult::from_str("1\n2\n3\n4\n5\n", [0; 32]);
849 let mut d = example.clone();
850 d.insert_at(3, &["hello", "world"])?;
851 assert_eq!(d.to_string(), "1\n2\nhello\nworld\n3\n4\n5\n");
852
853 let mut d = example.clone();
854 d.insert_at(6, &["hello", "world"])?;
855 assert_eq!(d.to_string(), "1\n2\n3\n4\n5\nhello\nworld\n");
856
857 let mut d = example.clone();
858 assert!(d.insert_at(0, &["hello", "world"]).is_err());
859 assert!(d.insert_at(7, &["hello", "world"]).is_err());
860 Ok(())
861 }
862
863 #[test]
864 fn push_reversed() {
865 let mut d = DiffResult::new([0; 32]);
866 d.push_reversed(&["7", "8", "9"]);
867 assert_eq!(d.to_string(), "9\n8\n7\n");
868 d.push_reversed(&["world", "hello", ""]);
869 assert_eq!(d.to_string(), "9\n8\n7\n\nhello\nworld\n");
870 }
871
872 #[test]
873 fn apply_command_simple() {
874 let example = DiffResult::from_str("a\nb\nc\nd\ne\nf\n", [0; 32]);
875
876 let mut d = example.clone();
877 assert_eq!(d.to_string(), "a\nb\nc\nd\ne\nf\n".to_string());
878 assert!(DiffCommand::DeleteToEnd { low: 5 }.apply_to(&mut d).is_ok());
879 assert_eq!(d.to_string(), "a\nb\nc\nd\n".to_string());
880
881 let mut d = example.clone();
882 assert!(
883 DiffCommand::Delete { low: 3, high: 5 }
884 .apply_to(&mut d)
885 .is_ok()
886 );
887 assert_eq!(d.to_string(), "a\nb\nf\n".to_string());
888
889 let mut d = example.clone();
890 assert!(
891 DiffCommand::Replace {
892 low: 3,
893 high: 5,
894 lines: vec!["hello", "world"]
895 }
896 .apply_to(&mut d)
897 .is_ok()
898 );
899 assert_eq!(d.to_string(), "a\nb\nhello\nworld\nf\n".to_string());
900
901 let mut d = example.clone();
902 assert!(
903 DiffCommand::Insert {
904 pos: 3,
905 lines: vec!["hello", "world"]
906 }
907 .apply_to(&mut d)
908 .is_ok()
909 );
910 assert_eq!(
911 d.to_string(),
912 "a\nb\nc\nhello\nworld\nd\ne\nf\n".to_string()
913 );
914 }
915
916 #[test]
917 fn parse_command() -> Result<()> {
918 fn parse(s: &str) -> Result<DiffCommand<'_>> {
919 let mut iter = s.lines();
920 let cmd = DiffCommand::from_line_iterator(&mut iter)?;
921 let cmd2 = DiffCommand::from_line_iterator(&mut iter)?;
922 if cmd2.is_some() {
923 panic!("Unexpected second command");
924 }
925 Ok(cmd.unwrap())
926 }
927
928 fn parse_err(s: &str) {
929 let mut iter = s.lines();
930 let cmd = DiffCommand::from_line_iterator(&mut iter);
931 assert!(matches!(cmd, Err(Error::BadDiff(_))));
932 }
933
934 let p = parse("3,8d\n")?;
935 assert!(matches!(p, DiffCommand::Delete { low: 3, high: 8 }));
936 let p = parse("3d\n")?;
937 assert!(matches!(p, DiffCommand::Delete { low: 3, high: 3 }));
938 let p = parse("100,$d\n")?;
939 assert!(matches!(p, DiffCommand::DeleteToEnd { low: 100 }));
940
941 let p = parse("30,40c\nHello\nWorld\n.\n")?;
942 assert!(matches!(
943 p,
944 DiffCommand::Replace {
945 low: 30,
946 high: 40,
947 ..
948 }
949 ));
950 assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
951 let p = parse("30c\nHello\nWorld\n.\n")?;
952 assert!(matches!(
953 p,
954 DiffCommand::Replace {
955 low: 30,
956 high: 30,
957 ..
958 }
959 ));
960 assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
961
962 let p = parse("999a\nHello\nWorld\n.\n")?;
963 assert!(matches!(p, DiffCommand::Insert { pos: 999, .. }));
964 assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
965 let p = parse("0a\nHello\nWorld\n.\n")?;
966 assert!(matches!(p, DiffCommand::Insert { pos: 0, .. }));
967 assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
968
969 parse_err("hello world");
970 parse_err("\n\n");
971 parse_err("$,5d");
972 parse_err("5,6,8d");
973 parse_err("8,5d");
974 parse_err("6");
975 parse_err("d");
976 parse_err("-10d");
977 parse_err("4,$c\na\n.");
978 parse_err("foo");
979 parse_err("5,10p");
980 parse_err("18446744073709551615a");
981 parse_err("1,18446744073709551615d");
982
983 Ok(())
984 }
985
986 #[test]
987 fn apply_transformation() -> Result<()> {
988 let example = DiffResult::from_str("1\n2\n3\n4\n5\n6\n7\n8\n9\n", [0; 32]);
989 let empty = DiffResult::new([1; 32]);
990
991 let mut inp = example.clone();
992 let mut out = empty.clone();
993 DiffCommand::DeleteToEnd { low: 5 }.apply_transformation(&mut inp, &mut out)?;
994 assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
995 assert_eq!(out.to_string(), "");
996
997 let mut inp = example.clone();
998 let mut out = empty.clone();
999 DiffCommand::DeleteToEnd { low: 9 }.apply_transformation(&mut inp, &mut out)?;
1000 assert_eq!(inp.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n");
1001 assert_eq!(out.to_string(), "");
1002
1003 let mut inp = example.clone();
1004 let mut out = empty.clone();
1005 DiffCommand::Delete { low: 3, high: 5 }.apply_transformation(&mut inp, &mut out)?;
1006 assert_eq!(inp.to_string(), "1\n2\n");
1007 assert_eq!(out.to_string(), "9\n8\n7\n6\n");
1008
1009 let mut inp = example.clone();
1010 let mut out = empty.clone();
1011 DiffCommand::Replace {
1012 low: 5,
1013 high: 6,
1014 lines: vec!["oh hey", "there"],
1015 }
1016 .apply_transformation(&mut inp, &mut out)?;
1017 assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
1018 assert_eq!(out.to_string(), "9\n8\n7\nthere\noh hey\n");
1019
1020 let mut inp = example.clone();
1021 let mut out = empty.clone();
1022 DiffCommand::Insert {
1023 pos: 3,
1024 lines: vec!["oh hey", "there"],
1025 }
1026 .apply_transformation(&mut inp, &mut out)?;
1027 assert_eq!(inp.to_string(), "1\n2\n3\n");
1028 assert_eq!(out.to_string(), "9\n8\n7\n6\n5\n4\nthere\noh hey\n");
1029 DiffCommand::Insert {
1030 pos: 0,
1031 lines: vec!["boom!"],
1032 }
1033 .apply_transformation(&mut inp, &mut out)?;
1034 assert_eq!(inp.to_string(), "");
1035 assert_eq!(
1036 out.to_string(),
1037 "9\n8\n7\n6\n5\n4\nthere\noh hey\n3\n2\n1\nboom!\n"
1038 );
1039
1040 let mut inp = example.clone();
1041 let mut out = empty.clone();
1042 let r = DiffCommand::Delete {
1043 low: 100,
1044 high: 200,
1045 }
1046 .apply_transformation(&mut inp, &mut out);
1047 assert!(r.is_err());
1048 let r = DiffCommand::Delete { low: 5, high: 200 }.apply_transformation(&mut inp, &mut out);
1049 assert!(r.is_err());
1050 let r = DiffCommand::Delete { low: 0, high: 1 }.apply_transformation(&mut inp, &mut out);
1051 assert!(r.is_err());
1052 let r = DiffCommand::DeleteToEnd { low: 10 }.apply_transformation(&mut inp, &mut out);
1053 assert!(r.is_err());
1054 Ok(())
1055 }
1056
1057 #[test]
1058 fn header() -> Result<()> {
1059 fn header_from(s: &str) -> Result<([u8; 32], [u8; 32])> {
1060 let mut iter = s.lines();
1061 parse_diff_header(&mut iter)
1062 }
1063
1064 let (a,b) = header_from(
1065 "network-status-diff-version 1
1066hash B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663 F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB"
1067 )?;
1068
1069 assert_eq!(
1070 &a[..],
1071 hex::decode("B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663")?
1072 );
1073 assert_eq!(
1074 &b[..],
1075 hex::decode("F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB")?
1076 );
1077
1078 assert!(header_from("network-status-diff-version 2\n").is_err());
1079 assert!(header_from("").is_err());
1080 assert!(header_from("5,$d\n1,2d\n").is_err());
1081 assert!(header_from("network-status-diff-version 1\n").is_err());
1082 assert!(
1083 header_from(
1084 "network-status-diff-version 1
1085hash x y
10865,5d"
1087 )
1088 .is_err()
1089 );
1090 assert!(
1091 header_from(
1092 "network-status-diff-version 1
1093hash x y
10945,5d"
1095 )
1096 .is_err()
1097 );
1098 assert!(
1099 header_from(
1100 "network-status-diff-version 1
1101hash AA BB
11025,5d"
1103 )
1104 .is_err()
1105 );
1106 assert!(
1107 header_from(
1108 "network-status-diff-version 1
1109oh hello there
11105,5d"
1111 )
1112 .is_err()
1113 );
1114 assert!(header_from("network-status-diff-version 1
1115hash B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663 F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB extra").is_err());
1116
1117 Ok(())
1118 }
1119
1120 #[test]
1121 fn apply_simple() {
1122 let pre = include_str!("../testdata/consensus1.txt");
1123 let diff = include_str!("../testdata/diff1.txt");
1124 let post = include_str!("../testdata/consensus2.txt");
1125
1126 let result = apply_diff_trivial(pre, diff).unwrap();
1127 assert!(result.check_digest().is_ok());
1128 assert_eq!(result.to_string(), post);
1129 }
1130
1131 #[test]
1132 fn sort_order() -> Result<()> {
1133 fn cmds(s: &str) -> Result<Vec<DiffCommand<'_>>> {
1134 let mut out = Vec::new();
1135 for cmd in DiffCommandIter::new(s.lines()) {
1136 out.push(cmd?);
1137 }
1138 Ok(out)
1139 }
1140
1141 let _ = cmds("6,9d\n5,5d\n")?;
1142 assert!(cmds("5,5d\n6,9d\n").is_err());
1143 assert!(cmds("5,5d\n6,6d\n").is_err());
1144 assert!(cmds("5,5d\n5,6d\n").is_err());
1145
1146 Ok(())
1147 }
1148
1149 #[test]
1151 fn cons_diff() {
1152 const WORDS: &[&str] = &[
1154 "citole",
1155 "aflow",
1156 "plowfoot",
1157 "coom",
1158 "retape",
1159 "perish",
1160 "overstifle",
1161 "ramshackle",
1162 "Romeo",
1163 "alme",
1164 "expressivity",
1165 "Kieffer",
1166 "tobe",
1167 "pronucleus",
1168 "countersconce",
1169 "puli",
1170 "acupunctuate",
1171 "heterolysis",
1172 "unwattled",
1173 "bismerpund",
1174 ];
1175
1176 let rng = &mut testing_rng();
1177 let mut left = (0..1000)
1178 .map(|_| WORDS.choose(rng).unwrap().to_string() + "\n")
1179 .collect::<String>();
1180 left += "directory-signature foo bar\n";
1181 let mut right = (0..1015)
1182 .map(|_| WORDS.choose(rng).unwrap().to_string() + "\n")
1183 .collect::<String>();
1184 right += "directory-signature foo baz\n";
1185
1186 let diff = gen_cons_diff(&left, &right).unwrap();
1187 let check = apply_diff(&left, &diff, None).unwrap().to_string();
1188 assert_eq!(right, check);
1189 }
1190
1191 #[test]
1192 fn dot_line() {
1193 let base = "";
1194 let target = "foo\nbar\n.\nbaz\nfoo\n";
1195 assert_eq!(
1196 gen_ed_diff(base, target).unwrap_err(),
1197 GenEdDiffError::ContainsDotLine { lno: 3 },
1198 );
1199
1200 let target = "foo\nbar\n. \t \nbaz\nfoo\n";
1202 assert_eq!(
1203 gen_ed_diff(base, target).unwrap_err(),
1204 GenEdDiffError::ContainsDotLine { lno: 3 },
1205 );
1206
1207 let target = "foo\nbar\n. foo\nbaz\nfoo\n";
1209 let _ = gen_ed_diff(base, target).unwrap();
1210
1211 let base = "directory-signature foo baz\n";
1213 let target = ".foo bar\n. bar\ndirectory-signature foo baz\n";
1214 assert_eq!(
1215 gen_cons_diff(base, target).unwrap(),
1216 "network-status-diff-version 1\n\
1217 hash D8138DC27D9A66F5760058A6BCB71B755462B9D26B811828F124D036DE329A58 \
1218 506AC3A4407BC5305DD0D08FED3F09C2FE69847541F642A8FD13D3BD06FFE432\n\
1219 1,$d\n\
1220 0a\n\
1221 .foo bar\n\
1222 . bar\n\
1223 directory-signature foo baz\n\
1224 .\n"
1225 );
1226 }
1227
1228 #[test]
1229 fn missing_newline() {
1230 let base = "";
1231 let target = "foo\nbar\nbaz";
1232 assert_eq!(
1233 gen_ed_diff(base, target).unwrap_err(),
1234 GenEdDiffError::MissingUnixLineEnding { lno: 3 }
1235 );
1236 }
1237
1238 #[test]
1239 fn mixed_with_crlf() {
1240 let base = "";
1241 let target = "foo\r\nbar\r\nbaz\nhello\r\n";
1242 assert_eq!(
1243 gen_ed_diff(base, target).unwrap_err(),
1244 GenEdDiffError::MissingUnixLineEnding { lno: 1 }
1245 );
1246 }
1247}