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
4 changes: 2 additions & 2 deletions examples/prism-core/src/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ mod tests {

#[test]
fn lowercase_normalised_correctly() {
let r_lower = parse("gahjjjkmokye4rvpzewztkh5fvi4pa3vl7gk2lfnubsgbv3pr5t4q");
let r_upper = parse("GAHJJJKMOKYE4RVPZEWZTKH5FVI4PA3VL7GK2LFNUBSGBV3PR5T4Q");
let r_lower = parse("gaycuyt553c5lhve2xpw5gmejt4bxgm7ahmjwlapzp53kjo7eiqadrsi");
let r_upper = parse("GAYCUYT553C5LHVE2XPW5GMEJT4BXGM7AHMJWLAPZP53KJO7EIQADRSI");
assert_eq!(r_lower.is_ok(), r_upper.is_ok());
}

Expand Down
1 change: 1 addition & 0 deletions examples/rust-address-fuzzer/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod mutators;

Copy link
Copy Markdown

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_random still fuzzes only random_string, while mutators/mod.rs suppresses their unused-code warning. Add a campaign that calls both truncate and pad on valid G/M seeds and passes each result to fuzz_one; otherwise this binary never exercises the new mutators.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/rust-address-fuzzer/src/main.rs` at line 1, Update the fuzz loop in
run_random to add a length-mutation campaign that uses valid G/M seeds, invokes
both mutators::truncate and mutators::pad, and passes each mutated result to
fuzz_one. Keep the existing random_string campaign intact and remove any
unused-code suppression in mutators/mod.rs that is no longer needed once these
helpers are exercised.

mod parse;
mod report;

Expand Down
215 changes: 215 additions & 0 deletions examples/rust-address-fuzzer/src/mutators/length.rs
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()
)
}
}
}
}
}
3 changes: 3 additions & 0 deletions examples/rust-address-fuzzer/src/mutators/mod.rs
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;
Loading