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#![allow(clippy::cognitive_complexity)] #![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)]
47#![deny(clippy::string_slice)] use std::fmt::{Display, Formatter, Write};
51use std::num::NonZeroUsize;
52use std::str::FromStr;
53
54mod err;
55use digest::Digest;
56pub use err::Error;
57use imara_diff::{Algorithm, Diff, Hunk, InternedInput};
58use tor_error::{internal, into_internal};
59use tor_netdoc::parse2::{ErrorProblem, ItemStream, KeywordRef, ParseError, ParseInput};
60
61use crate::err::GenEdDiffError;
62
63type Result<T> = std::result::Result<T, Error>;
65
66const DIRECTORY_SIGNATURE_KEYWORD: KeywordRef = KeywordRef::new_const("directory-signature");
69
70const CONSENSUS_SIGNED_SHA3_256_HASH_TAIL: &str = "directory-signature ";
72
73static_assertions::const_assert!(std::mem::size_of::<usize>() >= std::mem::size_of::<u32>());
75
76pub fn gen_cons_diff(
95 base: &str,
96 target: &str,
97 size_strictness: DiffSizeStrictness,
98) -> Result<String> {
99 let (base_signed, _) = split_directory_signatures(base)?;
101 let base_lines = base_signed.chars().filter(|c| *c == '\n').count() + 1;
102
103 let base_signed_hash = hex::encode_upper({
105 let mut h = tor_llcrypto::d::Sha3_256::new();
106 h.update(base_signed);
107 h.update(CONSENSUS_SIGNED_SHA3_256_HASH_TAIL);
108 h.finalize()
109 });
110 let target_hash = hex::encode_upper(tor_llcrypto::d::Sha3_256::digest(target.as_bytes()));
111
112 let ed_diff = gen_ed_diff(base_signed, target).map_err(|e| match e {
114 GenEdDiffError::MissingUnixLineEnding { lno } => Error::InvalidInput(ParseError::new(
115 ErrorProblem::OtherBadDocument("line does not end with '\\n'"),
116 "consdiff",
117 "",
118 lno,
119 None,
120 )),
121 GenEdDiffError::ContainsDotLine { lno } => Error::InvalidInput(ParseError::new(
122 ErrorProblem::OtherBadDocument("contains dotline"),
123 "consdiff",
124 "",
125 lno,
126 None,
127 )),
128 GenEdDiffError::Write(_) => internal!("string write was not infallible?").into(),
129 })?;
130
131 let result = format!(
132 "network-status-diff-version 1\n\
133 hash {base_signed_hash} {target_hash}\n\
134 {base_lines},$d\n\
135 {ed_diff}"
136 );
137
138 let check = match apply_diff(base, &result, None, size_strictness) {
140 Ok(v) => v,
141 Err(Error::DiffTooLarge) => return Err(Error::DiffTooLarge),
142 Err(e) => return Err(into_internal!("unable to apply generated diff")(e).into()),
143 };
144
145 if check.to_string() != target {
146 Err(internal!("result does not match?"))?;
147 }
148
149 Ok(result)
150}
151
152fn split_directory_signatures(input: &str) -> Result<(&str, &str)> {
154 let parse_input = ParseInput::new(input, "");
155 let mut items = ItemStream::new(&parse_input);
156
157 loop {
159 let item = items
164 .peek_keyword()
165 .map_err(|e| ParseError::new(e, "consdiff", "", items.lno_for_error(), None))?;
166
167 match item {
168 Some(DIRECTORY_SIGNATURE_KEYWORD) => {
169 let offset = items.byte_position();
170 return Ok(input
171 .split_at_checked(offset)
172 .ok_or_else(|| internal!("Calculated an invalid offset"))?);
173 }
174 Some(_) => {
175 let _ = items.next();
177 }
178 None => {
179 return Err(Error::InvalidInput(ParseError::new(
181 ErrorProblem::MissingItem {
182 keyword: DIRECTORY_SIGNATURE_KEYWORD.as_str(),
183 },
184 "consdiff",
185 "",
186 items.lno_for_error(),
187 None,
188 )));
189 }
190 }
191 }
192}
193
194fn gen_ed_diff(base: &str, target: &str) -> std::result::Result<String, GenEdDiffError> {
199 let mut result = String::new();
200
201 let input = InternedInput::new(base, target);
204 let mut diff = Diff::compute(Algorithm::Myers, &input);
205 diff.postprocess_lines(&input);
206
207 let hunks = diff.hunks().collect::<Vec<_>>();
209 for hunk in hunks.into_iter().rev() {
210 let hunk_type = HunkType::determine(&hunk);
212 match hunk_type {
213 HunkType::Append => writeln!(result, "{}{hunk_type}", hunk.before.start)?,
215 HunkType::Delete | HunkType::Change => {
216 if hunk.before.start + 1 == hunk.before.end {
217 writeln!(result, "{}{hunk_type}", hunk.before.start + 1)?;
219 } else {
220 writeln!(
223 result,
224 "{},{}{hunk_type}",
225 hunk.before.start + 1,
226 hunk.before.end
227 )?;
228 }
229 }
230 }
231
232 match hunk_type {
234 HunkType::Append | HunkType::Change => {
235 let range = (hunk.after.start)..(hunk.after.end);
236 let tlines = range
237 .map(|idx| {
238 let idx = usize::try_from(idx).expect("32-bit static assertion violated?");
239 input.interner[input.after[idx]]
240 })
241 .collect::<Vec<_>>();
242
243 for (lno, line) in tlines.iter().copied().enumerate() {
244 if line.ends_with("\r\n") || !line.ends_with("\n") {
246 return Err(GenEdDiffError::MissingUnixLineEnding { lno: lno + 1 });
248 }
249
250 if line.trim_end() == "." {
258 return Err(GenEdDiffError::ContainsDotLine { lno: lno + 1 });
260 }
261
262 write!(result, "{line}")?;
264 }
265
266 writeln!(result, ".")?;
268 }
269 HunkType::Delete => {}
270 }
271 }
272
273 Ok(result)
274}
275
276#[derive(Clone, Copy, Debug, derive_more::Display)]
278enum HunkType {
279 #[display("a")]
281 Append,
282 #[display("d")]
284 Delete,
285 #[display("c")]
287 Change,
288}
289
290impl HunkType {
291 fn determine(hunk: &Hunk) -> Self {
293 if hunk.is_pure_insertion() {
294 Self::Append
295 } else if hunk.is_pure_removal() {
296 Self::Delete
297 } else {
298 Self::Change
299 }
300 }
301}
302
303pub fn looks_like_diff(s: &str) -> bool {
306 s.starts_with("network-status-diff-version")
307}
308
309#[cfg(any(test, feature = "slow-diff-apply"))]
315pub fn apply_diff_trivial<'a>(input: &'a str, diff: &'a str) -> Result<DiffResult<'a>> {
316 let mut diff_lines = diff.lines();
317 let (_, d2) = parse_diff_header(&mut diff_lines)?;
318
319 let mut diffable = DiffResult::from_str(input, d2);
320
321 for command in DiffCommandIter::new(diff_lines) {
322 command?.apply_to(&mut diffable)?;
323 }
324
325 Ok(diffable)
326}
327
328pub fn apply_diff<'a>(
334 input: &'a str,
335 diff: &'a str,
336 check_digest_in: Option<[u8; 32]>,
337 size_strictness: DiffSizeStrictness,
338) -> Result<DiffResult<'a>> {
339 let input_bytes = input.len();
340 let diff_bytes = diff.len();
341 let diff_lines = count_nl(diff);
343
344 let mut input = DiffResult::from_str(input, [0; 32]);
345 let input_lines = input.lines.len();
346
347 size_strictness.check(input_bytes, input_lines, diff_bytes, diff_lines)?;
348
349 let mut diff_lines = diff.lines();
350 let (d1, d2) = parse_diff_header(&mut diff_lines)?;
351 if let Some(d_want) = check_digest_in {
352 if d1 != d_want {
353 return Err(Error::CantApply("listed digest does not match document"));
354 }
355 }
356
357 let mut output = DiffResult::new(d2);
358
359 for command in DiffCommandIter::new(diff_lines) {
360 command?.apply_transformation(&mut input, &mut output)?;
361 }
362
363 output.push_reversed(&input.lines[..]);
364
365 output.lines.reverse();
366 Ok(output)
367}
368
369#[derive(Clone, Copy, Debug)]
372#[non_exhaustive]
373pub enum DiffSizeStrictness {
374 None,
376
377 Apply,
379
380 Generate,
384}
385
386impl DiffSizeStrictness {
387 fn check(
390 &self,
391 input_bytes: usize,
392 input_lines: usize,
393 diff_bytes: usize,
394 diff_lines: usize,
395 ) -> Result<()> {
396 use DiffSizeStrictness as S;
397 use Error::DiffTooLarge;
398
399 match self {
400 S::None => {}
401 S::Generate => {
402 if diff_bytes >= input_bytes {
405 return Err(DiffTooLarge);
406 }
407
408 S::Apply.check(input_bytes, input_lines, diff_bytes, diff_lines)?;
410 }
411 S::Apply => {
412 const BYTE_THRESHOLD: usize = 64 * 1024;
413 const LINE_THRESHOLD: usize = 1024;
414 if diff_bytes >= BYTE_THRESHOLD && diff_bytes >= input_bytes.saturating_mul(2) {
415 return Err(DiffTooLarge);
416 }
417 if diff_lines >= LINE_THRESHOLD && diff_lines >= input_lines.saturating_mul(3) {
418 return Err(DiffTooLarge);
419 }
420 }
421 }
422
423 Ok(())
424 }
425}
426
427fn parse_diff_header<'a, I>(iter: &mut I) -> Result<([u8; 32], [u8; 32])>
430where
431 I: Iterator<Item = &'a str>,
432{
433 let line1 = iter.next();
434 if line1 != Some("network-status-diff-version 1") {
435 return Err(Error::BadDiff("unrecognized or missing header"));
436 }
437 let line2 = iter.next().ok_or(Error::BadDiff("header truncated"))?;
438 if !line2.starts_with("hash ") {
439 return Err(Error::BadDiff("missing 'hash' line"));
440 }
441 let elts: Vec<_> = line2.split_ascii_whitespace().collect();
442 if elts.len() != 3 {
443 return Err(Error::BadDiff("invalid 'hash' line"));
444 }
445 let d1 = hex::decode(elts[1])?;
446 let d2 = hex::decode(elts[2])?;
447 match (d1.try_into(), d2.try_into()) {
448 (Ok(a), Ok(b)) => Ok((a, b)),
449 _ => Err(Error::BadDiff("wrong digest lengths on 'hash' line")),
450 }
451}
452
453#[derive(Clone, Debug)]
459enum DiffCommand<'a> {
460 Delete {
462 low: usize,
464 high: usize,
466 },
467 DeleteToEnd {
469 low: usize,
471 },
472 Replace {
475 low: usize,
477 high: usize,
479 lines: Vec<&'a str>,
481 },
482 Insert {
484 pos: usize,
486 lines: Vec<&'a str>,
488 },
489}
490
491#[derive(Clone, Debug)]
496pub struct DiffResult<'a> {
497 d_post: [u8; 32],
499 lines: Vec<&'a str>,
501}
502
503#[derive(Clone, Copy, Debug)]
506enum RangeEnd {
507 Num(NonZeroUsize),
509 DollarSign,
511}
512
513impl FromStr for RangeEnd {
514 type Err = Error;
515 fn from_str(s: &str) -> Result<RangeEnd> {
516 if s == "$" {
517 Ok(RangeEnd::DollarSign)
518 } else {
519 let v: NonZeroUsize = s.parse()?;
520 if v.get() == usize::MAX {
521 return Err(Error::BadDiff("range cannot end at usize::MAX"));
522 }
523 Ok(RangeEnd::Num(v))
524 }
525 }
526}
527
528impl<'a> DiffCommand<'a> {
529 #[cfg(any(test, feature = "slow-diff-apply"))]
534 fn apply_to(&self, target: &mut DiffResult<'a>) -> Result<()> {
535 match self {
536 Self::Delete { low, high } => {
537 target.remove_lines(*low, *high)?;
538 }
539 Self::DeleteToEnd { low } => {
540 target.remove_lines(*low, target.lines.len())?;
541 }
542 Self::Replace { low, high, lines } => {
543 target.remove_lines(*low, *high)?;
544 target.insert_at(*low, lines)?;
545 }
546 Self::Insert { pos, lines } => {
547 target.insert_at(*pos + 1, lines)?;
550 }
551 };
552 Ok(())
553 }
554
555 fn apply_transformation(
574 &self,
575 input: &mut DiffResult<'a>,
576 output: &mut DiffResult<'a>,
577 ) -> Result<()> {
578 if let Some(succ) = self.following_lines() {
579 if let Some(subslice) = input.lines.get(succ - 1..) {
580 output.push_reversed(subslice);
582 } else {
583 return Err(Error::CantApply(
585 "ending line number didn't correspond to document",
586 ));
587 }
588 }
589
590 if let Some(lines) = self.lines() {
591 output.push_reversed(lines);
593 }
594
595 let remove = self.first_removed_line();
596 if remove == 0 || (!self.is_insert() && remove > input.lines.len()) {
597 return Err(Error::CantApply(
598 "starting line number didn't correspond to document",
599 ));
600 }
601 input.lines.truncate(remove - 1);
602
603 Ok(())
604 }
605
606 fn lines(&self) -> Option<&[&'a str]> {
608 match self {
609 Self::Replace { lines, .. } | Self::Insert { lines, .. } => Some(lines.as_slice()),
610 _ => None,
611 }
612 }
613
614 fn linebuf_mut(&mut self) -> Option<&mut Vec<&'a str>> {
617 match self {
618 Self::Replace { lines, .. } | Self::Insert { lines, .. } => Some(lines),
619 _ => None,
620 }
621 }
622
623 fn following_lines(&self) -> Option<usize> {
628 match self {
629 Self::Delete { high, .. } | Self::Replace { high, .. } => Some(high + 1),
630 Self::DeleteToEnd { .. } => None,
631 Self::Insert { pos, .. } => Some(pos + 1),
632 }
633 }
634
635 fn first_removed_line(&self) -> usize {
641 match self {
642 Self::Delete { low, .. } => *low,
643 Self::DeleteToEnd { low } => *low,
644 Self::Replace { low, .. } => *low,
645 Self::Insert { pos, .. } => *pos + 1,
646 }
647 }
648
649 fn is_insert(&self) -> bool {
651 matches!(self, Self::Insert { .. })
652 }
653
654 fn from_line_iterator<I>(iter: &mut I) -> Result<Option<Self>>
657 where
658 I: Iterator<Item = &'a str>,
659 {
660 let command = match iter.next() {
661 Some(s) => s,
662 None => return Ok(None),
663 };
664
665 if command.len() < 2 || !command.is_ascii() {
669 return Err(Error::BadDiff("command too short"));
670 }
671
672 let (range, command) = command.split_at(command.len() - 1);
673 let (low, high) = if let Some((lo, hi)) = range.split_once(',') {
674 (lo.parse::<usize>()?, Some(hi.parse::<RangeEnd>()?))
675 } else {
676 (range.parse::<usize>()?, None)
677 };
678
679 if low == usize::MAX {
680 return Err(Error::BadDiff("range cannot begin at usize::MAX"));
681 }
682
683 match (low, high) {
684 (lo, Some(RangeEnd::Num(hi))) if lo > hi.into() => {
685 return Err(Error::BadDiff("mis-ordered lines in range"));
686 }
687 (_, _) => (),
688 }
689
690 let mut cmd = match (command, low, high) {
691 ("d", low, None) => Self::Delete { low, high: low },
692 ("d", low, Some(RangeEnd::Num(high))) => Self::Delete {
693 low,
694 high: high.into(),
695 },
696 ("d", low, Some(RangeEnd::DollarSign)) => Self::DeleteToEnd { low },
697 ("c", low, None) => Self::Replace {
698 low,
699 high: low,
700 lines: Vec::new(),
701 },
702 ("c", low, Some(RangeEnd::Num(high))) => Self::Replace {
703 low,
704 high: high.into(),
705 lines: Vec::new(),
706 },
707 ("a", low, None) => Self::Insert {
708 pos: low,
709 lines: Vec::new(),
710 },
711 (_, _, _) => return Err(Error::BadDiff("can't parse command line")),
712 };
713
714 if let Some(ref mut linebuf) = cmd.linebuf_mut() {
715 loop {
718 match iter.next() {
719 None => return Err(Error::BadDiff("unterminated block to insert")),
720 Some(".") => break,
721 Some(line) => linebuf.push(line),
722 }
723 }
724 }
725
726 Ok(Some(cmd))
727 }
728}
729
730struct DiffCommandIter<'a, I>
736where
737 I: Iterator<Item = &'a str>,
738{
739 iter: I,
741
742 last_cmd_first_removed: Option<usize>,
745}
746
747impl<'a, I> DiffCommandIter<'a, I>
748where
749 I: Iterator<Item = &'a str>,
750{
751 fn new(iter: I) -> Self {
753 DiffCommandIter {
754 iter,
755 last_cmd_first_removed: None,
756 }
757 }
758}
759
760impl<'a, I> Iterator for DiffCommandIter<'a, I>
761where
762 I: Iterator<Item = &'a str>,
763{
764 type Item = Result<DiffCommand<'a>>;
765 fn next(&mut self) -> Option<Result<DiffCommand<'a>>> {
766 match DiffCommand::from_line_iterator(&mut self.iter) {
767 Err(e) => Some(Err(e)),
768 Ok(None) => None,
769 Ok(Some(c)) => match (self.last_cmd_first_removed, c.following_lines()) {
770 (Some(_), None) => Some(Err(Error::BadDiff("misordered commands"))),
771 (Some(a), Some(b)) if a < b => Some(Err(Error::BadDiff("misordered commands"))),
772 (_, _) => {
773 self.last_cmd_first_removed = Some(c.first_removed_line());
774 Some(Ok(c))
775 }
776 },
777 }
778 }
779}
780
781impl<'a> DiffResult<'a> {
782 fn from_str(s: &'a str, d_post: [u8; 32]) -> Self {
785 let lines: Vec<_> = s.lines().collect();
789
790 DiffResult { d_post, lines }
791 }
792
793 fn new(d_post: [u8; 32]) -> Self {
796 DiffResult {
797 d_post,
798 lines: Vec::new(),
799 }
800 }
801
802 fn push_reversed(&mut self, lines: &[&'a str]) {
805 self.lines.extend(lines.iter().rev());
806 }
807
808 #[cfg(any(test, feature = "slow-diff-apply"))]
813 fn remove_lines(&mut self, first: usize, last: usize) -> Result<()> {
814 if first > self.lines.len() || last > self.lines.len() || first == 0 || last == 0 {
815 Err(Error::CantApply("line out of range"))
816 } else {
817 let n_to_remove = last - first + 1;
818 if last != self.lines.len() {
819 self.lines[..].copy_within((last).., first - 1);
820 }
821 self.lines.truncate(self.lines.len() - n_to_remove);
822 Ok(())
823 }
824 }
825
826 #[cfg(any(test, feature = "slow-diff-apply"))]
832 fn insert_at(&mut self, pos: usize, lines: &[&'a str]) -> Result<()> {
833 if pos > self.lines.len() + 1 || pos == 0 {
834 Err(Error::CantApply("position out of range"))
835 } else {
836 let orig_len = self.lines.len();
837 self.lines.resize(self.lines.len() + lines.len(), "");
838 self.lines
839 .copy_within(pos - 1..orig_len, pos - 1 + lines.len());
840 self.lines[(pos - 1)..(pos + lines.len() - 1)].copy_from_slice(lines);
841 Ok(())
842 }
843 }
844
845 pub fn check_digest(&self) -> Result<()> {
849 use digest::Digest;
850 use tor_llcrypto::d::Sha3_256;
851 let mut d = Sha3_256::new();
852 for line in &self.lines {
853 d.update(line.as_bytes());
854 d.update(b"\n");
855 }
856 if d.finalize() == self.d_post.into() {
857 Ok(())
858 } else {
859 Err(Error::CantApply("Wrong digest after applying diff"))
860 }
861 }
862}
863
864impl<'a> Display for DiffResult<'a> {
865 fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
866 for elt in &self.lines {
867 writeln!(f, "{}", elt)?;
868 }
869 Ok(())
870 }
871}
872
873fn count_nl(s: &str) -> usize {
878 s.bytes().filter(|b| *b == b'\n').count()
879}
880
881#[cfg(test)]
882mod test {
883 #![allow(clippy::bool_assert_comparison)]
885 #![allow(clippy::clone_on_copy)]
886 #![allow(clippy::dbg_macro)]
887 #![allow(clippy::mixed_attributes_style)]
888 #![allow(clippy::print_stderr)]
889 #![allow(clippy::print_stdout)]
890 #![allow(clippy::single_char_pattern)]
891 #![allow(clippy::unwrap_used)]
892 #![allow(clippy::unchecked_time_subtraction)]
893 #![allow(clippy::useless_vec)]
894 #![allow(clippy::needless_pass_by_value)]
895 #![allow(clippy::string_slice)] use rand::seq::IndexedRandom;
899 use tor_basic_utils::test_rng::testing_rng;
900
901 use super::DiffSizeStrictness as DSS;
902 use super::*;
903
904 #[test]
905 fn remove() -> Result<()> {
906 let example = DiffResult::from_str("1\n2\n3\n4\n5\n6\n7\n8\n9\n", [0; 32]);
907
908 let mut d = example.clone();
909 d.remove_lines(5, 7)?;
910 assert_eq!(d.to_string(), "1\n2\n3\n4\n8\n9\n");
911
912 let mut d = example.clone();
913 d.remove_lines(1, 9)?;
914 assert_eq!(d.to_string(), "");
915
916 let mut d = example.clone();
917 d.remove_lines(1, 1)?;
918 assert_eq!(d.to_string(), "2\n3\n4\n5\n6\n7\n8\n9\n");
919
920 let mut d = example.clone();
921 d.remove_lines(6, 9)?;
922 assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n");
923
924 let mut d = example.clone();
925 assert!(d.remove_lines(6, 10).is_err());
926 assert!(d.remove_lines(0, 1).is_err());
927 assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n9\n");
928
929 Ok(())
930 }
931
932 #[test]
933 fn insert() -> Result<()> {
934 let example = DiffResult::from_str("1\n2\n3\n4\n5\n", [0; 32]);
935 let mut d = example.clone();
936 d.insert_at(3, &["hello", "world"])?;
937 assert_eq!(d.to_string(), "1\n2\nhello\nworld\n3\n4\n5\n");
938
939 let mut d = example.clone();
940 d.insert_at(6, &["hello", "world"])?;
941 assert_eq!(d.to_string(), "1\n2\n3\n4\n5\nhello\nworld\n");
942
943 let mut d = example.clone();
944 assert!(d.insert_at(0, &["hello", "world"]).is_err());
945 assert!(d.insert_at(7, &["hello", "world"]).is_err());
946 Ok(())
947 }
948
949 #[test]
950 fn push_reversed() {
951 let mut d = DiffResult::new([0; 32]);
952 d.push_reversed(&["7", "8", "9"]);
953 assert_eq!(d.to_string(), "9\n8\n7\n");
954 d.push_reversed(&["world", "hello", ""]);
955 assert_eq!(d.to_string(), "9\n8\n7\n\nhello\nworld\n");
956 }
957
958 #[test]
959 fn apply_command_simple() {
960 let example = DiffResult::from_str("a\nb\nc\nd\ne\nf\n", [0; 32]);
961
962 let mut d = example.clone();
963 assert_eq!(d.to_string(), "a\nb\nc\nd\ne\nf\n".to_string());
964 assert!(DiffCommand::DeleteToEnd { low: 5 }.apply_to(&mut d).is_ok());
965 assert_eq!(d.to_string(), "a\nb\nc\nd\n".to_string());
966
967 let mut d = example.clone();
968 assert!(
969 DiffCommand::Delete { low: 3, high: 5 }
970 .apply_to(&mut d)
971 .is_ok()
972 );
973 assert_eq!(d.to_string(), "a\nb\nf\n".to_string());
974
975 let mut d = example.clone();
976 assert!(
977 DiffCommand::Replace {
978 low: 3,
979 high: 5,
980 lines: vec!["hello", "world"]
981 }
982 .apply_to(&mut d)
983 .is_ok()
984 );
985 assert_eq!(d.to_string(), "a\nb\nhello\nworld\nf\n".to_string());
986
987 let mut d = example.clone();
988 assert!(
989 DiffCommand::Insert {
990 pos: 3,
991 lines: vec!["hello", "world"]
992 }
993 .apply_to(&mut d)
994 .is_ok()
995 );
996 assert_eq!(
997 d.to_string(),
998 "a\nb\nc\nhello\nworld\nd\ne\nf\n".to_string()
999 );
1000 }
1001
1002 #[test]
1003 fn parse_command() -> Result<()> {
1004 fn parse(s: &str) -> Result<DiffCommand<'_>> {
1005 let mut iter = s.lines();
1006 let cmd = DiffCommand::from_line_iterator(&mut iter)?;
1007 let cmd2 = DiffCommand::from_line_iterator(&mut iter)?;
1008 if cmd2.is_some() {
1009 panic!("Unexpected second command");
1010 }
1011 Ok(cmd.unwrap())
1012 }
1013
1014 fn parse_err(s: &str) {
1015 let mut iter = s.lines();
1016 let cmd = DiffCommand::from_line_iterator(&mut iter);
1017 assert!(matches!(cmd, Err(Error::BadDiff(_))));
1018 }
1019
1020 let p = parse("3,8d\n")?;
1021 assert!(matches!(p, DiffCommand::Delete { low: 3, high: 8 }));
1022 let p = parse("3d\n")?;
1023 assert!(matches!(p, DiffCommand::Delete { low: 3, high: 3 }));
1024 let p = parse("100,$d\n")?;
1025 assert!(matches!(p, DiffCommand::DeleteToEnd { low: 100 }));
1026
1027 let p = parse("30,40c\nHello\nWorld\n.\n")?;
1028 assert!(matches!(
1029 p,
1030 DiffCommand::Replace {
1031 low: 30,
1032 high: 40,
1033 ..
1034 }
1035 ));
1036 assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
1037 let p = parse("30c\nHello\nWorld\n.\n")?;
1038 assert!(matches!(
1039 p,
1040 DiffCommand::Replace {
1041 low: 30,
1042 high: 30,
1043 ..
1044 }
1045 ));
1046 assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
1047
1048 let p = parse("999a\nHello\nWorld\n.\n")?;
1049 assert!(matches!(p, DiffCommand::Insert { pos: 999, .. }));
1050 assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
1051 let p = parse("0a\nHello\nWorld\n.\n")?;
1052 assert!(matches!(p, DiffCommand::Insert { pos: 0, .. }));
1053 assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
1054
1055 parse_err("hello world");
1056 parse_err("\n\n");
1057 parse_err("$,5d");
1058 parse_err("5,6,8d");
1059 parse_err("8,5d");
1060 parse_err("6");
1061 parse_err("d");
1062 parse_err("-10d");
1063 parse_err("4,$c\na\n.");
1064 parse_err("foo");
1065 parse_err("5,10p");
1066 parse_err("18446744073709551615a");
1067 parse_err("1,18446744073709551615d");
1068
1069 Ok(())
1070 }
1071
1072 #[test]
1073 fn apply_transformation() -> Result<()> {
1074 let example = DiffResult::from_str("1\n2\n3\n4\n5\n6\n7\n8\n9\n", [0; 32]);
1075 let empty = DiffResult::new([1; 32]);
1076
1077 let mut inp = example.clone();
1078 let mut out = empty.clone();
1079 DiffCommand::DeleteToEnd { low: 5 }.apply_transformation(&mut inp, &mut out)?;
1080 assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
1081 assert_eq!(out.to_string(), "");
1082
1083 let mut inp = example.clone();
1084 let mut out = empty.clone();
1085 DiffCommand::DeleteToEnd { low: 9 }.apply_transformation(&mut inp, &mut out)?;
1086 assert_eq!(inp.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n");
1087 assert_eq!(out.to_string(), "");
1088
1089 let mut inp = example.clone();
1090 let mut out = empty.clone();
1091 DiffCommand::Delete { low: 3, high: 5 }.apply_transformation(&mut inp, &mut out)?;
1092 assert_eq!(inp.to_string(), "1\n2\n");
1093 assert_eq!(out.to_string(), "9\n8\n7\n6\n");
1094
1095 let mut inp = example.clone();
1096 let mut out = empty.clone();
1097 DiffCommand::Replace {
1098 low: 5,
1099 high: 6,
1100 lines: vec!["oh hey", "there"],
1101 }
1102 .apply_transformation(&mut inp, &mut out)?;
1103 assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
1104 assert_eq!(out.to_string(), "9\n8\n7\nthere\noh hey\n");
1105
1106 let mut inp = example.clone();
1107 let mut out = empty.clone();
1108 DiffCommand::Insert {
1109 pos: 3,
1110 lines: vec!["oh hey", "there"],
1111 }
1112 .apply_transformation(&mut inp, &mut out)?;
1113 assert_eq!(inp.to_string(), "1\n2\n3\n");
1114 assert_eq!(out.to_string(), "9\n8\n7\n6\n5\n4\nthere\noh hey\n");
1115 DiffCommand::Insert {
1116 pos: 0,
1117 lines: vec!["boom!"],
1118 }
1119 .apply_transformation(&mut inp, &mut out)?;
1120 assert_eq!(inp.to_string(), "");
1121 assert_eq!(
1122 out.to_string(),
1123 "9\n8\n7\n6\n5\n4\nthere\noh hey\n3\n2\n1\nboom!\n"
1124 );
1125
1126 let mut inp = example.clone();
1127 let mut out = empty.clone();
1128 let r = DiffCommand::Delete {
1129 low: 100,
1130 high: 200,
1131 }
1132 .apply_transformation(&mut inp, &mut out);
1133 assert!(r.is_err());
1134 let r = DiffCommand::Delete { low: 5, high: 200 }.apply_transformation(&mut inp, &mut out);
1135 assert!(r.is_err());
1136 let r = DiffCommand::Delete { low: 0, high: 1 }.apply_transformation(&mut inp, &mut out);
1137 assert!(r.is_err());
1138 let r = DiffCommand::DeleteToEnd { low: 10 }.apply_transformation(&mut inp, &mut out);
1139 assert!(r.is_err());
1140 Ok(())
1141 }
1142
1143 #[test]
1144 fn header() -> Result<()> {
1145 fn header_from(s: &str) -> Result<([u8; 32], [u8; 32])> {
1146 let mut iter = s.lines();
1147 parse_diff_header(&mut iter)
1148 }
1149
1150 let (a,b) = header_from(
1151 "network-status-diff-version 1
1152hash B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663 F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB"
1153 )?;
1154
1155 assert_eq!(
1156 &a[..],
1157 hex::decode("B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663")?
1158 );
1159 assert_eq!(
1160 &b[..],
1161 hex::decode("F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB")?
1162 );
1163
1164 assert!(header_from("network-status-diff-version 2\n").is_err());
1165 assert!(header_from("").is_err());
1166 assert!(header_from("5,$d\n1,2d\n").is_err());
1167 assert!(header_from("network-status-diff-version 1\n").is_err());
1168 assert!(
1169 header_from(
1170 "network-status-diff-version 1
1171hash x y
11725,5d"
1173 )
1174 .is_err()
1175 );
1176 assert!(
1177 header_from(
1178 "network-status-diff-version 1
1179hash x y
11805,5d"
1181 )
1182 .is_err()
1183 );
1184 assert!(
1185 header_from(
1186 "network-status-diff-version 1
1187hash AA BB
11885,5d"
1189 )
1190 .is_err()
1191 );
1192 assert!(
1193 header_from(
1194 "network-status-diff-version 1
1195oh hello there
11965,5d"
1197 )
1198 .is_err()
1199 );
1200 assert!(header_from("network-status-diff-version 1
1201hash B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663 F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB extra").is_err());
1202
1203 Ok(())
1204 }
1205
1206 #[test]
1207 fn apply_simple() {
1208 let pre = include_str!("../testdata/consensus1.txt");
1209 let diff = include_str!("../testdata/diff1.txt");
1210 let post = include_str!("../testdata/consensus2.txt");
1211
1212 let result = apply_diff_trivial(pre, diff).unwrap();
1213 assert!(result.check_digest().is_ok());
1214 assert_eq!(result.to_string(), post);
1215 }
1216
1217 #[test]
1218 fn sort_order() -> Result<()> {
1219 fn cmds(s: &str) -> Result<Vec<DiffCommand<'_>>> {
1220 let mut out = Vec::new();
1221 for cmd in DiffCommandIter::new(s.lines()) {
1222 out.push(cmd?);
1223 }
1224 Ok(out)
1225 }
1226
1227 let _ = cmds("6,9d\n5,5d\n")?;
1228 assert!(cmds("5,5d\n6,9d\n").is_err());
1229 assert!(cmds("5,5d\n6,6d\n").is_err());
1230 assert!(cmds("5,5d\n5,6d\n").is_err());
1231
1232 Ok(())
1233 }
1234
1235 #[test]
1237 fn cons_diff() {
1238 const WORDS: &[&str] = &[
1240 "citole",
1241 "aflow",
1242 "plowfoot",
1243 "coom",
1244 "retape",
1245 "perish",
1246 "overstifle",
1247 "ramshackle",
1248 "Romeo",
1249 "alme",
1250 "expressivity",
1251 "Kieffer",
1252 "tobe",
1253 "pronucleus",
1254 "countersconce",
1255 "puli",
1256 "acupunctuate",
1257 "heterolysis",
1258 "unwattled",
1259 "bismerpund",
1260 ];
1261
1262 let rng = &mut testing_rng();
1263 let mut left = (0..1000)
1264 .map(|_| WORDS.choose(rng).unwrap().to_string() + "\n")
1265 .collect::<String>();
1266 left += "directory-signature foo bar\n";
1267 let mut right = (0..1015)
1268 .map(|_| WORDS.choose(rng).unwrap().to_string() + "\n")
1269 .collect::<String>();
1270 right += "directory-signature foo baz\n";
1271
1272 let diff = gen_cons_diff(&left, &right, DSS::None).unwrap();
1273 let check = apply_diff(&left, &diff, None, DSS::None)
1274 .unwrap()
1275 .to_string();
1276 assert_eq!(right, check);
1277 }
1278
1279 #[test]
1280 fn dot_line() {
1281 let base = "";
1282 let target = "foo\nbar\n.\nbaz\nfoo\n";
1283 assert_eq!(
1284 gen_ed_diff(base, target).unwrap_err(),
1285 GenEdDiffError::ContainsDotLine { lno: 3 },
1286 );
1287
1288 let target = "foo\nbar\n. \t \nbaz\nfoo\n";
1290 assert_eq!(
1291 gen_ed_diff(base, target).unwrap_err(),
1292 GenEdDiffError::ContainsDotLine { lno: 3 },
1293 );
1294
1295 let target = "foo\nbar\n. foo\nbaz\nfoo\n";
1297 let _ = gen_ed_diff(base, target).unwrap();
1298
1299 let base = "directory-signature foo baz\n";
1301 let target = ".foo bar\n. bar\ndirectory-signature foo baz\n";
1302 assert_eq!(
1303 gen_cons_diff(base, target, DSS::None).unwrap(),
1304 "network-status-diff-version 1\n\
1305 hash D8138DC27D9A66F5760058A6BCB71B755462B9D26B811828F124D036DE329A58 \
1306 506AC3A4407BC5305DD0D08FED3F09C2FE69847541F642A8FD13D3BD06FFE432\n\
1307 1,$d\n\
1308 0a\n\
1309 .foo bar\n\
1310 . bar\n\
1311 directory-signature foo baz\n\
1312 .\n"
1313 );
1314 }
1315
1316 #[test]
1317 fn missing_newline() {
1318 let base = "";
1319 let target = "foo\nbar\nbaz";
1320 assert_eq!(
1321 gen_ed_diff(base, target).unwrap_err(),
1322 GenEdDiffError::MissingUnixLineEnding { lno: 3 }
1323 );
1324 }
1325
1326 #[test]
1327 fn mixed_with_crlf() {
1328 let base = "";
1329 let target = "foo\r\nbar\r\nbaz\nhello\r\n";
1330 assert_eq!(
1331 gen_ed_diff(base, target).unwrap_err(),
1332 GenEdDiffError::MissingUnixLineEnding { lno: 1 }
1333 );
1334 }
1335}