diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 687e770..e96f667 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,3 +88,27 @@ jobs: run: cargo doc --no-deps --workspace --document-private-items env: RUSTDOCFLAGS: -D warnings + + fuzz: + name: Fuzz Tests + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Install nightly toolchain + uses: dtolnay/rust-toolchain@nightly + + - uses: Swatinem/rust-cache@v2 + + - name: Install cargo-fuzz + run: cargo install cargo-fuzz + + - name: Run fuzz tests + run: | + # Run each fuzz target for 45 seconds (total ~3 minutes for 4 targets) + for target in unified_diff_printer postprocess_heuristics comprehensive_diff diff_compute_with; do + cargo fuzz run $target --release -- -max_total_time=45 + done diff --git a/README.md b/README.md index fc09118..b1e722e 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,9 @@ The sourcecode of the helix editor. +## Testing + +`imara-diff` includes comprehensive fuzz testing using [cargo-fuzz](https://rust-fuzz.github.io/book/cargo-fuzz.html) to ensure robustness against arbitrary inputs. Fuzz tests cover all major algorithms (Myers, Histogram, MyersMinimal), postprocessing with different heuristics, and unified diff printing. See the [fuzz/README.md](fuzz/README.md) for more details on running fuzz tests locally. ## Stability Policy diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..7265389 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,184 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "cc" +version = "1.2.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "imara-diff" +version = "0.2.0" +dependencies = [ + "hashbrown", + "memchr", +] + +[[package]] +name = "imara-diff-fuzz" +version = "0.0.0" +dependencies = [ + "imara-diff", + "libfuzzer-sys", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..c742dcb --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "imara-diff-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = { version = "0.4", features = ["arbitrary-derive"] } + +[dependencies.imara-diff] +path = ".." +features = ["unified_diff"] + +[[bin]] +name = "comprehensive_diff" +path = "fuzz_targets/comprehensive_diff.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "diff_compute_with" +path = "fuzz_targets/diff_compute_with.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "postprocess_heuristics" +path = "fuzz_targets/postprocess_heuristics.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "unified_diff_printer" +path = "fuzz_targets/unified_diff_printer.rs" +test = false +doc = false +bench = false diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 0000000..dae0670 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,41 @@ +# Fuzz Testing + +This directory contains fuzz tests for imara-diff using [cargo-fuzz](https://rust-fuzz.github.io/book/cargo-fuzz.html). + +## Running Fuzz Tests + +### Prerequisites +- Nightly Rust toolchain: `rustup install nightly` +- cargo-fuzz: `cargo install cargo-fuzz` + +### Running a specific target +```bash +# Run for a specific time (e.g., 60 seconds) +cargo +nightly fuzz run comprehensive_diff -- -max_total_time=60 + +# Run with a specific number of runs +cargo +nightly fuzz run comprehensive_diff -- -runs=1000000 +``` + +### Running all targets +```bash +for target in comprehensive_diff diff_compute_with postprocess_heuristics unified_diff_printer; do + cargo +nightly fuzz run --release $target -- -max_total_time=60 +done +``` + +### Analyzing coverage +```bash +cargo +nightly fuzz coverage comprehensive_diff +``` + +## CI Integration + +Fuzz tests are automatically run in CI for 3 minutes total (45 seconds per target × 4 targets) to ensure no regressions in robustness. + +## Adding New Fuzz Targets + +1. Create a new file in `fuzz_targets/` directory +2. Add a new `[[bin]]` entry in `Cargo.toml` +3. Update the CI workflow to run the new target +4. Update this README with a description of the new target diff --git a/fuzz/fuzz_targets/comprehensive_diff.rs b/fuzz/fuzz_targets/comprehensive_diff.rs new file mode 100644 index 0000000..26642dc --- /dev/null +++ b/fuzz/fuzz_targets/comprehensive_diff.rs @@ -0,0 +1,72 @@ +#![no_main] + +use imara_diff::{Algorithm, Diff, InternedInput}; +use libfuzzer_sys::fuzz_target; + +use libfuzzer_sys::arbitrary; + +#[derive(arbitrary::Arbitrary, Debug)] +struct Input<'a> { + before: &'a [u8], + before_str: &'a str, + after: &'a [u8], + after_str: &'a str, +} + +/// Tests all three diff algorithms (Myers, Histogram, MyersMinimal) with: +/// - Computing diffs on arbitrary string inputs +/// - Postprocessing with no heuristic and line heuristic +/// - Unified diff printing +/// - Basic queries (count_additions, count_removals, is_added, is_removed) +/// - Hunks iteration +fn do_fuzz( + Input { + before, + before_str, + after, + after_str, + }: Input<'_>, +) { + // Create interned input + let input = InternedInput::new(before, after); + + // Test all three diff algorithms + for algorithm in [ + Algorithm::Histogram, + Algorithm::Myers, + Algorithm::MyersMinimal, + ] { + // Compute diff + let mut diff = Diff::compute(algorithm, &input); + + // Test basic queries + let _ = diff.count_additions(); + let _ = diff.count_removals(); + + // Test hunks iteration + for hunk in diff.hunks() { + let _ = hunk.is_pure_insertion(); + let _ = hunk.is_pure_removal(); + let _ = hunk.invert(); + } + + // Test postprocessing with no heuristic + diff.postprocess_no_heuristic(&input); + + // Test postprocessing with line heuristic + diff.postprocess_lines(&input); + } + + let input = InternedInput::new(before_str, after_str); + let mut word_input = InternedInput::default(); + let mut word_diff = Diff::default(); + + let diff = Diff::compute(Algorithm::Myers, &input); + for hunk in diff.hunks() { + hunk.latin_word_diff(&input, &mut word_input, &mut word_diff); + } +} + +fuzz_target!(|input: Input<'_>| { + do_fuzz(input); +}); diff --git a/fuzz/fuzz_targets/diff_compute_with.rs b/fuzz/fuzz_targets/diff_compute_with.rs new file mode 100644 index 0000000..c81300b --- /dev/null +++ b/fuzz/fuzz_targets/diff_compute_with.rs @@ -0,0 +1,68 @@ +#![no_main] + +use imara_diff::{Algorithm, Diff, Token}; +use libfuzzer_sys::fuzz_target; + +/// Tests the lower-level `compute_with` API that works directly with Token sequences: +/// - Creating arbitrary token sequences +/// - Computing diffs with all algorithms +/// - Querying individual token states +/// - Iterating through hunks +fn do_fuzz(data: &[u8]) { + // Test the lower-level compute_with API that works directly with tokens + if data.len() < 4 { + return; + } + + // Use first two bytes to determine before/after lengths + let before_len = (data[0] as usize % 100).min(data.len() / 2); + let after_len = (data[1] as usize % 100).min(data.len() / 2); + + // Create token sequences from remaining bytes + let mut before_tokens = Vec::new(); + let mut after_tokens = Vec::new(); + + for i in 0..before_len { + if i + 2 < data.len() { + before_tokens.push(Token::from(data[i + 2] as u32 % 256)); + } + } + + for i in 0..after_len { + if i + 2 + before_len < data.len() { + after_tokens.push(Token::from(data[i + 2 + before_len] as u32 % 256)); + } + } + + // Test all algorithms with compute_with + for algorithm in [ + Algorithm::Histogram, + Algorithm::Myers, + Algorithm::MyersMinimal, + ] { + let mut diff = Diff::default(); + diff.compute_with(algorithm, &before_tokens, &after_tokens, 256); + + // Test basic queries + let _ = diff.count_additions(); + let _ = diff.count_removals(); + + // Test is_removed and is_added for valid indices + for i in 0..before_tokens.len() as u32 { + let _ = diff.is_removed(i); + } + for i in 0..after_tokens.len() as u32 { + let _ = diff.is_added(i); + } + + // Test hunks + for hunk in diff.hunks() { + let _ = hunk.is_pure_insertion(); + let _ = hunk.is_pure_removal(); + } + } +} + +fuzz_target!(|data: &[u8]| { + do_fuzz(data); +}); diff --git a/fuzz/fuzz_targets/postprocess_heuristics.rs b/fuzz/fuzz_targets/postprocess_heuristics.rs new file mode 100644 index 0000000..5e8e01b --- /dev/null +++ b/fuzz/fuzz_targets/postprocess_heuristics.rs @@ -0,0 +1,68 @@ +#![no_main] + +use imara_diff::{Algorithm, Diff, IndentHeuristic, IndentLevel, InternedInput}; +use libfuzzer_sys::fuzz_target; + +use libfuzzer_sys::arbitrary; + +#[derive(arbitrary::Arbitrary, Debug)] +struct Input<'a> { + before: &'a str, + after: &'a str, + ident_level: u8, +} + +/// Tests postprocessing with different heuristics: +/// - No heuristic +/// - Line heuristic (default indent-based) +/// - Custom indent heuristic with different tab sizes +/// - Validates hunk ranges are valid after postprocessing +fn do_fuzz( + Input { + before, + after, + ident_level, + }: Input<'_>, +) { + let input = InternedInput::new(before, after); + + // Test with different algorithms + for algorithm in [Algorithm::Histogram, Algorithm::Myers] { + let mut diff = Diff::compute(algorithm, &input); + + // Test postprocess with no heuristic + diff.postprocess_no_heuristic(&input); + let _ = diff.count_additions(); + let _ = diff.count_removals(); + + // Test postprocess with line heuristic + let mut diff2 = Diff::compute(algorithm, &input); + diff2.postprocess_lines(&input); + let _ = diff2.count_additions(); + let _ = diff2.count_removals(); + + // Test postprocess with custom indent heuristic + let mut diff3 = Diff::compute(algorithm, &input); + diff3.postprocess_with_heuristic( + &input, + IndentHeuristic::new(|token| { + IndentLevel::for_ascii_line( + input.interner[token].as_bytes().iter().copied(), + ident_level, + ) + }), + ); + let _ = diff3.count_additions(); + let _ = diff3.count_removals(); + + // Verify hunks are valid after postprocessing + for hunk in diff.hunks() { + assert!(hunk.before.start <= hunk.before.end); + assert!(hunk.after.start <= hunk.after.end); + } + } +} + +fuzz_target!(|input: Input<'_>| { + do_fuzz(input); +}); diff --git a/fuzz/fuzz_targets/unified_diff_printer.rs b/fuzz/fuzz_targets/unified_diff_printer.rs new file mode 100644 index 0000000..1ab3a31 --- /dev/null +++ b/fuzz/fuzz_targets/unified_diff_printer.rs @@ -0,0 +1,70 @@ +#![no_main] + +use imara_diff::{Algorithm, BasicLineDiffPrinter, Diff, InternedInput, UnifiedDiffConfig}; +use libfuzzer_sys::arbitrary; +use libfuzzer_sys::fuzz_target; + +/// Valid prefixes for unified diff output lines +const VALID_DIFF_LINE_PREFIXES: [char; 4] = [' ', '+', '-', '@']; + +#[derive(arbitrary::Arbitrary, Debug)] +struct Input<'a> { + before: &'a str, + after: &'a str, + context_len: u32, +} + +/// Tests unified diff printing with: +/// - Different context lengths (0-10) +/// - Various input combinations +/// - Validates output format (lines start with ' ', '+', '-', or '@') +fn do_fuzz( + Input { + before, + after, + context_len, + }: Input<'_>, +) { + let input = InternedInput::new(before, after); + + // Test with different algorithms + for algorithm in [ + Algorithm::Histogram, + Algorithm::Myers, + Algorithm::MyersMinimal, + ] { + let mut diff = Diff::compute(algorithm, &input); + + // Postprocess before printing + diff.postprocess_lines(&input); + + // Create printer and config + let printer = BasicLineDiffPrinter(&input.interner); + let mut config = UnifiedDiffConfig::default(); + config.context_len(context_len); + + // Generate unified diff + let unified = diff.unified_diff(&printer, config, &input); + let output = unified.to_string(); + + // Basic sanity checks on output + // It should be valid UTF-8 (already guaranteed by to_string) + // Lines should start with valid diff prefixes + for line in output.lines() { + if !line.is_empty() { + let first_char = line.chars().next().unwrap(); + // Should be a valid diff line prefix + assert!( + VALID_DIFF_LINE_PREFIXES.contains(&first_char), + "Invalid diff line prefix: '{}' in line: '{}'", + first_char, + line + ); + } + } + } +} + +fuzz_target!(|input: Input<'_>| { + do_fuzz(input); +}); diff --git a/src/sink.rs b/src/sink.rs deleted file mode 100644 index 07c96b8..0000000 --- a/src/sink.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::ops::Range; - -/// Trait for processing the edit-scripts computed with [`diff`](crate::diff) -pub trait Sink: Sized { - type Out; - - /// This method is called whenever a diff [`algorithm`](crate::Algorithm) - /// finds a change between the two processed input files. - /// A change is a continuous subsequence of [tokens](crate::intern::Token) `before` that needs - /// to be replaced by a different continuous subsequence of tokens `after` to construct the second file from the first. - /// - /// These token subsequences are passed to this function in **strictly monotonically increasing order**. - /// That means that for two subsequent calls `process_change(before1, after1)` and `process_change(before2, after2)` - /// the following always holds: - /// - /// ``` no_compile - /// assert!(before1.end < before2.start); - /// assert!(after1.end < after2.start); - /// ``` - /// - /// # Parameters - /// - **`before`** - the **position** of the removed token subsequence in the original file. - /// - **`after`** - the **position** of the inserted token subsequence in the destination file. - /// - /// # Notes - //// - /// A `Sink` has no function to indicate that a section of a file remains unchanged. - /// However due to the monotonically increasing calls, implementations can easily determine - /// which subsequences remain unchanged by saving `before.end`/`after.end`. - /// The range between `before.start`/`after.end` and the previous `before.end`/`after.end` - /// is always unchanged. - fn process_change(&mut self, before: Range, after: Range); - - /// This function is called after all calls to `process_change` are complete - /// to obtain the final diff result - fn finish(self) -> Self::Out; - - /// Utility method that constructs a [`Counter`] that tracks the total number - /// of inserted and removed tokens in the changes passed to [`process_change`](crate::Sink::process_change). - fn with_counter(self) -> Counter { - Counter::new(self) - } -} - -impl, Range)> Sink for T { - type Out = (); - - fn process_change(&mut self, before: Range, after: Range) { - self(before, after) - } - - fn finish(self) -> Self::Out {} -} - -impl Sink for () { - type Out = (); - fn process_change(&mut self, _before: Range, _after: Range) {} - fn finish(self) -> Self::Out {} -} - -/// A [`Sink`] which wraps a different sink -/// and counts the number of `removed` and `inserted` [tokens](crate::intern::Token). -pub struct Counter { - /// Total number of recorded inserted [`tokens`](crate::intern::Token). - /// Computed by summing the lengths of the `after` subsequences passed to [`process_change`](crate::Sink::process_change). - pub removals: u32, - /// Total number of recorded inserted [`tokens`](crate::intern::Token). - /// Computed by summing the lengths of the `after` subsequences passed to [`process_change`](crate::Sink::process_change). - pub insertions: u32, - /// The [`Sink`] for which the counter records [`tokens`](crate::intern::Token). - /// All calls to [`process_change`](crate::Sink::process_change) are forwarded to the `sink` by the counter. - /// After [`finish`](crate::Sink::finish) is called, this field contains the output returned by the [`finish`](crate::Sink::finish) - /// method of the wrapped [`Sink`]. - pub wrapped: T, -} - -impl Counter { - pub fn new(sink: S) -> Self { - Self { - insertions: 0, - removals: 0, - wrapped: sink, - } - } -} - -impl Sink for Counter { - type Out = Counter; - fn process_change(&mut self, before: Range, after: Range) { - self.removals += before.end - before.start; - self.insertions += after.end - after.start; - self.wrapped.process_change(before, after) - } - - fn finish(self) -> Self::Out { - Counter { - removals: self.removals, - insertions: self.insertions, - wrapped: self.wrapped.finish(), - } - } -} - -impl Counter { - pub fn total(&self) -> usize { - self.insertions as usize + self.removals as usize - } -} - -impl Default for Counter<()> { - fn default() -> Self { - Counter::new(()) - } -} diff --git a/src/slider_heuristic.rs b/src/slider_heuristic.rs index a54e8c1..f3f8591 100644 --- a/src/slider_heuristic.rs +++ b/src/slider_heuristic.rs @@ -119,13 +119,14 @@ impl IndentLevel { /// # Parameters /// /// * `src` - An iterator over the bytes of the line - /// * `tab_width` - The number of spaces that a tab character represents + /// * `tab_width` - The number of spaces that a tab character represents (min is 1) /// /// # Returns /// /// The computed indentation level, or `BLANK` if the line contains only whitespace pub fn for_ascii_line(src: impl IntoIterator, tab_width: u8) -> IndentLevel { let mut indent_level = IndentLevel(0); + let tab_width = tab_width.max(1); for c in src { match c { b' ' => indent_level.0 += 1, diff --git a/src/unified_diff.rs b/src/unified_diff.rs index c975e63..725331a 100644 --- a/src/unified_diff.rs +++ b/src/unified_diff.rs @@ -222,22 +222,17 @@ pub struct UnifiedDiff<'a, P: UnifiedDiffPrinter> { impl Display for UnifiedDiff<'_, P> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let first_hunk = self.diff.hunks().next().unwrap_or_default(); - let mut pos = first_hunk - .before - .start - .saturating_sub(self.config.context_len); + let context_len = self.config.context_len.min(1024 * 1024); + let mut pos = first_hunk.before.start.saturating_sub(context_len); let mut before_context_start = pos; - let mut after_context_start = first_hunk - .after - .start - .saturating_sub(self.config.context_len); + let mut after_context_start = first_hunk.after.start.saturating_sub(context_len); let mut before_context_len = 0; let mut after_context_len = 0; let mut buffer = String::new(); for hunk in self.diff.hunks() { - if hunk.before.start - pos > 2 * self.config.context_len { + if hunk.before.start - pos > 2 * context_len { if !buffer.is_empty() { - let end = (pos + self.config.context_len).min(self.before.len() as u32); + let end = (pos + context_len).min(self.before.len() as u32); self.printer.display_header( &mut *f, before_context_start, @@ -251,9 +246,9 @@ impl Display for UnifiedDiff<'_, P> { } buffer.clear(); } - pos = hunk.before.start - self.config.context_len; + pos = hunk.before.start - context_len; before_context_start = pos; - after_context_start = hunk.after.start - self.config.context_len; + after_context_start = hunk.after.start - context_len; before_context_len = 0; after_context_len = 0; } @@ -271,7 +266,7 @@ impl Display for UnifiedDiff<'_, P> { pos = hunk.before.end; } if !buffer.is_empty() { - let end = (pos + self.config.context_len).min(self.before.len() as u32); + let end = (pos + context_len).min(self.before.len() as u32); self.printer.display_header( &mut *f, before_context_start,