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
#![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)] // This can reasonably be done for explicitness
39
#![allow(clippy::uninlined_format_args)]
40
#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
41
#![allow(clippy::result_large_err)] // temporary workaround for arti#587
42
#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
43
#![allow(clippy::needless_lifetimes)] // See arti#1765
44
#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
45
#![allow(clippy::collapsible_if)] // See arti#2342
46
#![deny(clippy::unused_async)]
47
#![deny(clippy::string_slice)] // See arti#2571
48
#![allow(recursion_depth_exceeding_limit)] // arti#2715, rust/issues/159228
49
//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
50

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

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

            
62
use crate::err::GenEdDiffError;
63

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

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

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

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

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

            
100
    // Compute the hashes for the header.
101
42
    let base_signed_hash = hex::encode_upper({
102
42
        let mut h = tor_llcrypto::d::Sha3_256::new();
103
42
        h.update(base_signed);
104
42
        h.update(CONSENSUS_SIGNED_SHA3_256_HASH_TAIL);
105
42
        h.finalize()
106
    });
107
42
    let target_hash = hex::encode_upper(tor_llcrypto::d::Sha3_256::digest(target.as_bytes()));
108

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

            
128
42
    let result = format!(
129
        "network-status-diff-version 1\n\
130
        hash {base_signed_hash} {target_hash}\n\
131
        {base_lines},$d\n\
132
        {ed_diff}"
133
    );
134

            
135
    // Ensure it is valid, refuse to emit an invalid diff.
136
42
    let check = apply_diff(base, &result, None).map_err(|_| internal!("apply call failed"))?;
137
42
    if check.to_string() != target {
138
        Err(internal!("result does not match?"))?;
139
42
    }
140

            
141
42
    Ok(result)
142
42
}
143

            
144
/// Splits `input` at the first `directory-signature`.
145
42
fn split_directory_signatures(input: &str) -> Result<(&str, &str)> {
146
42
    let parse_input = ParseInput::new(input, "");
147
42
    let mut items = ItemStream::new(&parse_input);
148

            
149
    // Parse the consensus item by item until the first `directory-signature`.
150
    loop {
151
        // We only peek in order to get the proper byte offset.
152
        // This is required because doing next() and breaking in the case of
153
        // a `directory-signature` would then lead to `.byte_offset()` yielding
154
        // the start of the second signature and not the start of the first one.
155
4892
        let item = items
156
4892
            .peek_keyword()
157
4892
            .map_err(|e| ParseError::new(e, "consdiff", "", items.lno_for_error(), None))?;
158

            
159
4892
        match item {
160
4892
            Some(DIRECTORY_SIGNATURE_KEYWORD) => {
161
42
                let offset = items.byte_position();
162
42
                return Ok(input
163
42
                    .split_at_checked(offset)
164
42
                    .ok_or_else(|| internal!("Calculated an invalid offset"))?);
165
            }
166
4850
            Some(_) => {
167
4850
                // Consume the just peeked item.
168
4850
                let _ = items.next();
169
4850
            }
170
            None => {
171
                // We are finished.
172
                return Err(Error::InvalidInput(ParseError::new(
173
                    ErrorProblem::MissingItem {
174
                        keyword: DIRECTORY_SIGNATURE_KEYWORD.as_str(),
175
                    },
176
                    "consdiff",
177
                    "",
178
                    items.lno_for_error(),
179
                    None,
180
                )));
181
            }
182
        }
183
    }
184
42
}
185

            
186
/// Generates an input agnostic ed diff.
187
///
188
/// This function does the general logic of [`gen_cons_diff()`] but works in a
189
/// document agnostic fashion.
190
52
fn gen_ed_diff(base: &str, target: &str) -> std::result::Result<String, GenEdDiffError> {
191
52
    let mut result = String::new();
192

            
193
    // We use Myers' algorithm as benchmarks have shown that it provides an
194
    // equal diff size as the ctor one while keeping an acceptable performance.
195
52
    let input = InternedInput::new(base, target);
196
52
    let mut diff = Diff::compute(Algorithm::Myers, &input);
197
52
    diff.postprocess_lines(&input);
198

            
199
    // Iterate through every a hunk, with a hunk being a block of changes.
200
52
    let hunks = diff.hunks().collect::<Vec<_>>();
201
992
    for hunk in hunks.into_iter().rev() {
202
        // Format the header.
203
992
        let hunk_type = HunkType::determine(&hunk);
204
992
        match hunk_type {
205
            // No need to do +1 because append is AFTER.
206
232
            HunkType::Append => writeln!(result, "{}{hunk_type}", hunk.before.start)?,
207
            HunkType::Delete | HunkType::Change => {
208
760
                if hunk.before.start + 1 == hunk.before.end {
209
                    // +1 because 1-indexed.
210
248
                    writeln!(result, "{}{hunk_type}", hunk.before.start + 1)?;
211
                } else {
212
                    // +1 because 1-indexed; no need to do +1 on end because
213
                    // the range is inclusive.
214
512
                    writeln!(
215
512
                        result,
216
                        "{},{}{hunk_type}",
217
512
                        hunk.before.start + 1,
218
                        hunk.before.end
219
                    )?;
220
                }
221
            }
222
        }
223

            
224
        // Format the body.
225
992
        match hunk_type {
226
            HunkType::Append | HunkType::Change => {
227
772
                let range = (hunk.after.start)..(hunk.after.end);
228
772
                let tlines = range
229
3058
                    .map(|idx| {
230
2816
                        let idx = usize::try_from(idx).expect("32-bit static assertion violated?");
231
2816
                        input.interner[input.after[idx]]
232
2816
                    })
233
772
                    .collect::<Vec<_>>();
234

            
235
2802
                for (lno, line) in tlines.iter().copied().enumerate() {
236
                    // Check that all lines end with a Unix line ending.
237
2802
                    if line.ends_with("\r\n") || !line.ends_with("\n") {
238
                        // +1 because 1-indexed.
239
4
                        return Err(GenEdDiffError::MissingUnixLineEnding { lno: lno + 1 });
240
2798
                    }
241

            
242
                    // Check for lines consisting of a single dot plus trailing
243
                    // whitespace characters.  No need to bother about "\r\n",
244
                    // because we checked that one above.  Although technically
245
                    // lines such as `. \n` are possible and understood
246
                    // as part of ed diffs, they are not legal in tor netdocs, and
247
                    // we want to be more defensive here for now; if it becomes a
248
                    // problem, we may remove it later.
249
2798
                    if line.trim_end() == "." {
250
                        // +1 because 1-indexed.
251
4
                        return Err(GenEdDiffError::ContainsDotLine { lno: lno + 1 });
252
2794
                    }
253

            
254
                    // All lines are newline terminated, no need to use writeln!
255
2794
                    write!(result, "{line}")?;
256
                }
257

            
258
                // Write the terminating dot.
259
764
                writeln!(result, ".")?;
260
            }
261
220
            HunkType::Delete => {}
262
        }
263
    }
264

            
265
44
    Ok(result)
266
52
}
267

            
268
/// The operational type of the hunk.
269
#[derive(Clone, Copy, Debug, derive_more::Display)]
270
enum HunkType {
271
    /// This is a pure appending.
272
    #[display("a")]
273
    Append,
274
    /// This is a pure deletion.
275
    #[display("d")]
276
    Delete,
277
    /// This is change with potential additions and deletions.
278
    #[display("c")]
279
    Change,
280
}
281

            
282
impl HunkType {
283
    /// Determines the type of the hunk.
284
992
    fn determine(hunk: &Hunk) -> Self {
285
992
        if hunk.is_pure_insertion() {
286
232
            Self::Append
287
760
        } else if hunk.is_pure_removal() {
288
220
            Self::Delete
289
        } else {
290
540
            Self::Change
291
        }
292
992
    }
293
}
294

            
295
/// Return true if `s` looks more like a consensus diff than some other kind
296
/// of document.
297
152
pub fn looks_like_diff(s: &str) -> bool {
298
152
    s.starts_with("network-status-diff-version")
299
152
}
300

            
301
/// Apply a given diff to an input text, and return the result from applying
302
/// that diff.
303
///
304
/// This is a slow version, for testing and correctness checking.  It uses
305
/// an O(n) operation to apply diffs, and therefore runs in O(n^2) time.
306
#[cfg(any(test, feature = "slow-diff-apply"))]
307
2
pub fn apply_diff_trivial<'a>(input: &'a str, diff: &'a str) -> Result<DiffResult<'a>> {
308
2
    let mut diff_lines = diff.lines();
309
2
    let (_, d2) = parse_diff_header(&mut diff_lines)?;
310

            
311
2
    let mut diffable = DiffResult::from_str(input, d2);
312

            
313
24
    for command in DiffCommandIter::new(diff_lines) {
314
24
        command?.apply_to(&mut diffable)?;
315
    }
316

            
317
2
    Ok(diffable)
318
2
}
319

            
320
/// Apply a given diff to an input text, and return the result from applying
321
/// that diff.
322
///
323
/// If `check_digest_in` is provided, require the diff to say that it
324
/// applies to a document with the provided digest.
325
196
pub fn apply_diff<'a>(
326
196
    input: &'a str,
327
196
    diff: &'a str,
328
196
    check_digest_in: Option<[u8; 32]>,
329
196
) -> Result<DiffResult<'a>> {
330
196
    let mut input = DiffResult::from_str(input, [0; 32]);
331

            
332
196
    let mut diff_lines = diff.lines();
333
196
    let (d1, d2) = parse_diff_header(&mut diff_lines)?;
334
196
    if let Some(d_want) = check_digest_in {
335
76
        if d1 != d_want {
336
            return Err(Error::CantApply("listed digest does not match document"));
337
76
        }
338
120
    }
339

            
340
196
    let mut output = DiffResult::new(d2);
341

            
342
2576
    for command in DiffCommandIter::new(diff_lines) {
343
2576
        command?.apply_transformation(&mut input, &mut output)?;
344
    }
345

            
346
196
    output.push_reversed(&input.lines[..]);
347

            
348
196
    output.lines.reverse();
349
196
    Ok(output)
350
196
}
351

            
352
/// Given a line iterator, check to make sure the first two lines are
353
/// a valid diff header as specified in dir-spec.txt.
354
218
fn parse_diff_header<'a, I>(iter: &mut I) -> Result<([u8; 32], [u8; 32])>
355
218
where
356
218
    I: Iterator<Item = &'a str>,
357
{
358
218
    let line1 = iter.next();
359
218
    if line1 != Some("network-status-diff-version 1") {
360
6
        return Err(Error::BadDiff("unrecognized or missing header"));
361
212
    }
362
212
    let line2 = iter.next().ok_or(Error::BadDiff("header truncated"))?;
363
210
    if !line2.starts_with("hash ") {
364
2
        return Err(Error::BadDiff("missing 'hash' line"));
365
208
    }
366
208
    let elts: Vec<_> = line2.split_ascii_whitespace().collect();
367
208
    if elts.len() != 3 {
368
2
        return Err(Error::BadDiff("invalid 'hash' line"));
369
206
    }
370
206
    let d1 = hex::decode(elts[1])?;
371
202
    let d2 = hex::decode(elts[2])?;
372
202
    match (d1.try_into(), d2.try_into()) {
373
200
        (Ok(a), Ok(b)) => Ok((a, b)),
374
2
        _ => Err(Error::BadDiff("wrong digest lengths on 'hash' line")),
375
    }
376
218
}
377

            
378
/// A command that can appear in a diff.  Each command tells us to
379
/// remove zero or more lines, and insert zero or more lines in their
380
/// place.
381
///
382
/// Commands refer to lines by 1-indexed line number.
383
#[derive(Clone, Debug)]
384
enum DiffCommand<'a> {
385
    /// Remove the lines from low through high, inclusive.
386
    Delete {
387
        /// The first line to remove
388
        low: usize,
389
        /// The last line to remove
390
        high: usize,
391
    },
392
    /// Remove the lines from low through the end of the file, inclusive.
393
    DeleteToEnd {
394
        /// The first line to remove
395
        low: usize,
396
    },
397
    /// Replace the lines from low through high, inclusive, with the
398
    /// lines in 'lines'.
399
    Replace {
400
        /// The first line to replace
401
        low: usize,
402
        /// The last line to replace
403
        high: usize,
404
        /// The text to insert instead
405
        lines: Vec<&'a str>,
406
    },
407
    /// Insert the provided 'lines' after the line with index 'pos'.
408
    Insert {
409
        /// The position after which to insert the text
410
        pos: usize,
411
        /// The text to insert
412
        lines: Vec<&'a str>,
413
    },
414
}
415

            
416
/// The result of applying one or more diff commands to an input string.
417
///
418
/// It refers to lines from the diff and the input by reference, to
419
/// avoid copying.
420
#[derive(Clone, Debug)]
421
pub struct DiffResult<'a> {
422
    /// An expected digest of the output, after it has been assembled.
423
    d_post: [u8; 32],
424
    /// The lines in the output.
425
    lines: Vec<&'a str>,
426
}
427

            
428
/// A possible value for the end of a range.  It can be either a line number,
429
/// or a dollar sign indicating "end of file".
430
#[derive(Clone, Copy, Debug)]
431
enum RangeEnd {
432
    /// A line number in the file.
433
    Num(NonZeroUsize),
434
    /// A dollar sign, indicating "end of file" in a delete command.
435
    DollarSign,
436
}
437

            
438
impl FromStr for RangeEnd {
439
    type Err = Error;
440
1458
    fn from_str(s: &str) -> Result<RangeEnd> {
441
1458
        if s == "$" {
442
126
            Ok(RangeEnd::DollarSign)
443
        } else {
444
1332
            let v: NonZeroUsize = s.parse()?;
445
1330
            if v.get() == usize::MAX {
446
2
                return Err(Error::BadDiff("range cannot end at usize::MAX"));
447
1328
            }
448
1328
            Ok(RangeEnd::Num(v))
449
        }
450
1458
    }
451
}
452

            
453
impl<'a> DiffCommand<'a> {
454
    /// Transform 'target' according to the this command.
455
    ///
456
    /// Because DiffResult internally uses a vector of line, this
457
    /// implementation is potentially O(n) in the size of the input.
458
    #[cfg(any(test, feature = "slow-diff-apply"))]
459
32
    fn apply_to(&self, target: &mut DiffResult<'a>) -> Result<()> {
460
32
        match self {
461
8
            Self::Delete { low, high } => {
462
8
                target.remove_lines(*low, *high)?;
463
            }
464
4
            Self::DeleteToEnd { low } => {
465
4
                target.remove_lines(*low, target.lines.len())?;
466
            }
467
16
            Self::Replace { low, high, lines } => {
468
16
                target.remove_lines(*low, *high)?;
469
16
                target.insert_at(*low, lines)?;
470
            }
471
4
            Self::Insert { pos, lines } => {
472
                // This '+1' seems off, but it's what the spec says. I wonder
473
                // if the spec is wrong.
474
4
                target.insert_at(*pos + 1, lines)?;
475
            }
476
        };
477
32
        Ok(())
478
32
    }
479

            
480
    /// Apply this command to 'input', moving lines into 'output'.
481
    ///
482
    /// This is a more efficient algorithm, but it requires that the
483
    /// diff commands are sorted in reverse order by line
484
    /// number. (Fortunately, the Tor ed diff format guarantees this.)
485
    ///
486
    /// Before calling this method, input and output must contain the
487
    /// results of having applied the previous command in the diff.
488
    /// (When no commands have been applied, input starts out as the
489
    /// original text, and output starts out empty.)
490
    ///
491
    /// This method applies the command by copying unaffected lines
492
    /// from the _end_ of input into output, adding any lines inserted
493
    /// by this command, and finally deleting any affected lines from
494
    /// input.
495
    ///
496
    /// We build the `output` value in reverse order, and then put it
497
    /// back to normal before giving it to the user.
498
2596
    fn apply_transformation(
499
2596
        &self,
500
2596
        input: &mut DiffResult<'a>,
501
2596
        output: &mut DiffResult<'a>,
502
2596
    ) -> Result<()> {
503
2596
        if let Some(succ) = self.following_lines() {
504
2470
            if let Some(subslice) = input.lines.get(succ - 1..) {
505
2466
                // Lines from `succ` onwards are unaffected.  Copy them.
506
2466
                output.push_reversed(subslice);
507
2466
            } else {
508
                // Oops, dubious line number.
509
4
                return Err(Error::CantApply(
510
4
                    "ending line number didn't correspond to document",
511
4
                ));
512
            }
513
126
        }
514

            
515
2592
        if let Some(lines) = self.lines() {
516
1908
            // These are the lines we're inserting.
517
1908
            output.push_reversed(lines);
518
1908
        }
519

            
520
2592
        let remove = self.first_removed_line();
521
2592
        if remove == 0 || (!self.is_insert() && remove > input.lines.len()) {
522
4
            return Err(Error::CantApply(
523
4
                "starting line number didn't correspond to document",
524
4
            ));
525
2588
        }
526
2588
        input.lines.truncate(remove - 1);
527

            
528
2588
        Ok(())
529
2596
    }
530

            
531
    /// Return the lines that we should add to the output
532
2600
    fn lines(&self) -> Option<&[&'a str]> {
533
2600
        match self {
534
1916
            Self::Replace { lines, .. } | Self::Insert { lines, .. } => Some(lines.as_slice()),
535
684
            _ => None,
536
        }
537
2600
    }
538

            
539
    /// Return a mutable reference to the vector of lines we should
540
    /// add to the output.
541
2630
    fn linebuf_mut(&mut self) -> Option<&mut Vec<&'a str>> {
542
2630
        match self {
543
1926
            Self::Replace { lines, .. } | Self::Insert { lines, .. } => Some(lines),
544
704
            _ => None,
545
        }
546
2630
    }
547

            
548
    /// Return the (1-indexed) line number of the first line in the
549
    /// input that comes _after_ this command, and is not affected by it.
550
    ///
551
    /// We use this line number to know which lines we should copy.
552
5212
    fn following_lines(&self) -> Option<usize> {
553
5212
        match self {
554
3998
            Self::Delete { high, .. } | Self::Replace { high, .. } => Some(high + 1),
555
248
            Self::DeleteToEnd { .. } => None,
556
966
            Self::Insert { pos, .. } => Some(pos + 1),
557
        }
558
5212
    }
559

            
560
    /// Return the (1-indexed) line number of the first line that we
561
    /// should clear from the input when processing this command.
562
    ///
563
    /// This can be the same as following_lines(), if we shouldn't
564
    /// actually remove any lines.
565
5202
    fn first_removed_line(&self) -> usize {
566
5202
        match self {
567
1128
            Self::Delete { low, .. } => *low,
568
248
            Self::DeleteToEnd { low } => *low,
569
2860
            Self::Replace { low, .. } => *low,
570
966
            Self::Insert { pos, .. } => *pos + 1,
571
        }
572
5202
    }
573

            
574
    /// Return true if this is an Insert command.
575
2590
    fn is_insert(&self) -> bool {
576
2590
        matches!(self, Self::Insert { .. })
577
2590
    }
578

            
579
    /// Extract a single command from a line iterator that yields lines
580
    /// of the diffs.  Return None if we're at the end of the iterator.
581
2870
    fn from_line_iterator<I>(iter: &mut I) -> Result<Option<Self>>
582
2870
    where
583
2870
        I: Iterator<Item = &'a str>,
584
    {
585
2870
        let command = match iter.next() {
586
2656
            Some(s) => s,
587
214
            None => return Ok(None),
588
        };
589

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

            
593
2656
        if command.len() < 2 || !command.is_ascii() {
594
6
            return Err(Error::BadDiff("command too short"));
595
2650
        }
596

            
597
2650
        let (range, command) = command.split_at(command.len() - 1);
598
2650
        let (low, high) = if let Some((lo, hi)) = range.split_once(',') {
599
1460
            (lo.parse::<usize>()?, Some(hi.parse::<RangeEnd>()?))
600
        } else {
601
1190
            (range.parse::<usize>()?, None)
602
        };
603

            
604
2638
        if low == usize::MAX {
605
2
            return Err(Error::BadDiff("range cannot begin at usize::MAX"));
606
2636
        }
607

            
608
2636
        match (low, high) {
609
1328
            (lo, Some(RangeEnd::Num(hi))) if lo > hi.into() => {
610
2
                return Err(Error::BadDiff("mis-ordered lines in range"));
611
            }
612
2634
            (_, _) => (),
613
        }
614

            
615
2634
        let mut cmd = match (command, low, high) {
616
2634
            ("d", low, None) => Self::Delete { low, high: low },
617
446
            ("d", low, Some(RangeEnd::Num(high))) => Self::Delete {
618
446
                low,
619
446
                high: high.into(),
620
446
            },
621
124
            ("d", low, Some(RangeEnd::DollarSign)) => Self::DeleteToEnd { low },
622
1930
            ("c", low, None) => Self::Replace {
623
562
                low,
624
562
                high: low,
625
562
                lines: Vec::new(),
626
562
            },
627
878
            ("c", low, Some(RangeEnd::Num(high))) => Self::Replace {
628
878
                low,
629
878
                high: high.into(),
630
878
                lines: Vec::new(),
631
878
            },
632
488
            ("a", low, None) => Self::Insert {
633
486
                pos: low,
634
486
                lines: Vec::new(),
635
486
            },
636
4
            (_, _, _) => return Err(Error::BadDiff("can't parse command line")),
637
        };
638

            
639
2630
        if let Some(ref mut linebuf) = cmd.linebuf_mut() {
640
            // The 'c' and 'a' commands take a series of lines followed by a
641
            // line containing a period.
642
            loop {
643
9076
                match iter.next() {
644
                    None => return Err(Error::BadDiff("unterminated block to insert")),
645
9076
                    Some(".") => break,
646
7150
                    Some(line) => linebuf.push(line),
647
                }
648
            }
649
704
        }
650

            
651
2630
        Ok(Some(cmd))
652
2870
    }
653
}
654

            
655
/// Iterator that wraps a line iterator and returns a sequence of
656
/// `Result<DiffCommand>`.
657
///
658
/// This iterator forces the commands to affect the file in reverse order,
659
/// so that we can use the O(n) algorithm for applying these diffs.
660
struct DiffCommandIter<'a, I>
661
where
662
    I: Iterator<Item = &'a str>,
663
{
664
    /// The underlying iterator.
665
    iter: I,
666

            
667
    /// The 'first removed line' of the last-parsed command; used to ensure
668
    /// that commands appear in reverse order.
669
    last_cmd_first_removed: Option<usize>,
670
}
671

            
672
impl<'a, I> DiffCommandIter<'a, I>
673
where
674
    I: Iterator<Item = &'a str>,
675
{
676
    /// Construct a new DiffCommandIter wrapping `iter`.
677
206
    fn new(iter: I) -> Self {
678
206
        DiffCommandIter {
679
206
            iter,
680
206
            last_cmd_first_removed: None,
681
206
        }
682
206
    }
683
}
684

            
685
impl<'a, I> Iterator for DiffCommandIter<'a, I>
686
where
687
    I: Iterator<Item = &'a str>,
688
{
689
    type Item = Result<DiffCommand<'a>>;
690
2816
    fn next(&mut self) -> Option<Result<DiffCommand<'a>>> {
691
2816
        match DiffCommand::from_line_iterator(&mut self.iter) {
692
            Err(e) => Some(Err(e)),
693
200
            Ok(None) => None,
694
2616
            Ok(Some(c)) => match (self.last_cmd_first_removed, c.following_lines()) {
695
                (Some(_), None) => Some(Err(Error::BadDiff("misordered commands"))),
696
2410
                (Some(a), Some(b)) if a < b => Some(Err(Error::BadDiff("misordered commands"))),
697
                (_, _) => {
698
2610
                    self.last_cmd_first_removed = Some(c.first_removed_line());
699
2610
                    Some(Ok(c))
700
                }
701
            },
702
        }
703
2816
    }
704
}
705

            
706
impl<'a> DiffResult<'a> {
707
    /// Construct a new DiffResult containing the provided string
708
    /// split into lines, and an expected post-transformation digest.
709
206
    fn from_str(s: &'a str, d_post: [u8; 32]) -> Self {
710
        // As per the [netdoc syntax], newlines should be discarded and ignored.
711
        //
712
        // [netdoc syntax]: https://spec.torproject.org/dir-spec/netdoc.html#netdoc-syntax
713
206
        let lines: Vec<_> = s.lines().collect();
714

            
715
206
        DiffResult { d_post, lines }
716
206
    }
717

            
718
    /// Return a new empty DiffResult with an expected
719
    /// post-transformation digests
720
200
    fn new(d_post: [u8; 32]) -> Self {
721
200
        DiffResult {
722
200
            d_post,
723
200
            lines: Vec::new(),
724
200
        }
725
200
    }
726

            
727
    /// Put every member of `lines` at the end of this DiffResult, in
728
    /// reverse order.
729
4574
    fn push_reversed(&mut self, lines: &[&'a str]) {
730
4574
        self.lines.extend(lines.iter().rev());
731
4574
    }
732

            
733
    /// Remove the 1-indexed lines from `first` through `last` inclusive.
734
    ///
735
    /// This has to move elements around within the vector, and so it
736
    /// is potentially O(n) in its length.
737
    #[cfg(any(test, feature = "slow-diff-apply"))]
738
40
    fn remove_lines(&mut self, first: usize, last: usize) -> Result<()> {
739
40
        if first > self.lines.len() || last > self.lines.len() || first == 0 || last == 0 {
740
4
            Err(Error::CantApply("line out of range"))
741
        } else {
742
36
            let n_to_remove = last - first + 1;
743
36
            if last != self.lines.len() {
744
28
                self.lines[..].copy_within((last).., first - 1);
745
28
            }
746
36
            self.lines.truncate(self.lines.len() - n_to_remove);
747
36
            Ok(())
748
        }
749
40
    }
750

            
751
    /// Insert the provided `lines` so that they appear at 1-indexed
752
    /// position `pos`.
753
    ///
754
    /// This has to move elements around within the vector, and so it
755
    /// is potentially O(n) in its length.
756
    #[cfg(any(test, feature = "slow-diff-apply"))]
757
28
    fn insert_at(&mut self, pos: usize, lines: &[&'a str]) -> Result<()> {
758
28
        if pos > self.lines.len() + 1 || pos == 0 {
759
4
            Err(Error::CantApply("position out of range"))
760
        } else {
761
24
            let orig_len = self.lines.len();
762
24
            self.lines.resize(self.lines.len() + lines.len(), "");
763
24
            self.lines
764
24
                .copy_within(pos - 1..orig_len, pos - 1 + lines.len());
765
24
            self.lines[(pos - 1)..(pos + lines.len() - 1)].copy_from_slice(lines);
766
24
            Ok(())
767
        }
768
28
    }
769

            
770
    /// See whether the output of this diff matches the target digest.
771
    ///
772
    /// If not, return an error.
773
78
    pub fn check_digest(&self) -> Result<()> {
774
        use digest::Digest;
775
        use tor_llcrypto::d::Sha3_256;
776
78
        let mut d = Sha3_256::new();
777
368
        for line in &self.lines {
778
368
            d.update(line.as_bytes());
779
368
            d.update(b"\n");
780
368
        }
781
78
        if d.finalize() == self.d_post.into() {
782
40
            Ok(())
783
        } else {
784
38
            Err(Error::CantApply("Wrong digest after applying diff"))
785
        }
786
78
    }
787
}
788

            
789
impl<'a> Display for DiffResult<'a> {
790
212
    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
791
12558
        for elt in &self.lines {
792
12558
            writeln!(f, "{}", elt)?;
793
        }
794
212
        Ok(())
795
212
    }
796
}
797

            
798
#[cfg(test)]
799
mod test {
800
    // @@ begin test lint list maintained by maint/add_warning @@
801
    #![allow(clippy::bool_assert_comparison)]
802
    #![allow(clippy::clone_on_copy)]
803
    #![allow(clippy::dbg_macro)]
804
    #![allow(clippy::mixed_attributes_style)]
805
    #![allow(clippy::print_stderr)]
806
    #![allow(clippy::print_stdout)]
807
    #![allow(clippy::single_char_pattern)]
808
    #![allow(clippy::unwrap_used)]
809
    #![allow(clippy::unchecked_time_subtraction)]
810
    #![allow(clippy::useless_vec)]
811
    #![allow(clippy::needless_pass_by_value)]
812
    #![allow(clippy::string_slice)] // See arti#2571
813
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
814

            
815
    use rand::seq::IndexedRandom;
816
    use tor_basic_utils::test_rng::testing_rng;
817

            
818
    use super::*;
819

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

            
824
        let mut d = example.clone();
825
        d.remove_lines(5, 7)?;
826
        assert_eq!(d.to_string(), "1\n2\n3\n4\n8\n9\n");
827

            
828
        let mut d = example.clone();
829
        d.remove_lines(1, 9)?;
830
        assert_eq!(d.to_string(), "");
831

            
832
        let mut d = example.clone();
833
        d.remove_lines(1, 1)?;
834
        assert_eq!(d.to_string(), "2\n3\n4\n5\n6\n7\n8\n9\n");
835

            
836
        let mut d = example.clone();
837
        d.remove_lines(6, 9)?;
838
        assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n");
839

            
840
        let mut d = example.clone();
841
        assert!(d.remove_lines(6, 10).is_err());
842
        assert!(d.remove_lines(0, 1).is_err());
843
        assert_eq!(d.to_string(), "1\n2\n3\n4\n5\n6\n7\n8\n9\n");
844

            
845
        Ok(())
846
    }
847

            
848
    #[test]
849
    fn insert() -> Result<()> {
850
        let example = DiffResult::from_str("1\n2\n3\n4\n5\n", [0; 32]);
851
        let mut d = example.clone();
852
        d.insert_at(3, &["hello", "world"])?;
853
        assert_eq!(d.to_string(), "1\n2\nhello\nworld\n3\n4\n5\n");
854

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

            
859
        let mut d = example.clone();
860
        assert!(d.insert_at(0, &["hello", "world"]).is_err());
861
        assert!(d.insert_at(7, &["hello", "world"]).is_err());
862
        Ok(())
863
    }
864

            
865
    #[test]
866
    fn push_reversed() {
867
        let mut d = DiffResult::new([0; 32]);
868
        d.push_reversed(&["7", "8", "9"]);
869
        assert_eq!(d.to_string(), "9\n8\n7\n");
870
        d.push_reversed(&["world", "hello", ""]);
871
        assert_eq!(d.to_string(), "9\n8\n7\n\nhello\nworld\n");
872
    }
873

            
874
    #[test]
875
    fn apply_command_simple() {
876
        let example = DiffResult::from_str("a\nb\nc\nd\ne\nf\n", [0; 32]);
877

            
878
        let mut d = example.clone();
879
        assert_eq!(d.to_string(), "a\nb\nc\nd\ne\nf\n".to_string());
880
        assert!(DiffCommand::DeleteToEnd { low: 5 }.apply_to(&mut d).is_ok());
881
        assert_eq!(d.to_string(), "a\nb\nc\nd\n".to_string());
882

            
883
        let mut d = example.clone();
884
        assert!(
885
            DiffCommand::Delete { low: 3, high: 5 }
886
                .apply_to(&mut d)
887
                .is_ok()
888
        );
889
        assert_eq!(d.to_string(), "a\nb\nf\n".to_string());
890

            
891
        let mut d = example.clone();
892
        assert!(
893
            DiffCommand::Replace {
894
                low: 3,
895
                high: 5,
896
                lines: vec!["hello", "world"]
897
            }
898
            .apply_to(&mut d)
899
            .is_ok()
900
        );
901
        assert_eq!(d.to_string(), "a\nb\nhello\nworld\nf\n".to_string());
902

            
903
        let mut d = example.clone();
904
        assert!(
905
            DiffCommand::Insert {
906
                pos: 3,
907
                lines: vec!["hello", "world"]
908
            }
909
            .apply_to(&mut d)
910
            .is_ok()
911
        );
912
        assert_eq!(
913
            d.to_string(),
914
            "a\nb\nc\nhello\nworld\nd\ne\nf\n".to_string()
915
        );
916
    }
917

            
918
    #[test]
919
    fn parse_command() -> Result<()> {
920
        fn parse(s: &str) -> Result<DiffCommand<'_>> {
921
            let mut iter = s.lines();
922
            let cmd = DiffCommand::from_line_iterator(&mut iter)?;
923
            let cmd2 = DiffCommand::from_line_iterator(&mut iter)?;
924
            if cmd2.is_some() {
925
                panic!("Unexpected second command");
926
            }
927
            Ok(cmd.unwrap())
928
        }
929

            
930
        fn parse_err(s: &str) {
931
            let mut iter = s.lines();
932
            let cmd = DiffCommand::from_line_iterator(&mut iter);
933
            assert!(matches!(cmd, Err(Error::BadDiff(_))));
934
        }
935

            
936
        let p = parse("3,8d\n")?;
937
        assert!(matches!(p, DiffCommand::Delete { low: 3, high: 8 }));
938
        let p = parse("3d\n")?;
939
        assert!(matches!(p, DiffCommand::Delete { low: 3, high: 3 }));
940
        let p = parse("100,$d\n")?;
941
        assert!(matches!(p, DiffCommand::DeleteToEnd { low: 100 }));
942

            
943
        let p = parse("30,40c\nHello\nWorld\n.\n")?;
944
        assert!(matches!(
945
            p,
946
            DiffCommand::Replace {
947
                low: 30,
948
                high: 40,
949
                ..
950
            }
951
        ));
952
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
953
        let p = parse("30c\nHello\nWorld\n.\n")?;
954
        assert!(matches!(
955
            p,
956
            DiffCommand::Replace {
957
                low: 30,
958
                high: 30,
959
                ..
960
            }
961
        ));
962
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
963

            
964
        let p = parse("999a\nHello\nWorld\n.\n")?;
965
        assert!(matches!(p, DiffCommand::Insert { pos: 999, .. }));
966
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
967
        let p = parse("0a\nHello\nWorld\n.\n")?;
968
        assert!(matches!(p, DiffCommand::Insert { pos: 0, .. }));
969
        assert_eq!(p.lines(), Some(&["Hello", "World"][..]));
970

            
971
        parse_err("hello world");
972
        parse_err("\n\n");
973
        parse_err("$,5d");
974
        parse_err("5,6,8d");
975
        parse_err("8,5d");
976
        parse_err("6");
977
        parse_err("d");
978
        parse_err("-10d");
979
        parse_err("4,$c\na\n.");
980
        parse_err("foo");
981
        parse_err("5,10p");
982
        parse_err("18446744073709551615a");
983
        parse_err("1,18446744073709551615d");
984

            
985
        Ok(())
986
    }
987

            
988
    #[test]
989
    fn apply_transformation() -> Result<()> {
990
        let example = DiffResult::from_str("1\n2\n3\n4\n5\n6\n7\n8\n9\n", [0; 32]);
991
        let empty = DiffResult::new([1; 32]);
992

            
993
        let mut inp = example.clone();
994
        let mut out = empty.clone();
995
        DiffCommand::DeleteToEnd { low: 5 }.apply_transformation(&mut inp, &mut out)?;
996
        assert_eq!(inp.to_string(), "1\n2\n3\n4\n");
997
        assert_eq!(out.to_string(), "");
998

            
999
        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).unwrap();
        let check = apply_diff(&left, &diff, 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).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 }
        );
    }
}