Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Fuzz

permissions:
contents: read

on:
push:
branches:
- master
pull_request:
schedule:
# Daily at 03:00 UTC
- cron: '0 3 * * *'

env:
CARGO_INCREMENTAL: 0
RUST_BACKTRACE: 1
CARGO_TERM_COLOR: always
CLICOLOR: 1
CI: 1
CARGO_FUZZ_VERSION: 0.13.1

concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true

jobs:
fuzz:
runs-on: ubuntu-latest
strategy:
matrix:
target:
- patch_from_str
- patch_from_bytes
- patch_set_gitdiff
- patch_set_unidiff
- patch_set_gitdiff_bytes
- patch_set_unidiff_bytes
steps:
- uses: actions/checkout@v6

- run: rustup toolchain install nightly
- run: rustup default nightly

- uses: dtolnay/install@cargo-fuzz

- run: cargo fuzz build ${{ matrix.target }}
- name: Run fuzzer
run: cargo fuzz run ${{ matrix.target }} -- -max_total_time=$TIME
env:
TIME: ${{ github.event_name == 'schedule' && '600' || '30' }}

- uses: actions/upload-artifact@v7
if: failure()
with:
name: fuzz-artifacts-${{ matrix.target }}-${{ github.sha }}
path: fuzz/artifacts/
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ keywords = ["diff", "patch", "merge"]
categories = ["text-processing"]
rust-version = "1.85.0"
edition = "2024"
exclude = ["/fuzz/"]

[package.metadata.docs.rs]
# To build locally:
Expand Down
4 changes: 4 additions & 0 deletions fuzz/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
target
corpus
artifacts
coverage
135 changes: 135 additions & 0 deletions fuzz/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 57 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
[package]
name = "diffy-fuzz"
version = "0.0.0"
publish = false
edition = "2024"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"

[dependencies.diffy]
path = ".."
features = ["binary"]

[[bin]]
name = "patch_from_str"
path = "fuzz_targets/patch_from_str.rs"
test = false
doc = false
bench = false

[[bin]]
name = "patch_from_bytes"
path = "fuzz_targets/patch_from_bytes.rs"
test = false
doc = false
bench = false

[[bin]]
name = "patch_set_gitdiff"
path = "fuzz_targets/patch_set_gitdiff.rs"
test = false
doc = false
bench = false

[[bin]]
name = "patch_set_unidiff"
path = "fuzz_targets/patch_set_unidiff.rs"
test = false
doc = false
bench = false

[[bin]]
name = "patch_set_gitdiff_bytes"
path = "fuzz_targets/patch_set_gitdiff_bytes.rs"
test = false
doc = false
bench = false

[[bin]]
name = "patch_set_unidiff_bytes"
path = "fuzz_targets/patch_set_unidiff_bytes.rs"
test = false
doc = false
bench = false
49 changes: 49 additions & 0 deletions fuzz/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Fuzzing

Uses [cargo-fuzz] with libFuzzer.

[cargo-fuzz]: https://github.com/rust-fuzz/cargo-fuzz

## Setup

```bash
cargo +nightly install cargo-fuzz
```

## Run

```bash
# List targets
cargo +nightly fuzz list

# Run specific target (indefinitely)
cargo +nightly fuzz run patch_from_str

# Run with time limit (seconds)
cargo +nightly fuzz run patch_from_str -- -max_total_time=60

# Run all targets (quick smoke test)
for t in $(cargo +nightly fuzz list); do
cargo +nightly fuzz run $t -- -max_total_time=10
done
```

## Targets

| Target | Tests |
|---------------------------|-----------------------------------------|
| `patch_from_str` | `Patch::from_str()` |
| `patch_from_bytes` | `Patch::from_bytes()` |
| `patch_set_gitdiff` | `PatchSet::parse(..., gitdiff())` |
| `patch_set_unidiff` | `PatchSet::parse(..., unidiff())` |
| `patch_set_gitdiff_bytes` | `PatchSet::parse_bytes(..., gitdiff())` |
| `patch_set_unidiff_bytes` | `PatchSet::parse_bytes(..., unidiff())` |

## Crashes

Crash inputs are saved to `fuzz/artifacts/<target>/`.
To reproduce:

```bash
cargo +nightly fuzz run <target> fuzz/artifacts/<target>/crash-<hash>
```
8 changes: 8 additions & 0 deletions fuzz/fuzz_targets/patch_from_bytes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#![no_main]

use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
// Should never panic - only return Ok or Err
let _ = diffy::Patch::from_bytes(data);
});
8 changes: 8 additions & 0 deletions fuzz/fuzz_targets/patch_from_str.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#![no_main]

use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &str| {
// Should never panic - only return Ok or Err
let _ = diffy::Patch::from_str(data);
});
11 changes: 11 additions & 0 deletions fuzz/fuzz_targets/patch_set_gitdiff.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#![no_main]

use diffy::patch_set::{ParseOptions, PatchSet};
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &str| {
for result in PatchSet::parse(data, ParseOptions::gitdiff()) {
// Consume every item to avoid short-circuiting on first `Err`.
let _ = result;
}
});
11 changes: 11 additions & 0 deletions fuzz/fuzz_targets/patch_set_gitdiff_bytes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#![no_main]

use diffy::patch_set::{ParseOptions, PatchSet};
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
// Consume every item to avoid short-circuiting on first `Err`.
for result in PatchSet::parse_bytes(data, ParseOptions::gitdiff()) {
let _ = result;
}
});
11 changes: 11 additions & 0 deletions fuzz/fuzz_targets/patch_set_unidiff.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#![no_main]

use diffy::patch_set::{ParseOptions, PatchSet};
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &str| {
for result in PatchSet::parse(data, ParseOptions::unidiff()) {
// Consume every item to avoid short-circuiting on first `Err`.
let _ = result;
}
});
11 changes: 11 additions & 0 deletions fuzz/fuzz_targets/patch_set_unidiff_bytes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#![no_main]

use diffy::patch_set::{ParseOptions, PatchSet};
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
// Consume every item to avoid short-circuiting on first `Err`.
for result in PatchSet::parse_bytes(data, ParseOptions::unidiff()) {
let _ = result;
}
});
Loading