-
Notifications
You must be signed in to change notification settings - Fork 80
feat(rust-fuzzer): implement truncate and pad length mutators + fix broken test data #300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
codeZe-us
merged 2 commits into
Boxkit-Labs:main
from
Yinklekay:feat/length-mutators-291
Jul 28, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| mod mutators; | ||
| mod parse; | ||
| mod report; | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,215 @@ | ||
| use rand::Rng; | ||
|
|
||
| /// Base32 alphabet used by StrKey (RFC 4648 without padding). | ||
| const STRKEY_ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; | ||
|
|
||
| /// Truncates a random number of trailing characters from `addr`. | ||
| /// | ||
| /// At least 1 character (and no more than half the address length) is removed. | ||
| /// The resulting string is guaranteed to have a different length than the | ||
| /// original, so the parser must reject it. | ||
| pub fn truncate(addr: &str, rng: &mut impl Rng) -> String { | ||
| let len = addr.len(); | ||
| // Remove between 1 and max(1, len/2) trailing chars | ||
| let max_remove = (len / 2).max(1); | ||
| let remove = rng.gen_range(1..=max_remove); | ||
| let truncated_len = len.saturating_sub(remove); | ||
| addr[..truncated_len].to_string() | ||
| } | ||
|
|
||
| /// Appends random base32 characters to `addr`. | ||
| /// | ||
| /// Between 1 and 16 extra characters are added, guaranteeing the result is | ||
| /// too long for any valid Stellar address. | ||
| pub fn pad(addr: &str, rng: &mut impl Rng) -> String { | ||
| let extra = rng.gen_range(1..=16); | ||
| let suffix: String = (0..extra) | ||
| .map(|_| STRKEY_ALPHABET[rng.gen_range(0..STRKEY_ALPHABET.len())] as char) | ||
| .collect(); | ||
| format!("{addr}{suffix}") | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use rand::rngs::StdRng; | ||
| use rand::SeedableRng; | ||
|
|
||
| // Valid addresses from the spec test vectors. | ||
| const VALID_G: &str = "GAYCUYT553C5LHVE2XPW5GMEJT4BXGM7AHMJWLAPZP53KJO7EIQADRSI"; | ||
| const VALID_M: &str = "MAYCUYT553C5LHVE2XPW5GMEJT4BXGM7AHMJWLAPZP53KJO7EIQACAAAAAAAAAAAAD672"; | ||
|
|
||
| // ------------------------------------------------------------------ | ||
| // setup guard: verify base addresses are parseable first | ||
| // ------------------------------------------------------------------ | ||
|
|
||
| #[test] | ||
| fn base_addresses_are_valid() { | ||
| assert!( | ||
| prism_core::address::parse(VALID_G).is_ok(), | ||
| "VALID_G must be parseable: {VALID_G}" | ||
| ); | ||
| assert!( | ||
| prism_core::address::parse(VALID_M).is_ok(), | ||
| "VALID_M must be parseable: {VALID_M}" | ||
| ); | ||
| } | ||
|
|
||
| // ------------------------------------------------------------------ | ||
| // truncate sanity checks | ||
| // ------------------------------------------------------------------ | ||
|
|
||
| #[test] | ||
| fn truncate_produces_shorter_string() { | ||
| let mut rng = StdRng::seed_from_u64(42); | ||
| let result = truncate(VALID_G, &mut rng); | ||
| assert!( | ||
| result.len() < VALID_G.len(), | ||
| "truncated string must be shorter ({} vs {})", | ||
| result.len(), | ||
| VALID_G.len() | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn truncate_preserves_prefix() { | ||
| let mut rng = StdRng::seed_from_u64(42); | ||
| let result = truncate(VALID_G, &mut rng); | ||
| assert_eq!(&result[..1], "G", "prefix must be preserved"); | ||
| } | ||
|
|
||
| // ------------------------------------------------------------------ | ||
| // truncate: every seed must produce Err (no panic, no Ok) | ||
| // ------------------------------------------------------------------ | ||
|
|
||
| #[test] | ||
| fn truncate_g_always_err_no_panic() { | ||
| for seed in 0..200 { | ||
| let mut rng = StdRng::seed_from_u64(seed); | ||
| let result = truncate(VALID_G, &mut rng); | ||
| let parse_out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { | ||
| prism_core::address::parse(&result) | ||
| })); | ||
| match parse_out { | ||
| Ok(Err(_)) => {} // expected | ||
| Ok(Ok(addr)) => { | ||
| panic!( | ||
| "truncated G input {result:?} (len={}, seed={seed}) parsed as OK({addr:?})", | ||
| result.len() | ||
| ) | ||
| } | ||
| Err(_) => { | ||
| panic!( | ||
| "truncated G input {result:?} (len={}, seed={seed}) caused a panic", | ||
| result.len() | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn truncate_m_always_err_no_panic() { | ||
| for seed in 0..200 { | ||
| let mut rng = StdRng::seed_from_u64(seed); | ||
| let result = truncate(VALID_M, &mut rng); | ||
| let parse_out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { | ||
| prism_core::address::parse(&result) | ||
| })); | ||
| match parse_out { | ||
| Ok(Err(_)) => {} // expected | ||
| Ok(Ok(addr)) => { | ||
| panic!( | ||
| "truncated M input {result:?} (len={}, seed={seed}) parsed as OK({addr:?})", | ||
| result.len() | ||
| ) | ||
| } | ||
| Err(_) => { | ||
| panic!( | ||
| "truncated M input {result:?} (len={}, seed={seed}) caused a panic", | ||
| result.len() | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // ------------------------------------------------------------------ | ||
| // pad sanity checks | ||
| // ------------------------------------------------------------------ | ||
|
|
||
| #[test] | ||
| fn pad_produces_longer_string() { | ||
| let mut rng = StdRng::seed_from_u64(42); | ||
| let result = pad(VALID_G, &mut rng); | ||
| assert!( | ||
| result.len() > VALID_G.len(), | ||
| "padded string must be longer ({} vs {})", | ||
| result.len(), | ||
| VALID_G.len() | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn pad_preserves_prefix() { | ||
| let mut rng = StdRng::seed_from_u64(42); | ||
| let result = pad(VALID_G, &mut rng); | ||
| assert_eq!(&result[..1], "G", "prefix must be preserved"); | ||
| } | ||
|
|
||
| // ------------------------------------------------------------------ | ||
| // pad: every seed must produce Err (no panic, no Ok) | ||
| // ------------------------------------------------------------------ | ||
|
|
||
| #[test] | ||
| fn pad_g_always_err_no_panic() { | ||
| for seed in 0..200 { | ||
| let mut rng = StdRng::seed_from_u64(seed); | ||
| let result = pad(VALID_G, &mut rng); | ||
| let parse_out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { | ||
| prism_core::address::parse(&result) | ||
| })); | ||
| match parse_out { | ||
| Ok(Err(_)) => {} // expected | ||
| Ok(Ok(addr)) => { | ||
| panic!( | ||
| "padded G input {result:?} (len={}, seed={seed}) parsed as OK({addr:?})", | ||
| result.len() | ||
| ) | ||
| } | ||
| Err(_) => { | ||
| panic!( | ||
| "padded G input {result:?} (len={}, seed={seed}) caused a panic", | ||
| result.len() | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn pad_m_always_err_no_panic() { | ||
| for seed in 0..200 { | ||
| let mut rng = StdRng::seed_from_u64(seed); | ||
| let result = pad(VALID_M, &mut rng); | ||
| let parse_out = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { | ||
| prism_core::address::parse(&result) | ||
| })); | ||
| match parse_out { | ||
| Ok(Err(_)) => {} // expected | ||
| Ok(Ok(addr)) => { | ||
| panic!( | ||
| "padded M input {result:?} (len={}, seed={seed}) parsed as OK({addr:?})", | ||
| result.len() | ||
| ) | ||
| } | ||
| Err(_) => { | ||
| panic!( | ||
| "padded M input {result:?} (len={}, seed={seed}) caused a panic", | ||
| result.len() | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| // Functions are exported for external use; unused in this binary crate. | ||
| #![allow(dead_code)] | ||
| pub mod length; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Actually feed length mutations into the fuzz loop.
Declaring the module does not register either helper:
run_randomstill fuzzes onlyrandom_string, whilemutators/mod.rssuppresses their unused-code warning. Add a campaign that calls bothtruncateandpadon valid G/M seeds and passes each result tofuzz_one; otherwise this binary never exercises the new mutators.🤖 Prompt for AI Agents