Skip to content

Commit 5c94f19

Browse files
committed
feat(fuzz): add fuzz targets for signature extraction and classify_span_change
- fuzz_signature: parses arbitrary strings as Rust source, extracts signature - fuzz_classify_span: feeds random diffs with random line ranges to span classifier - Promote extract_signature and classify_span_change to pub(crate) for fuzz access
1 parent ad95ca1 commit 5c94f19

6 files changed

Lines changed: 82 additions & 3 deletions

File tree

fuzz/Cargo.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ cargo-fuzz = true
1313

1414
[dependencies]
1515
libfuzzer-sys = "0.4"
16-
commitbee = { path = ".." }
16+
commitbee = { path = "..", features = ["lang-rust"] }
1717

1818
[[bin]]
1919
name = "fuzz_sanitizer"
@@ -29,3 +29,13 @@ doc = false
2929
name = "fuzz_diff_parser"
3030
path = "fuzz_targets/fuzz_diff_parser.rs"
3131
doc = false
32+
33+
[[bin]]
34+
name = "fuzz_signature"
35+
path = "fuzz_targets/fuzz_signature.rs"
36+
doc = false
37+
38+
[[bin]]
39+
name = "fuzz_classify_span"
40+
path = "fuzz_targets/fuzz_classify_span.rs"
41+
doc = false
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// SPDX-FileCopyrightText: 2026 Sephyi <me@sephy.io>
2+
//
3+
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
4+
5+
#![no_main]
6+
7+
use libfuzzer_sys::fuzz_target;
8+
9+
fuzz_target!(|data: &[u8]| {
10+
// classify_diff_span must never panic on any input
11+
if data.len() < 16 {
12+
return;
13+
}
14+
// Use first 16 bytes for line range parameters, rest as diff
15+
let new_start = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize % 1000;
16+
let new_end = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize % 1000;
17+
let old_start = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize % 1000;
18+
let old_end = u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize % 1000;
19+
if let Ok(diff) = std::str::from_utf8(&data[16..]) {
20+
let _ = commitbee::classify_diff_span(diff, new_start, new_end, old_start, old_end);
21+
}
22+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// SPDX-FileCopyrightText: 2026 Sephyi <me@sephy.io>
2+
//
3+
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
4+
5+
#![no_main]
6+
7+
use libfuzzer_sys::fuzz_target;
8+
9+
fuzz_target!(|data: &str| {
10+
// extract_rust_signature must never panic on any input
11+
let _ = commitbee::extract_rust_signature(data);
12+
});

src/lib.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,38 @@ pub fn scan_full_diff_for_secrets(diff: &str) -> Vec<services::safety::SecretMat
5555
pub fn parse_diff_hunks(diff: &str) -> Vec<services::analyzer::DiffHunk> {
5656
services::analyzer::DiffHunk::parse_from_diff(diff)
5757
}
58+
59+
/// Extract signature from Rust source code for fuzz target access.
60+
///
61+
/// Parses the source with tree-sitter Rust, finds the first top-level definition,
62+
/// and extracts its signature. Must never panic on any input.
63+
#[cfg(feature = "lang-rust")]
64+
pub fn extract_rust_signature(source: &str) -> Option<String> {
65+
use tree_sitter::Parser;
66+
let mut parser = Parser::new();
67+
if parser
68+
.set_language(&tree_sitter_rust::LANGUAGE.into())
69+
.is_err()
70+
{
71+
return None;
72+
}
73+
let tree = parser.parse(source, None)?;
74+
let root = tree.root_node();
75+
let first_child = root.child(0)?;
76+
services::analyzer::AnalyzerService::extract_signature(first_child, source)
77+
}
78+
79+
/// Classify whether a diff span contains whitespace-only changes for fuzz target access.
80+
///
81+
/// Wrapper around `ContextBuilder::classify_span_change`. Must never panic on any input.
82+
pub fn classify_diff_span(
83+
diff: &str,
84+
new_start: usize,
85+
new_end: usize,
86+
old_start: usize,
87+
old_end: usize,
88+
) -> Option<bool> {
89+
services::context::ContextBuilder::classify_span_change(
90+
diff, new_start, new_end, old_start, old_end,
91+
)
92+
}

src/services/analyzer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ impl AnalyzerService {
456456

457457
/// Extract the signature from a definition node by taking text before the body.
458458
/// Two-strategy: child_by_field_name("body") primary, BODY_NODE_KINDS fallback.
459-
fn extract_signature(node: tree_sitter::Node, source: &str) -> Option<String> {
459+
pub(crate) fn extract_signature(node: tree_sitter::Node, source: &str) -> Option<String> {
460460
let node_start = node.start_byte();
461461

462462
// Strategy 1: named "body" field (most grammars)

src/services/context.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ impl ContextBuilder {
224224
///
225225
/// Returns `None` if no changes in span, `Some(true)` if whitespace-only,
226226
/// `Some(false)` if semantic changes detected.
227-
fn classify_span_change(
227+
pub(crate) fn classify_span_change(
228228
diff: &str,
229229
new_start: usize,
230230
new_end: usize,

0 commit comments

Comments
 (0)