aboutsummaryrefslogtreecommitdiffstats
path: root/matcher/fuzz
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@elliehuxtable.com>2026-03-16 15:22:49 -0700
committerEllie Huxtable <ellie@elliehuxtable.com>2026-03-16 15:22:49 -0700
commit8f9777ce7aecfe1a163a915e3245466b9dd9ac2e (patch)
treea878a0495e5f84266b7e36918a9e1a9432b0ddd8 /matcher/fuzz
downloadatuin-8f9777ce7aecfe1a163a915e3245466b9dd9ac2e.zip
Squashed 'crates/atuin-nucleo/' content from commit 4253de9f
git-subtree-dir: crates/atuin-nucleo git-subtree-split: 4253de9faabb4e5c6d81d946a5e35a90f87347ee
Diffstat (limited to 'matcher/fuzz')
-rw-r--r--matcher/fuzz/.gitignore4
-rw-r--r--matcher/fuzz/Cargo.toml29
-rw-r--r--matcher/fuzz/fuzz_targets/fuzz_target_1.rs78
3 files changed, 111 insertions, 0 deletions
diff --git a/matcher/fuzz/.gitignore b/matcher/fuzz/.gitignore
new file mode 100644
index 00000000..1a45eee7
--- /dev/null
+++ b/matcher/fuzz/.gitignore
@@ -0,0 +1,4 @@
+target
+corpus
+artifacts
+coverage
diff --git a/matcher/fuzz/Cargo.toml b/matcher/fuzz/Cargo.toml
new file mode 100644
index 00000000..1b9d8a7f
--- /dev/null
+++ b/matcher/fuzz/Cargo.toml
@@ -0,0 +1,29 @@
+[package]
+name = "fzf_oxide-fuzz"
+version = "0.0.0"
+publish = false
+edition = "2021"
+
+[package.metadata]
+cargo-fuzz = true
+
+[dependencies]
+libfuzzer-sys = "0.4"
+arbitrary = { version = "1", features = ["derive"] }
+
+[dependencies.fzf_oxide]
+path = ".."
+
+# Prevent this from interfering with workspaces
+[workspace]
+members = ["."]
+
+[profile.release]
+debug = 1
+
+[[bin]]
+name = "fuzz_target_1"
+path = "fuzz_targets/fuzz_target_1.rs"
+test = false
+doc = false
+
diff --git a/matcher/fuzz/fuzz_targets/fuzz_target_1.rs b/matcher/fuzz/fuzz_targets/fuzz_target_1.rs
new file mode 100644
index 00000000..d9df7d36
--- /dev/null
+++ b/matcher/fuzz/fuzz_targets/fuzz_target_1.rs
@@ -0,0 +1,78 @@
+#![no_main]
+
+use fzf_oxide::{chars, Matcher, MatcherConfig, Utf32Str};
+use libfuzzer_sys::arbitrary::Arbitrary;
+use libfuzzer_sys::fuzz_target;
+
+#[derive(Arbitrary, Debug)]
+pub struct Input<'a> {
+ haystack: &'a str,
+ needle: &'a str,
+ ignore_case: bool,
+ normalize: bool,
+}
+
+fuzz_target!(|data: Input<'_>| {
+ let mut data = data;
+ let mut config = MatcherConfig::DEFAULT;
+ config.ignore_case = data.ignore_case;
+ config.normalize = data.normalize;
+ let mut matcher = Matcher::new(config);
+ let mut indices_optimal = Vec::new();
+ let mut indices_greedy = Vec::new();
+ let mut needle_buf = Vec::new();
+ let mut haystack_buf = Vec::new();
+ let normalize = |mut c: char| {
+ if config.normalize {
+ c = chars::normalize(c);
+ }
+ if config.ignore_case {
+ c = chars::to_lower_case(c);
+ }
+ c
+ };
+ let needle: String = data.needle.chars().map(normalize).collect();
+ let needle_chars: Vec<_> = needle.chars().collect();
+ let needle = Utf32Str::new(&needle, &mut needle_buf);
+ let haystack = Utf32Str::new(data.haystack, &mut haystack_buf);
+
+ let greedy_score = matcher.fuzzy_indices_greedy(haystack, needle, &mut indices_greedy);
+ if greedy_score.is_some() {
+ let match_chars: Vec<_> = indices_greedy
+ .iter()
+ .map(|&i| normalize(haystack.get(i)))
+ .collect();
+ assert_eq!(
+ match_chars, needle_chars,
+ "failed match, found {indices_greedy:?} {match_chars:?} (greedy)"
+ );
+ }
+ let optimal_score = matcher.fuzzy_indices(haystack, needle, &mut indices_optimal);
+ if optimal_score.is_some() {
+ let match_chars: Vec<_> = indices_optimal
+ .iter()
+ .map(|&i| normalize(haystack.get(i)))
+ .collect();
+ assert_eq!(
+ match_chars, needle_chars,
+ "failed match, found {indices_optimal:?} {match_chars:?}"
+ );
+ }
+ match (greedy_score, optimal_score) {
+ (None, Some(score)) => unreachable!("optimal matched {score} but greedy did not match"),
+ (Some(score), None) => unreachable!("greedy matched {score} but optimal did not match"),
+ (Some(greedy), Some(optimal)) => {
+ assert!(
+ greedy <= optimal,
+ "optimal score must be atleast the same as greedy score {greedy} {optimal}"
+ );
+ if indices_greedy == indices_optimal {
+ assert_eq!(
+ greedy, optimal,
+ "if matching same char greedy and optimal score should be identical"
+ )
+ }
+ }
+ (None, None) => (),
+ }
+});