fuzz: land the name_validation target (§3.3, wave 7)

This commit is contained in:
glm-5.3-flash committed 2026-10-10 22:08:23 +00:00
1 parent f7c1bd79b0
commit 5043f7bcd8
52 files changed
+465 -6

No files matched your search

+1
View File
@@ -5,4 +5,5 @@
pub mod arbitrary_value;
pub mod every_parse;
pub mod name_validation;
pub mod payload_codec;
+272
View File
@@ -0,0 +1,272 @@
//! Invariant logic for the `name_validation` target (docs/research/
//! fuzzing.md §3.3, `alkstore/src/validation.rs`): the entry-point
//! name-validation surface over the whole string space — no-panic on
//! any byte sequence (lossy-decoded and `arbitrary`-structured alike),
//! deterministic accept/reject (double-invocation pinned), rejection
//! only via the typed `InvalidName`/`ReservedName` taxonomy
//! (ADR-008 §4/§5), the reserved-prefix rule byte-exact and
//! leading-only with the shared/local kind split (the local kind takes
//! the non-empty rule only — a leading `__alkstore_` is legal there),
//! the error carrying the supplied name verbatim (names are not
//! trimmed), and length-independence of the verdict class. No length
//! cap is pinned (ADR-008 §4 pins non-empty + reserved-prefix only), so
//! "exactness at the cap" is pinned as: nothing at any length flips a
//! verdict — the only exact boundaries in the rule are empty↔one-char,
//! whitespace-only↔content, and the byte-exact prefix itself.
use arbitrary::{Arbitrary, Unstructured};
use alkstore::{
validate_local_name, validate_name, validate_shared_name, Error, NameKind,
RESERVED_LISTENER_RECONNECTED, RESERVED_PREFIX,
};
/// The probed length neighborhoods: the empty↔one-char boundary
/// (0↔1), the reserved-prefix length (11↔12), the Postgres identifier
/// bytes (63↔64), the target's max_len (4095↔4096), and the
/// payload-cap scale (65536). None of these is a pinned cap — the ADR
/// pins no length limit — so the invariant is that the verdict class
/// never changes with length.
const LENGTH_PROBES: &[usize] = &[0, 1, 2, 3, 11, 12, 62, 63, 64, 65, 4095, 4096, 65_536];
/// The verdict of one validation call, narrowed to the taxonomy's typed
/// rejections: `Accept`, `InvalidName { name }`, or
/// `ReservedName { name }`. Any other outcome is a contract violation
/// and panics at construction — the typed-rejection invariant.
#[derive(Debug, Clone, PartialEq, Eq)]
enum Verdict {
Accept,
InvalidName(String),
ReservedName(String),
}
pub fn fuzz_name_validation(data: &[u8]) {
let lossy = String::from_utf8_lossy(data);
assert_verdict_invariants(&lossy);
assert_verdict_battery();
assert_structured(data);
assert_length_independence();
}
/// One string across the whole invariant set: deterministic (the
/// double-invocation pin), typed-only, all three entry points agreeing,
/// and the observed verdicts matching the locally recomputed rule.
fn assert_verdict_invariants(name: &str) {
let shared = shared_verdict(name);
assert_eq!(
shared,
shared_verdict(name),
"the shared-kind verdict must be deterministic for {name:?}"
);
let local = local_verdict(name);
assert_eq!(
local,
local_verdict(name),
"the local-kind verdict must be deterministic for {name:?}"
);
assert_eq!(
shared,
kind_verdict(NameKind::Shared, name),
"`validate_shared_name` must agree with `validate_name(Shared)` for {name:?}"
);
assert_eq!(
local,
kind_verdict(NameKind::Local, name),
"`validate_local_name` must agree with `validate_name(Local)` for {name:?}"
);
assert_classification(name, &shared, &local);
}
/// The rule of record recomputed locally and asserted against the
/// observed verdicts: empty or whitespace-only → `InvalidName` on both
/// kinds (names are not trimmed — the rejection never trims the name it
/// reports); otherwise the byte-exact leading reserved prefix rejects
/// `ReservedName` on the shared kind and is legal on the local kind;
/// anything else accepts on both kinds.
fn assert_classification(name: &str, shared: &Verdict, local: &Verdict) {
if name.is_empty() || name.trim().is_empty() {
assert_eq!(
shared,
&Verdict::InvalidName(name.to_owned()),
"empty/whitespace-only must reject `InvalidName` on the shared kind for {name:?}"
);
assert_eq!(
local,
&Verdict::InvalidName(name.to_owned()),
"empty/whitespace-only must reject `InvalidName` on the local kind for {name:?}"
);
return;
}
if name.starts_with(RESERVED_PREFIX) {
assert_eq!(
shared,
&Verdict::ReservedName(name.to_owned()),
"a leading reserved prefix must reject `ReservedName` on the shared kind for {name:?} — the error carries the name verbatim"
);
assert_eq!(
local,
&Verdict::Accept,
"the local kind takes the non-empty rule only — a leading reserved prefix is legal there: {name:?}"
);
return;
}
assert_eq!(
shared,
&Verdict::Accept,
"a non-empty, non-whitespace shared name without the exact reserved prefix must accept: {name:?}"
);
assert_eq!(
local,
&Verdict::Accept,
"a non-empty, non-whitespace local name must accept: {name:?}"
);
}
/// The casing/boundary battery for the reserved-prefix rule: the rule
/// is byte-exact and leading-only, so the near-miss forms (casing
/// deviations, the missing trailing underscore, embedded and
/// whitespace-led prefix occurrences) must accept on the shared kind,
/// while the byte-exact prefix (alone, suffixed, whitespace-suffixed —
/// anything not all-whitespace) must reject.
const BATTERY: &[&str] = &[
"",
" ",
"\t\n",
"\u{a0}",
"a",
"é-😀",
"a\u{0}b",
"__alkstore_",
"__alkstore_x",
RESERVED_LISTENER_RECONNECTED,
"__alkstore_ ",
"__alkstore_x_",
"__alkstore",
"__ALKSTORE_x",
"__Alkstore_x",
"__alkStore_x",
"__alkstore_X",
"x__alkstore_y",
" __alkstore_x",
];
fn assert_verdict_battery() {
for name in BATTERY {
assert_verdict_invariants(name);
}
}
/// The structured path: `arbitrary`-derived strings plus their
/// boundary-derivative forms (leading/trailing/embedded reserved
/// prefix, whitespace-wrapped, case-shifted) through the same
/// invariant set.
fn assert_structured(data: &[u8]) {
let mut u = Unstructured::new(data);
for _ in 0..2 {
let Ok(name) = String::arbitrary(&mut u) else {
return;
};
assert_verdict_invariants(&name);
for variant in derived_variants(&name) {
assert_verdict_invariants(&variant);
}
}
}
fn derived_variants(name: &str) -> Vec<String> {
let mut variants = vec![
format!("{}{}", RESERVED_PREFIX, name),
format!("{}{}", name, RESERVED_PREFIX),
format!("x{}{}y", RESERVED_PREFIX, name),
format!(" {}", name),
format!("{} ", name),
];
variants.push(name.to_uppercase());
variants
}
/// Verdict-class length-independence: the same content class (plain,
/// whitespace-only, reserved-prefixed, casing near-miss, embedded
/// prefix) holds its verdict at every probed length. No length cap is
/// pinned — a length anywhere is not a rejection cause.
fn assert_length_independence() {
for &len in LENGTH_PROBES {
let plain = "a".repeat(len);
assert_verdict_invariants(&plain);
let ws_only = " ".repeat(len);
assert_verdict_invariants(&ws_only);
let reserved = format!("{}{}", RESERVED_PREFIX, "a".repeat(len));
assert_verdict_invariants(&reserved);
let casing = format!("__ALKSTORE_{}", "a".repeat(len));
assert_verdict_invariants(&casing);
let embedded = format!("{}{}{}", "a".repeat(len), RESERVED_PREFIX, "b".repeat(len));
assert_verdict_invariants(&embedded);
}
}
fn shared_verdict(name: &str) -> Verdict {
match validate_shared_name(name) {
Ok(()) => Verdict::Accept,
Err(Error::InvalidName { name }) => Verdict::InvalidName(name),
Err(Error::ReservedName { name }) => Verdict::ReservedName(name),
Err(other) => panic!(
"shared-name rejection must be typed `InvalidName`/`ReservedName`, got {other:?} for {name:?}"
),
}
}
fn local_verdict(name: &str) -> Verdict {
match validate_local_name(name) {
Ok(()) => Verdict::Accept,
Err(Error::InvalidName { name }) => Verdict::InvalidName(name),
Err(Error::ReservedName { name }) => Verdict::ReservedName(name),
Err(other) => panic!(
"local-name rejection must be typed `InvalidName`/`ReservedName`, got {other:?} for {name:?}"
),
}
}
fn kind_verdict(kind: NameKind, name: &str) -> Verdict {
match validate_name(kind, name) {
Ok(()) => Verdict::Accept,
Err(Error::InvalidName { name }) => Verdict::InvalidName(name),
Err(Error::ReservedName { name }) => Verdict::ReservedName(name),
Err(other) => panic!(
"name rejection must be typed `InvalidName`/`ReservedName`, got {other:?} for {name:?}"
),
}
}
#[cfg(test)]
mod corpus_replay {
use super::*;
use std::path::PathBuf;
#[test]
fn committed_corpus_replays_through_the_invariants() {
let corpus = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../corpus/name_validation");
let mut count = 0usize;
for entry in std::fs::read_dir(&corpus).expect("corpus directory is committed") {
let path = entry.expect("corpus entry readable").path();
let data = std::fs::read(&path).expect("corpus entry readable");
fuzz_name_validation(&data);
count += 1;
}
assert!(
count >= 32,
"committed seed corpus is present, found {count}"
);
}
#[test]
fn invariants_hold_without_corpus() {
fuzz_name_validation(b"");
fuzz_name_validation(b"a");
fuzz_name_validation(b" ");
fuzz_name_validation(b"__alkstore_");
fuzz_name_validation(b"__alkstore_listener_reconnected__");
fuzz_name_validation(b"__alkstore_l\xffistener");
fuzz_name_validation(&[0u8; 16]);
fuzz_name_validation(&[b'a'; 65_537]);
}
}