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,