1
#![cfg_attr(docsrs, feature(doc_cfg))]
2
#![doc = include_str!("../README.md")]
3
// @@ begin lint list maintained by maint/add_warning @@
4
#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
5
#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
6
#![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)] // See arti#2556
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
#![warn(clippy::or_fun_call)]
28
#![deny(clippy::print_stderr)]
29
#![deny(clippy::print_stdout)]
30
#![warn(clippy::rc_buffer)]
31
#![deny(clippy::ref_option_ref)]
32
#![warn(clippy::semicolon_if_nothing_returned)]
33
#![warn(clippy::trait_duplication_in_bounds)]
34
#![deny(clippy::unchecked_time_subtraction)]
35
#![deny(clippy::unnecessary_wraps)]
36
#![warn(clippy::unseparated_literal_suffix)]
37
#![deny(clippy::unwrap_used)]
38
#![deny(clippy::mod_module_files)]
39
#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
40
#![allow(clippy::uninlined_format_args)]
41
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
42
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
43
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
44
#![allow(clippy::needless_lifetimes)] // See arti#1765
45
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
46
#![allow(clippy::collapsible_if)] // See arti#2342
47
#![deny(clippy::unused_async)]
48
#![deny(clippy::string_slice)] // See arti#2571
49
#![allow(recursion_depth_exceeding_limit)] // arti#2715, rust/issues/159228
50
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
51

            
52
use std::fmt::{Display, Formatter, Write};
53
use std::num::NonZeroUsize;
54
use std::str::FromStr;
55

            
56
mod err;
57
use digest::Digest;
58
pub use err::Error;
59
use imara_diff::{Algorithm, Diff, Hunk, InternedInput};
60
use tor_error::{internal, into_internal};
61
use tor_netdoc::parse2::{ErrorProblem, ItemStream, KeywordRef, ParseError, ParseInput};
62

            
63
use crate::err::GenEdDiffError;
64

            
65
/// Result type used by this crate
66
type Result<T> = std::result::Result<T, Error>;
67

            
68
/// The keyword that identifies a directory signature line.
69
// TODO: We probably want this in tor-netdoc.
70
const DIRECTORY_SIGNATURE_KEYWORD: KeywordRef = KeywordRef::new_const("directory-signature");
71

            
72
/// When hashing the signed part of the consensus, append this tail to the end.
73
const CONSENSUS_SIGNED_SHA3_256_HASH_TAIL: &str = "directory-signature ";
74

            
75
// Do not compile if we cannot safely convert a u32 into a usize.
76
static_assertions::const_assert!(std::mem::size_of::<usize>() >= std::mem::size_of::<u32>());
77

            
78
/// Generates a consensus diff.
79
///
80
/// This implementation is different from the one in CTor, because it uses a
81
/// different algorithm, namely [`Algorithm::Myers`] from the [`imara_diff`]
82
/// crate, which is more efficient than CTor in terms of runtime and about as
83
/// equally efficient as CTor in output size.
84
///
85
/// The CTor implementation makes heavy use of the fact that the input is a
86
/// valid consensus and that the routers in it are ordered.  This allows for
87
/// some divide-and-conquer mechanisms and the cost of requiring more parsing.
88
///
89
/// Here, we only minimally parse the consensus, in order to only obtain the
90
/// first `directory-signature` item and to cut everything including itself off
91
/// from the input, as demanded by the specification.
92
///
93
/// All outputs of this function are guaranteed to work with this
94
/// [`apply_diff()`] implementation as a check is performed before returning,
95
/// because returning an unusable diff would be terrible.
96
39
pub fn gen_cons_diff(
97
39
    base: &str,
98
39
    target: &str,
99
39
    size_strictness: DiffSizeStrictness,
100
39
) -> Result<String> {
101
    // Throw away the signatures.
102
39
    let (base_signed, _) = split_directory_signatures(base)?;
103
175490
    let base_lines = base_signed.chars().filter(|c| *c == '\n').count() + 1;
104

            
105
    // Compute the hashes for the header.
106
39
    let base_signed_hash = hex::encode_upper({
107
39
        let mut h = tor_llcrypto::d::Sha3_256::new();
108
39
        h.update(base_signed);
109
39
        h.update(CONSENSUS_SIGNED_SHA3_256_HASH_TAIL);
110
39
        h.finalize()
111
    });
112
39
    let target_hash = hex::encode_upper(tor_llcrypto::d::Sha3_256::digest(target.as_bytes()));
113

            
114
    // Compose the result with header.
115
39
    let ed_diff = gen_ed_diff(base_signed, target).map_err(|e| match e {
116
        GenEdDiffError::MissingUnixLineEnding { lno } => Error::InvalidInput(ParseError::new(
117
            ErrorProblem::OtherBadDocument("line does not end with '\\n'"),
118
            "consdiff",
119
            "",
120
            lno,
121
            None,
122
        )),
123
        GenEdDiffError::ContainsDotLine { lno } => Error::InvalidInput(ParseError::new(
124
            ErrorProblem::OtherBadDocument("contains dotline"),
125
            "consdiff",
126
            "",
127
            lno,
128
            None,
129
        )),
130
        GenEdDiffError::Write(_) => internal!("string write was not infallible?").into(),
131
    })?;
132

            
133
39
    let result = format!(
134
        "network-status-diff-version 1\n\
135
        hash {base_signed_hash} {target_hash}\n\
136
        {base_lines},$d\n\
137
        {ed_diff}"
138
    );
139

            
140
    // Ensure it is valid, refuse to emit an invalid diff.
141
39
    let check = match apply_diff(base, &result, None, size_strictness) {
142
39
        Ok(v) => v,
143
        Err(Error::DiffTooLarge) => return Err(Error::DiffTooLarge),
144
        Err(e) => return Err(into_internal!("unable to apply generated diff")(e).into()),
145
    };
146

            
147
39
    if check.to_string() != target {
148
        Err(internal!("result does not match?"))?;
149
39
    }
150

            
151
39
    Ok(result)
152
39
}
153

            
154
/// Splits `input` at the first `directory-signature`.
155
39
fn split_directory_signatures(input: &str) -> Result<(&str, &str)> {
156
39
    let parse_input = ParseInput::new(input, "");
157
39
    let mut items = ItemStream::new(&parse_input);
158

            
159
    // Parse the consensus item by item until the first `directory-signature`.
160
    loop {
161
        // We only peek in order to get the proper byte offset.
162
        // This is required because doing next() and breaking in the case of
163
        // a `directory-signature` would then lead to `.byte_offset()` yielding
164
        // the start of the second signature and not the start of the first one.
165
3814
        let item = items
166
3814
            .peek_keyword()
167
3814
            .map_err(|e| ParseError::new(e, "consdiff", "", items.lno_for_error(), None))?;
168

            
169
3814
        match item {
170
3814
            Some(DIRECTORY_SIGNATURE_KEYWORD) => {
171
39
                let offset = items.byte_position();
172
39
                return Ok(input
173
39
                    .split_at_checked(offset)
174
39
                    .ok_or_else(|| internal!("Calculated an invalid offset"))?);
175
            }
176
3775
            Some(_) => {
177
3775
                // Consume the just peeked item.
178
3775
                let _ = items.next();
179
3775
            }
180
            None => {
181
                // We are finished.
182
                return Err(Error::InvalidInput(ParseError::new(
183
                    ErrorProblem::MissingItem {
184
                        keyword: DIRECTORY_SIGNATURE_KEYWORD.as_str(),
185
                    },
186
                    "consdiff",
187
                    "",
188
                    items.lno_for_error(),
189
                    None,
190
                )));
191
            }
192
        }
193
    }
194
39
}
195

            
196
/// Generates an input agnostic ed diff.
197
///
198
/// This function does the general logic of [`gen_cons_diff()`] but works in a
199
/// document agnostic fashion.
200
44
fn gen_ed_diff(base: &str, target: &str) -> std::result::Result<String, GenEdDiffError> {
201
44
    let mut result = String::new();
202

            
203
    // We use Myers' algorithm as benchmarks have shown that it provides an
204
    // equal diff size as the ctor one while keeping an acceptable performance.
205
44
    let input = InternedInput::new(base, target);
206
44
    let mut diff = Diff::compute(Algorithm::Myers, &input);
207
44
    diff.postprocess_lines(&input);
208

            
209
    // Iterate through every a hunk, with a hunk being a block of changes.
210
44
    let hunks = diff.hunks().collect::<Vec<_>>();
211
653
    for hunk in hunks.into_iter().rev() {
212
        // Format the header.
213
653
        let hunk_type = HunkType::determine(&hunk);
214
653
        match hunk_type {
215
            // No need to do +1 because append is AFTER.
216
144
            HunkType::Append => writeln!(result, "{}{hunk_type}", hunk.before.start)?,
217
            HunkType::Delete | HunkType::Change => {
218
509
                if hunk.before.start + 1 == hunk.before.end {
219
                    // +1 because 1-indexed.
220
139
                    writeln!(result, "{}{hunk_type}", hunk.before.start + 1)?;
221
                } else {
222
                    // +1 because 1-indexed; no need to do +1 on end because
223
                    // the range is inclusive.
224
370
                    writeln!(
225
370
                        result,
226
                        "{},{}{hunk_type}",
227
370
                        hunk.before.start + 1,
228
                        hunk.before.end
229
                    )?;
230
                }
231
            }
232
        }
233

            
234
        // Format the body.
235
653
        match hunk_type {
236
            HunkType::Append | HunkType::Change => {
237
543
                let range = (hunk.after.start)..(hunk.after.end);
238
543
                let tlines = range
239
2087
                    .map(|idx| {
240
2087
                        let idx = usize::try_from(idx).expect("32-bit static assertion violated?");
241
2087
                        input.interner[input.after[idx]]
242
2087
                    })
243
543
                    .collect::<Vec<_>>();
244

            
245
2080
                for (lno, line) in tlines.iter().copied().enumerate() {
246
                    // Check that all lines end with a Unix line ending.
247
2080
                    if line.ends_with("\r\n") || !line.ends_with("\n") {
248
                        // +1 because 1-indexed.
249
2
                        return Err(GenEdDiffError::MissingUnixLineEnding { lno: lno + 1 });
250
2078
                    }
251

            
252
                    // Check for lines consisting of a single dot plus trailing
253
                    // whitespace characters.  No need to bother about "\r\n",
254
                    // because we checked that one above.  Although technically
255
                    // lines such as `. \n` are possible and understood
256
                    // as part of ed diffs, they are not legal in tor netdocs, and
257
                    // we want to be more defensive here for now; if it becomes a
258
                    // problem, we may remove it later.
259
2078
                    if line.trim_end() == "." {
260
                        // +1 because 1-indexed.
261
2
                        return Err(GenEdDiffError::ContainsDotLine { lno: lno + 1 });
262
2076
                    }
263

            
264
                    // All lines are newline terminated, no need to use writeln!
265
2076
                    write!(result, "{line}")?;
266
                }
267

            
268
                // Write the terminating dot.
269
539
                writeln!(result, ".")?;
270
            }
271
110
            HunkType::Delete => {}
272
        }
273
    }
274

            
275
40
    Ok(result)
276
44
}
277

            
278
/// The operational type of the hunk.
279
#[derive(Clone, Copy, Debug, derive_more::Display)]
280
enum HunkType {
281
    /// This is a pure appending.
282
    #[display("a")]
283
    Append,
284
    /// This is a pure deletion.
285
    #[display("d")]
286
    Delete,
287
    /// This is change with potential additions and deletions.
288
    #[display("c")]
289
    Change,
290
}
291

            
292
impl HunkType {
293
    /// Determines the type of the hunk.
294
653
    fn determine(hunk: &Hunk) -> Self {
295
653
        if hunk.is_pure_insertion() {
296
144
            Self::Append
297
509
        } else if hunk.is_pure_removal() {
298
110
            Self::Delete
299
        } else {
300
399
            Self::Change
301
        }
302
653
    }
303
}
304

            
305
/// Return true if `s` looks more like a consensus diff than some other kind
306
/// of document.
307
148
pub fn looks_like_diff(s: &str) -> bool {
308
148
    s.starts_with("network-status-diff-version")
309
148
}
310

            
311
/// Apply a given diff to an input text, and return the result from applying
312
/// that diff.
313
///
314
/// This is a slow version, for testing and correctness checking.  It uses
315
/// an O(n) operation to apply diffs, and therefore runs in O(n^2) time.
316
#[cfg(any(test, feature = "slow-diff-apply"))]
317
1
pub fn apply_diff_trivial<'a>(input: &'a str, diff: &'a str) -> Result<DiffResult<'a>> {
318
1
    let mut diff_lines = diff.lines();
319
1
    let (_, d2) = parse_diff_header(&mut diff_lines)?;
320

            
321
1
    let mut diffable = DiffResult::from_str(input, d2);
322

            
323
12
    for command in DiffCommandIter::new(diff_lines) {
324
12
        command?.apply_to(&mut diffable)?;
325
    }
326

            
327
1
    Ok(diffable)
328
1
}
329

            
330
/// Apply a given diff to an input text, and return the result from applying
331
/// that diff.
332
///
333
/// If `check_digest_in` is provided, require the diff to say that it
334
/// applies to a document with the provided digest.
335
188
pub fn apply_diff<'a>(
336
188
    input: &'a str,
337
188
    diff: &'a str,
338
188
    check_digest_in: Option<[u8; 32]>,
339
188
    size_strictness: DiffSizeStrictness,
340
188
) -> Result<DiffResult<'a>> {
341
188
    let input_bytes = input.len();
342
188
    let diff_bytes = diff.len();
343
    // This actually counts newlines, and can be off-by-one, but that's okay.
344
188
    let diff_lines = count_nl(diff);
345

            
346
188
    let mut input = DiffResult::from_str(input, [0; 32]);
347
188
    let input_lines = input.lines.len();
348

            
349
188
    size_strictness.check(input_bytes, input_lines, diff_bytes, diff_lines)?;
350

            
351
188
    let mut diff_lines = diff.lines();
352
188
    let (d1, d2) = parse_diff_header(&mut diff_lines)?;
353
188
    if let Some(d_want) = check_digest_in {
354
74
        if d1 != d_want {
355
            return Err(Error::CantApply("listed digest does not match document"));
356
74
        }
357
114
    }
358

            
359
188
    let mut output = DiffResult::new(d2);
360

            
361
1890
    for command in DiffCommandIter::new(diff_lines) {
362
1890
        command?.apply_transformation(&mut input, &mut output)?;
363
    }
364

            
365
188
    output.push_reversed(&input.lines[..]);
366

            
367
188
    output.lines.reverse();
368
188
    Ok(output)
369
188
}
370

            
371
/// A degree of strictness to apply when checking the size of a diff versus
372
/// the size of the associated consensus.
373
#[derive(Clone, Copy, Debug)]
374
#[non_exhaustive]
375
pub enum DiffSizeStrictness {
376
    /// Do not perform any checks.
377
    None,
378

            
379
    /// Check whether the diff is reasonable to apply to the input.
380
    Apply,
381

            
382
    /// Check whether the diff is reasonable to serve as a diff against the input.
383
    ///
384
    /// This is an even stricter version of [`DiffSizeStrictness::Apply`].
385
    Generate,
386
}
387

            
388
impl DiffSizeStrictness {
389
    /// Check whether a given diff is implausibly or uselessly large in comparison with
390
    /// the input, and return an error if so.
391
188
    fn check(
392
188
        &self,
393
188
        input_bytes: usize,
394
188
        input_lines: usize,
395
188
        diff_bytes: usize,
396
188
        diff_lines: usize,
397
188
    ) -> Result<()> {
398
        use DiffSizeStrictness as S;
399
        use Error::DiffTooLarge;
400

            
401
188
        match self {
402
114
            S::None => {}
403
            S::Generate => {
404
                // If we just generated a diff that isn't smaller than the consensus we started with,
405
                // there is no point in serving it. It wouldn't save size.
406
                if diff_bytes >= input_bytes {
407
                    return Err(DiffTooLarge);
408
                }
409

            
410
                // Also, check whether the client would reject this diff when trying to apply it.
411
                S::Apply.check(input_bytes, input_lines, diff_bytes, diff_lines)?;
412
            }
413
            S::Apply => {
414
                const BYTE_THRESHOLD: usize = 64 * 1024;
415
                const LINE_THRESHOLD: usize = 1024;
416
74
                if diff_bytes >= BYTE_THRESHOLD && diff_bytes >= input_bytes.saturating_mul(2) {
417
                    return Err(DiffTooLarge);
418
74
                }
419
74
                if diff_lines >= LINE_THRESHOLD && diff_lines >= input_lines.saturating_mul(3) {
420
                    return Err(DiffTooLarge);
421
74
                }
422
            }
423
        }
424

            
425
188
        Ok(())
426
188
    }
427
}
428

            
429
/// Given a line iterator, check to make sure the first two lines are
430
/// a valid diff header as specified in dir-spec.txt.
431
199
fn parse_diff_header<'a, I>(iter: &mut I) -> Result<([u8; 32], [u8; 32])>
432
199
where
433
199
    I: Iterator<Item = &'a str>,
434
{
435
199
    let line1 = iter.next();
436
199
    if line1 != Some("network-status-diff-version 1") {
437
3
        return Err(Error::BadDiff("unrecognized or missing header"));
438
196
    }
439
196
    let line2 = iter.next().ok_or(Error::BadDiff("header truncated"))?;
440
195
    if !line2.starts_with("hash ") {
441
1
        return Err(Error::BadDiff("missing 'hash' line"));
442
194
    }
443
194
    let elts: Vec<_> = line2.split_ascii_whitespace().collect();
444
194
    if elts.len() != 3 {
445
1
        return Err(Error::BadDiff("invalid 'hash' line"));
446
193
    }
447
193
    let d1 = hex::decode(elts[1])?;
448
191
    let d2 = hex::decode(elts[2])?;
449
191
    match (d1.try_into(), d2.try_into()) {
450
190
        (Ok(a), Ok(b)) => Ok((a, b)),
451
1
        _ => Err(Error::BadDiff("wrong digest lengths on 'hash' line")),
452
    }
453
199
}
454

            
455
/// A command that can appear in a diff.  Each command tells us to
456
/// remove zero or more lines, and insert zero or more lines in their
457
/// place.
458
///
459
/// Commands refer to lines by 1-indexed line number.
460
#[derive(Clone, Debug)]
461
enum DiffCommand<'a> {
462
    /// Remove the lines from low through high, inclusive.
463
    Delete {
464
        /// The first line to remove
465
        low: usize,
466
        /// The last line to remove
467
        high: usize,
468
    },
469
    /// Remove the lines from low through the end of the file, inclusive.
470
    DeleteToEnd {
471
        /// The first line to remove
472
        low: usize,
473
    },
474
    /// Replace the lines from low through high, inclusive, with the
475
    /// lines in 'lines'.
476
    Replace {
477
        /// The first line to replace
478
        low: usize,
479
        /// The last line to replace
480
        high: usize,
481
        /// The text to insert instead
482
        lines: Vec<&'a str>,
483
    },
484
    /// Insert the provided 'lines' after the line with index 'pos'.
485
    Insert {
486
        /// The position after which to insert the text
487
        pos: usize,
488
        /// The text to insert
489
        lines: Vec<&'a str>,
490
    },
491
}
492

            
493
/// The result of applying one or more diff commands to an input string.
494
///
495
/// It refers to lines from the diff and the input by reference, to
496
/// avoid copying.
497
#[derive(Clone, Debug)]
498
pub struct DiffResult<'a> {
499
    /// An expected digest of the output, after it has been assembled.
500
    d_post: [u8; 32],
501
    /// The lines in the output.
502
    lines: Vec<&'a str>,
503
}
504

            
505
/// A possible value for the end of a range.  It can be either a line number,
506
/// or a dollar sign indicating "end of file".
507
#[derive(Clone, Copy, Debug)]
508
enum RangeEnd {
509
    /// A line number in the file.
510
    Num(NonZeroUsize),
511
    /// A dollar sign, indicating "end of file" in a delete command.
512
    DollarSign,
513
}
514

            
515
impl FromStr for RangeEnd {
516
    type Err = Error;
517
1137
    fn from_str(s: &str) -> Result<RangeEnd> {
518
1137
        if s == "$" {
519
117
            Ok(RangeEnd::DollarSign)
520
        } else {
521
1020
            let v: NonZeroUsize = s.parse()?;
522
1019
            if v.get() == usize::MAX {
523
1
                return Err(Error::BadDiff("range cannot end at usize::MAX"));
524
1018
            }
525
1018
            Ok(RangeEnd::Num(v))
526
        }
527
1137
    }
528
}
529

            
530
impl<'a> DiffCommand<'a> {
531
    /// Transform 'target' according to the this command.
532
    ///
533
    /// Because DiffResult internally uses a vector of line, this
534
    /// implementation is potentially O(n) in the size of the input.
535
    #[cfg(any(test, feature = "slow-diff-apply"))]
536
16
    fn apply_to(&self, target: &mut DiffResult<'a>) -> Result<()> {
537
16
        match self {
538
4
            Self::Delete { low, high } => {
539
4
                target.remove_lines(*low, *high)?;
540
            }
541
2
            Self::DeleteToEnd { low } => {
542
2
                target.remove_lines(*low, target.lines.len())?;
543
            }
544
8
            Self::Replace { low, high, lines } => {
545
8
                target.remove_lines(*low, *high)?;
546
8
                target.insert_at(*low, lines)?;
547
            }
548
2
            Self::Insert { pos, lines } => {
549
                // This '+1' seems off, but it's what the spec says. I wonder
550
                // if the spec is wrong.
551
2
                target.insert_at(*pos + 1, lines)?;
552
            }
553
        };
554
16
        Ok(())
555
16
    }
556

            
557
    /// Apply this command to 'input', moving lines into 'output'.
558
    ///
559
    /// This is a more efficient algorithm, but it requires that the
560
    /// diff commands are sorted in reverse order by line
561
    /// number. (Fortunately, the Tor ed diff format guarantees this.)
562
    ///
563
    /// Before calling this method, input and output must contain the
564
    /// results of having applied the previous command in the diff.
565
    /// (When no commands have been applied, input starts out as the
566
    /// original text, and output starts out empty.)
567
    ///
568
    /// This method applies the command by copying unaffected lines
569
    /// from the _end_ of input into output, adding any lines inserted
570
    /// by this command, and finally deleting any affected lines from
571
    /// input.
572
    ///
573
    /// We build the `output` value in reverse order, and then put it
574
    /// back to normal before giving it to the user.
575
1900
    fn apply_transformation(
576
1900
        &self,
577
1900
        input: &mut DiffResult<'a>,
578
1900
        output: &mut DiffResult<'a>,
579
1900
    ) -> Result<()> {
580
1900
        if let Some(succ) = self.following_lines() {
581
1783
            if let Some(subslice) = input.lines.get(succ - 1..) {
582
1781
                // Lines from `succ` onwards are unaffected.  Copy them.
583
1781
                output.push_reversed(subslice);
584
1781
            } else {
585
                // Oops, dubious line number.
586
2
                return Err(Error::CantApply(
587
2
                    "ending line number didn't correspond to document",
588
2
                ));
589
            }
590
117
        }
591

            
592
1898
        if let Some(lines) = self.lines() {
593
1448
            // These are the lines we're inserting.
594
1448
            output.push_reversed(lines);
595
1448
        }
596

            
597
1898
        let remove = self.first_removed_line();
598
1898
        if remove == 0 || (!self.is_insert() && remove > input.lines.len()) {
599
2
            return Err(Error::CantApply(
600
2
                "starting line number didn't correspond to document",
601
2
            ));
602
1896
        }
603
1896
        input.lines.truncate(remove - 1);
604

            
605
1896
        Ok(())
606
1900
    }
607

            
608
    /// Return the lines that we should add to the output
609
1902
    fn lines(&self) -> Option<&[&'a str]> {
610
1902
        match self {
611
1452
            Self::Replace { lines, .. } | Self::Insert { lines, .. } => Some(lines.as_slice()),
612
450
            _ => None,
613
        }
614
1902
    }
615

            
616
    /// Return a mutable reference to the vector of lines we should
617
    /// add to the output.
618
1917
    fn linebuf_mut(&mut self) -> Option<&mut Vec<&'a str>> {
619
1917
        match self {
620
1457
            Self::Replace { lines, .. } | Self::Insert { lines, .. } => Some(lines),
621
460
            _ => None,
622
        }
623
1917
    }
624

            
625
    /// Return the (1-indexed) line number of the first line in the
626
    /// input that comes _after_ this command, and is not affected by it.
627
    ///
628
    /// We use this line number to know which lines we should copy.
629
3810
    fn following_lines(&self) -> Option<usize> {
630
3810
        match self {
631
2947
            Self::Delete { high, .. } | Self::Replace { high, .. } => Some(high + 1),
632
232
            Self::DeleteToEnd { .. } => None,
633
631
            Self::Insert { pos, .. } => Some(pos + 1),
634
        }
635
3810
    }
636

            
637
    /// Return the (1-indexed) line number of the first line that we
638
    /// should clear from the input when processing this command.
639
    ///
640
    /// This can be the same as following_lines(), if we shouldn't
641
    /// actually remove any lines.
642
3805
    fn first_removed_line(&self) -> usize {
643
3805
        match self {
644
672
            Self::Delete { low, .. } => *low,
645
232
            Self::DeleteToEnd { low } => *low,
646
2270
            Self::Replace { low, .. } => *low,
647
631
            Self::Insert { pos, .. } => *pos + 1,
648
        }
649
3805
    }
650

            
651
    /// Return true if this is an Insert command.
652
1897
    fn is_insert(&self) -> bool {
653
1897
        matches!(self, Self::Insert { .. })
654
1897
    }
655

            
656
    /// Extract a single command from a line iterator that yields lines
657
    /// of the diffs.  Return None if we're at the end of the iterator.
658
2127
    fn from_line_iterator<I>(iter: &mut I) -> Result<Option<Self>>
659
2127
    where
660
2127
        I: Iterator<Item = &'a str>,
661
    {
662
2127
        let command = match iter.next() {
663
1930
            Some(s) => s,
664
197
            None => return Ok(None),
665
        };
666

            
667
        // `command` can be of these forms: `Rc`, `Rd`, `N,$d`, and `Na`,
668
        // where R is a range of form `N,N`, and where N is a line number.
669

            
670
1930
        if command.len() < 2 || !command.is_ascii() {
671
3
            return Err(Error::BadDiff("command too short"));
672
1927
        }
673

            
674
1927
        let (range, command) = command.split_at(command.len() - 1);
675
1927
        let (low, high) = if let Some((lo, hi)) = range.split_once(',') {
676
1138
            (lo.parse::<usize>()?, Some(hi.parse::<RangeEnd>()?))
677
        } else {
678
789
            (range.parse::<usize>()?, None)
679
        };
680

            
681
1921
        if low == usize::MAX {
682
1
            return Err(Error::BadDiff("range cannot begin at usize::MAX"));
683
1920
        }
684

            
685
1920
        match (low, high) {
686
1018
            (lo, Some(RangeEnd::Num(hi))) if lo > hi.into() => {
687
1
                return Err(Error::BadDiff("mis-ordered lines in range"));
688
            }
689
1919
            (_, _) => (),
690
        }
691

            
692
1919
        let mut cmd = match (command, low, high) {
693
1919
            ("d", low, None) => Self::Delete { low, high: low },
694
301
            ("d", low, Some(RangeEnd::Num(high))) => Self::Delete {
695
301
                low,
696
301
                high: high.into(),
697
301
            },
698
116
            ("d", low, Some(RangeEnd::DollarSign)) => Self::DeleteToEnd { low },
699
1459
            ("c", low, None) => Self::Replace {
700
425
                low,
701
425
                high: low,
702
425
                lines: Vec::new(),
703
425
            },
704
715
            ("c", low, Some(RangeEnd::Num(high))) => Self::Replace {
705
715
                low,
706
715
                high: high.into(),
707
715
                lines: Vec::new(),
708
715
            },
709
318
            ("a", low, None) => Self::Insert {
710
317
                pos: low,
711
317
                lines: Vec::new(),
712
317
            },
713
2
            (_, _, _) => return Err(Error::BadDiff("can't parse command line")),
714
        };
715

            
716
1917
        if let Some(ref mut linebuf) = cmd.linebuf_mut() {
717
            // The 'c' and 'a' commands take a series of lines followed by a
718
            // line containing a period.
719
            loop {
720
7110
                match iter.next() {
721
                    None => return Err(Error::BadDiff("unterminated block to insert")),
722
7110
                    Some(".") => break,
723
5653
                    Some(line) => linebuf.push(line),
724
                }
725
            }
726
460
        }
727

            
728
1917
        Ok(Some(cmd))
729
2127
    }
730
}
731

            
732
/// Iterator that wraps a line iterator and returns a sequence of
733
/// `Result<DiffCommand>`.
734
///
735
/// This iterator forces the commands to affect the file in reverse order,
736
/// so that we can use the O(n) algorithm for applying these diffs.
737
struct DiffCommandIter<'a, I>
738
where
739
    I: Iterator<Item = &'a str>,
740
{
741
    /// The underlying iterator.
742
    iter: I,
743

            
744
    /// The 'first removed line' of the last-parsed command; used to ensure
745
    /// that commands appear in reverse order.
746
    last_cmd_first_removed: Option<usize>,
747
}
748

            
749
impl<'a, I> DiffCommandIter<'a, I>
750
where
751
    I: Iterator<Item = &'a str>,
752
{
753
    /// Construct a new DiffCommandIter wrapping `iter`.
754
193
    fn new(iter: I) -> Self {
755
193
        DiffCommandIter {
756
193
            iter,
757
193
            last_cmd_first_removed: None,
758
193
        }
759
193
    }
760
}
761

            
762
impl<'a, I> Iterator for DiffCommandIter<'a, I>
763
where
764
    I: Iterator<Item = &'a str>,
765
{
766
    type Item = Result<DiffCommand<'a>>;
767
2100
    fn next(&mut self) -> Option<Result<DiffCommand<'a>>> {
768
2100
        match DiffCommand::from_line_iterator(&mut self.iter) {
769
            Err(e) => Some(Err(e)),
770
190
            Ok(None) => None,
771
1910
            Ok(Some(c)) => match (self.last_cmd_first_removed, c.following_lines()) {
772
                (Some(_), None) => Some(Err(Error::BadDiff("misordered commands"))),
773
1717
                (Some(a), Some(b)) if a < b => Some(Err(Error::BadDiff("misordered commands"))),
774
                (_, _) => {
775
1907
                    self.last_cmd_first_removed = Some(c.first_removed_line());
776
1907
                    Some(Ok(c))
777
                }
778
            },
779
        }
780
2100
    }
781
}
782

            
783
impl<'a> DiffResult<'a> {
784
    /// Construct a new DiffResult containing the provided string
785
    /// split into lines, and an expected post-transformation digest.
786
193
    fn from_str(s: &'a str, d_post: [u8; 32]) -> Self {
787
        // As per the [netdoc syntax], newlines should be discarded and ignored.
788
        //
789
        // [netdoc syntax]: https://spec.torproject.org/dir-spec/netdoc.html#netdoc-syntax
790
193
        let lines: Vec<_> = s.lines().collect();
791

            
792
193
        DiffResult { d_post, lines }
793
193
    }
794

            
795
    /// Return a new empty DiffResult with an expected
796
    /// post-transformation digests
797
190
    fn new(d_post: [u8; 32]) -> Self {
798
190
        DiffResult {
799
190
            d_post,
800
190
            lines: Vec::new(),
801
190
        }
802
190
    }
803

            
804
    /// Put every member of `lines` at the end of this DiffResult, in
805
    /// reverse order.
806
3419
    fn push_reversed(&mut self, lines: &[&'a str]) {
807
3419
        self.lines.extend(lines.iter().rev());
808
3419
    }
809

            
810
    /// Remove the 1-indexed lines from `first` through `last` inclusive.
811
    ///
812
    /// This has to move elements around within the vector, and so it
813
    /// is potentially O(n) in its length.
814
    #[cfg(any(test, feature = "slow-diff-apply"))]
815
20
    fn remove_lines(&mut self, first: usize, last: usize) -> Result<()> {
816
20
        if first > self.lines.len() || last > self.lines.len() || first == 0 || last == 0 {
817
2
            Err(Error::CantApply("line out of range"))
818
        } else {
819
18
            let n_to_remove = last - first + 1;
820
18
            if last != self.lines.len() {
821
14
                self.lines[..].copy_within((last).., first - 1);
822
14
            }
823
18
            self.lines.truncate(self.lines.len() - n_to_remove);
824
18
            Ok(())
825
        }
826
20
    }
827

            
828
    /// Insert the provided `lines` so that they appear at 1-indexed
829
    /// position `pos`.
830
    ///
831
    /// This has to move elements around within the vector, and so it
832
    /// is potentially O(n) in its length.
833
    #[cfg(any(test, feature = "slow-diff-apply"))]
834
14
    fn insert_at(&mut self, pos: usize, lines: &[&'a str]) -> Result<()> {
835
14
        if pos > self.lines.len() + 1 || pos == 0 {
836
2
            Err(Error::CantApply("position out of range"))
837
        } else {
838
12
            let orig_len = self.lines.len();
839
12
            self.lines.resize(self.lines.len() + lines.len(), "");
840
12
            self.lines
841
12
                .copy_within(pos - 1..orig_len, pos - 1 + lines.len());
842
12
            self.lines[(pos - 1)..(pos + lines.len() - 1)].copy_from_slice(lines);
843
12
            Ok(())
844
        }
845
14
    }
846

            
847
    /// See whether the output of this diff matches the target digest.
848
    ///
849
    /// If not, return an error.
850
75
    pub fn check_digest(&self) -> Result<()> {
851
        use digest::Digest;
852
        use tor_llcrypto::d::Sha3_256;
853
75
        let mut d = Sha3_256::new();
854
292
        for line in &self.lines {
855
292
            d.update(line.as_bytes());
856
292
            d.update(b"\n");
857
292
        }
858
75
        if d.finalize() == self.d_post.into() {
859
38
            Ok(())
860
        } else {
861
37
            Err(Error::CantApply("Wrong digest after applying diff"))
862
        }
863
75
    }
864
}
865

            
866
impl<'a> Display for DiffResult<'a> {
867
178
    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
868
10113
        for elt in &self.lines {
869
10113
            writeln!(f, "{}", elt)?;
870
        }
871
178
        Ok(())
872
178
    }
873
}
874

            
875
/// Count the number of newlines that appear in s.
876
///
877
/// This is potentially one less than the number of lines in s, if the final line is not terminated,
878
///but that's okay for our uses here.
879
188
fn count_nl(s: &str) -> usize {
880
288303
    s.bytes().filter(|b| *b == b'\n').count()
881
188
}
882

            
883
#[cfg(test)]
884
mod test {
885
    // @@ begin test lint list maintained by maint/add_warning @@
886
    #![allow(clippy::bool_assert_comparison)]
887
    #![allow(clippy::clone_on_copy)]
888
    #![allow(clippy::dbg_macro)]
889
    #![allow(clippy::mixed_attributes_style)]
890
    #![allow(clippy::print_stderr)]
891
    #![allow(clippy::print_stdout)]
892
    #![allow(clippy::single_char_pattern)]
893
    #![allow(clippy::unwrap_used)]
894
    #![allow(clippy::unchecked_time_subtraction)]
895
    #![allow(clippy::useless_vec)]
896
    #![allow(clippy::needless_pass_by_value)]
897
    #![allow(clippy::string_slice)] // See arti#2571
898
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
899

            
900
    use rand::seq::IndexedRandom;
901
    use tor_basic_utils::test_rng::testing_rng;
902

            
903
    use super::DiffSizeStrictness as DSS;
904
    use super::*;
905

            
906
    #[test]
907
    fn remove() -> Result<()> {
908
        let example = DiffResult::from_str("1\n2\n3\n4\n5\n6\n7\n8\n9\n", [0; 32]);
909

            
910
        let mut d = example.clone();
911
        d.remove_lines(5, 7)?;
912
        assert_eq!(d.to_string(), "1\n2\n3\n4\n8\n9\n");
913

            
914
        let mut d = example.clone();
915
        d.remove_lines(1, 9)?;
916
        assert_eq!(d.to_string(), "");
917

            
918
        let mut d = example.clone();
919
        d.remove_lines(1, 1)?;
920
        assert_eq!(d.to_string(), "2\n3\n4\n5\n6\n7\n8\n9\n");
921

            
922
        let mut d = example.clone();
923
        d.remove_lines(6, 9)?;
924
        assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n");
925

            
926
        let mut d = example.clone();
927
        assert!(d.remove_lines(6, 10).is_err());
928
        assert!(d.remove_lines(0, 1).is_err());
929
        assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n9\n");
930

            
931
        Ok(())
932
    }
933

            
934
    #[test]
935
    fn insert() -> Result<()> {
936
        let example = DiffResult::from_str("1\n2\n3\n4\n5\n", [0; 32]);
937
        let mut d = example.clone();
938
        d.insert_at(3, &["hello", "world"])?;
939
        assert_eq!(d.to_string(), "1\n2\nhello\nworld\n3\n4\n5\n");
940

            
941
        let mut d = example.clone();
942
        d.insert_at(6, &["hello", "world"])?;
943
        assert_eq!(d.to_string(), "1\n2\n3\n4\n5\nhello\nworld\n");
944

            
945
        let mut d = example.clone();
946
        assert!(d.insert_at(0, &["hello", "world"]).is_err());
947
        assert!(d.insert_at(7, &["hello", "world"]).is_err());
948
        Ok(())
949
    }
950

            
951
    #[test]
952
    fn push_reversed() {
953
        let mut d = DiffResult::new([0; 32]);
954
        d.push_reversed(&["7", "8", "9"]);
955
        assert_eq!(d.to_string(), "9\n8\n7\n");
956
        d.push_reversed(&["world", "hello", ""]);
957
        assert_eq!(d.to_string(), "9\n8\n7\n\nhello\nworld\n");
958
    }
959

            
960
    #[test]
961
    fn apply_command_simple() {
962
        let example = DiffResult::from_str("a\nb\nc\nd\ne\nf\n", [0; 32]);
963

            
964
        let mut d = example.clone();
965
        assert_eq!(d.to_string(), "a\nb\nc\nd\ne\nf\n".to_string());
966
        assert!(DiffCommand::DeleteToEnd { low: 5 }.apply_to(&mut d).is_ok());
967
        assert_eq!(d.to_string(), "a\nb\nc\nd\n".to_string());
968

            
969
        let mut d = example.clone();
970
        assert!(
971
            DiffCommand::Delete { low: 3, high: 5 }
972
                .apply_to(&mut d)
973
                .is_ok()
974
        );
975
        assert_eq!(d.to_string(), "a\nb\nf\n".to_string());
976

            
977
        let mut d = example.clone();
978
        assert!(
979
            DiffCommand::Replace {
980
                low: 3,
981
                high: 5,
982
                lines: vec!["hello", "world"]
983
            }
984
            .apply_to(&mut d)
985
            .is_ok()
986
        );
987
        assert_eq!(d.to_string(), "a\nb\nhello\nworld\nf\n".to_string());
988

            
989
        let mut d = example.clone();
990
        assert!(
991
            DiffCommand::Insert {
992
                pos: 3,
993
                lines: vec!["hello", "world"]
994
            }
995
            .apply_to(&mut d)
996
            .is_ok()
997
        );
998
        assert_eq!(
999
            d.to_string(),
            "a\nb\nc\nhello\nworld\nd\ne\nf\n".to_string()
        );
    }
    #[test]
    fn parse_command() -> Result<()> {
        fn parse(s: &str) -> Result<DiffCommand<'_>> {
            let mut iter = s.lines();
            let cmd = DiffCommand::from_line_iterator(&mut iter)?;
            let cmd2 = DiffCommand::from_line_iterator(&mut iter)?;
            if cmd2.is_some() {
                panic!("Unexpected second command");
            }
            Ok(cmd.unwrap())
        }
        fn parse_err(s: &str) {
            let mut iter = s.lines();
            let cmd = DiffCommand::from_line_iterator(&mut iter);
            assert!(matches!(cmd, Err(Error::BadDiff(_))));
        }
        let p = parse("3,8d\n")?;
        assert!(matches!(p, DiffCommand::Delete { low: 3, high: 8 }));
        let p = parse("3d\n")?;
        assert!(matches!(p, DiffCommand::Delete { low: 3, high: 3 }));
        let p = parse("100,$d\n")?;
        assert!(matches!(p, DiffCommand::DeleteToEnd { low: 100 }));
        let p = parse("30,40c\nHello\nWorld\n.\n")?;
        assert!(matches!(
            p,
            DiffCommand::Replace {
                low: 30,
                high: 40,
                ..
            }
        ));
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
        let p = parse("30c\nHello\nWorld\n.\n")?;
        assert!(matches!(
            p,
            DiffCommand::Replace {
                low: 30,
                high: 30,
                ..
            }
        ));
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
        let p = parse("999a\nHello\nWorld\n.\n")?;
        assert!(matches!(p, DiffCommand::Insert { pos: 999, .. }));
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
        let p = parse("0a\nHello\nWorld\n.\n")?;
        assert!(matches!(p, DiffCommand::Insert { pos: 0, .. }));
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
        parse_err("hello world");
        parse_err("\n\n");
        parse_err("$,5d");
        parse_err("5,6,8d");
        parse_err("8,5d");
        parse_err("6");
        parse_err("d");
        parse_err("-10d");
        parse_err("4,$c\na\n.");
        parse_err("foo");
        parse_err("5,10p");
        parse_err("18446744073709551615a");
        parse_err("1,18446744073709551615d");
        Ok(())
    }
    #[test]
    fn apply_transformation() -> Result<()> {
        let example = DiffResult::from_str("1\n2\n3\n4\n5\n6\n7\n8\n9\n", [0; 32]);
        let empty = DiffResult::new([1; 32]);
        let mut inp = example.clone();
        let mut out = empty.clone();
        DiffCommand::DeleteToEnd { low: 5 }.apply_transformation(&mut inp, &mut out)?;
        assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
        assert_eq!(out.to_string(), "");
        let mut inp = example.clone();
        let mut out = empty.clone();
        DiffCommand::DeleteToEnd { low: 9 }.apply_transformation(&mut inp, &mut out)?;
        assert_eq!(inp.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n");
        assert_eq!(out.to_string(), "");
        let mut inp = example.clone();
        let mut out = empty.clone();
        DiffCommand::Delete { low: 3, high: 5 }.apply_transformation(&mut inp, &mut out)?;
        assert_eq!(inp.to_string(), "1\n2\n");
        assert_eq!(out.to_string(), "9\n8\n7\n6\n");
        let mut inp = example.clone();
        let mut out = empty.clone();
        DiffCommand::Replace {
            low: 5,
            high: 6,
            lines: vec!["oh hey", "there"],
        }
        .apply_transformation(&mut inp, &mut out)?;
        assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
        assert_eq!(out.to_string(), "9\n8\n7\nthere\noh hey\n");
        let mut inp = example.clone();
        let mut out = empty.clone();
        DiffCommand::Insert {
            pos: 3,
            lines: vec!["oh hey", "there"],
        }
        .apply_transformation(&mut inp, &mut out)?;
        assert_eq!(inp.to_string(), "1\n2\n3\n");
        assert_eq!(out.to_string(), "9\n8\n7\n6\n5\n4\nthere\noh hey\n");
        DiffCommand::Insert {
            pos: 0,
            lines: vec!["boom!"],
        }
        .apply_transformation(&mut inp, &mut out)?;
        assert_eq!(inp.to_string(), "");
        assert_eq!(
            out.to_string(),
            "9\n8\n7\n6\n5\n4\nthere\noh hey\n3\n2\n1\nboom!\n"
        );
        let mut inp = example.clone();
        let mut out = empty.clone();
        let r = DiffCommand::Delete {
            low: 100,
            high: 200,
        }
        .apply_transformation(&mut inp, &mut out);
        assert!(r.is_err());
        let r = DiffCommand::Delete { low: 5, high: 200 }.apply_transformation(&mut inp, &mut out);
        assert!(r.is_err());
        let r = DiffCommand::Delete { low: 0, high: 1 }.apply_transformation(&mut inp, &mut out);
        assert!(r.is_err());
        let r = DiffCommand::DeleteToEnd { low: 10 }.apply_transformation(&mut inp, &mut out);
        assert!(r.is_err());
        Ok(())
    }
    #[test]
    fn header() -> Result<()> {
        fn header_from(s: &str) -> Result<([u8; 32], [u8; 32])> {
            let mut iter = s.lines();
            parse_diff_header(&mut iter)
        }
        let (a,b) = header_from(
            "network-status-diff-version 1
hash B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663 F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB"
        )?;
        assert_eq!(
            &a[..],
            hex::decode("B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663")?
        );
        assert_eq!(
            &b[..],
            hex::decode("F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB")?
        );
        assert!(header_from("network-status-diff-version 2\n").is_err());
        assert!(header_from("").is_err());
        assert!(header_from("5,$d\n1,2d\n").is_err());
        assert!(header_from("network-status-diff-version 1\n").is_err());
        assert!(
            header_from(
                "network-status-diff-version 1
hash x y
5,5d"
            )
            .is_err()
        );
        assert!(
            header_from(
                "network-status-diff-version 1
hash x y
5,5d"
            )
            .is_err()
        );
        assert!(
            header_from(
                "network-status-diff-version 1
hash AA BB
5,5d"
            )
            .is_err()
        );
        assert!(
            header_from(
                "network-status-diff-version 1
oh hello there
5,5d"
            )
            .is_err()
        );
        assert!(header_from("network-status-diff-version 1
hash B03DA3ACA1D3C1D083E3FF97873002416EBD81A058B406D5C5946EAB53A79663 F6789F35B6B3BA58BB23D29E53A8ED6CBB995543DBE075DD5671481C4BA677FB extra").is_err());
        Ok(())
    }
    #[test]
    fn apply_simple() {
        let pre = include_str!("../testdata/consensus1.txt");
        let diff = include_str!("../testdata/diff1.txt");
        let post = include_str!("../testdata/consensus2.txt");
        let result = apply_diff_trivial(pre, diff).unwrap();
        assert!(result.check_digest().is_ok());
        assert_eq!(result.to_string(), post);
    }
    #[test]
    fn sort_order() -> Result<()> {
        fn cmds(s: &str) -> Result<Vec<DiffCommand<'_>>> {
            let mut out = Vec::new();
            for cmd in DiffCommandIter::new(s.lines()) {
                out.push(cmd?);
            }
            Ok(out)
        }
        let _ = cmds("6,9d\n5,5d\n")?;
        assert!(cmds("5,5d\n6,9d\n").is_err());
        assert!(cmds("5,5d\n6,6d\n").is_err());
        assert!(cmds("5,5d\n5,6d\n").is_err());
        Ok(())
    }
    /// Test for cons diff using a random word generator.
    #[test]
    fn cons_diff() {
        // cat /usr/share/dict/words | sort -R | head -n 20 | sed 's/^/"/g' | sed 's/$/",/g'
        const WORDS: &[&str] = &[
            "citole",
            "aflow",
            "plowfoot",
            "coom",
            "retape",
            "perish",
            "overstifle",
            "ramshackle",
            "Romeo",
            "alme",
            "expressivity",
            "Kieffer",
            "tobe",
            "pronucleus",
            "countersconce",
            "puli",
            "acupunctuate",
            "heterolysis",
            "unwattled",
            "bismerpund",
        ];
        let rng = &mut testing_rng();
        let mut left = (0..1000)
            .map(|_| WORDS.choose(rng).unwrap().to_string() + "\n")
            .collect::<String>();
        left += "directory-signature foo bar\n";
        let mut right = (0..1015)
            .map(|_| WORDS.choose(rng).unwrap().to_string() + "\n")
            .collect::<String>();
        right += "directory-signature foo baz\n";
        let diff = gen_cons_diff(&left, &right, DSS::None).unwrap();
        let check = apply_diff(&left, &diff, None, DSS::None)
            .unwrap()
            .to_string();
        assert_eq!(right, check);
    }
    #[test]
    fn dot_line() {
        let base = "";
        let target = "foo\nbar\n.\nbaz\nfoo\n";
        assert_eq!(
            gen_ed_diff(base, target).unwrap_err(),
            GenEdDiffError::ContainsDotLine { lno: 3 },
        );
        // Also check for dot lines with trailing spaces.
        let target = "foo\nbar\n.   \t \nbaz\nfoo\n";
        assert_eq!(
            gen_ed_diff(base, target).unwrap_err(),
            GenEdDiffError::ContainsDotLine { lno: 3 },
        );
        // A line starting with a dot and not ending in WS shall be fine though.
        let target = "foo\nbar\n.   foo\nbaz\nfoo\n";
        let _ = gen_ed_diff(base, target).unwrap();
        // Use gen_cons_diff here to assume that it is actually applied.
        let base = "directory-signature foo baz\n";
        let target = ".foo bar\n. bar\ndirectory-signature foo baz\n";
        assert_eq!(
            gen_cons_diff(base, target, DSS::None).unwrap(),
            "network-status-diff-version 1\n\
            hash D8138DC27D9A66F5760058A6BCB71B755462B9D26B811828F124D036DE329A58 \
            506AC3A4407BC5305DD0D08FED3F09C2FE69847541F642A8FD13D3BD06FFE432\n\
            1,$d\n\
            0a\n\
            .foo bar\n\
            . bar\n\
            directory-signature foo baz\n\
            .\n"
        );
    }
    #[test]
    fn missing_newline() {
        let base = "";
        let target = "foo\nbar\nbaz";
        assert_eq!(
            gen_ed_diff(base, target).unwrap_err(),
            GenEdDiffError::MissingUnixLineEnding { lno: 3 }
        );
    }
    #[test]
    fn mixed_with_crlf() {
        let base = "";
        let target = "foo\r\nbar\r\nbaz\nhello\r\n";
        assert_eq!(
            gen_ed_diff(base, target).unwrap_err(),
            GenEdDiffError::MissingUnixLineEnding { lno: 1 }
        );
    }
}