use keyhog_core::{
hyperscan_cache_filename, hyperscan_cache_header_is_valid, write_hyperscan_cache_header,
HYPERSCAN_CACHE_HEADER_LEN, HYPERSCAN_CACHE_MAGIC, HYPERSCAN_CACHE_VERSION,
};
use proptest::prelude::*;
fn canonical_header() -> Vec<u8> {
let mut h = Vec::new();
write_hyperscan_cache_header(&mut h);
h
}
#[test]
fn written_header_is_valid_and_canonical() {
let h = canonical_header();
assert_eq!(h.len(), HYPERSCAN_CACHE_HEADER_LEN);
assert!(hyperscan_cache_header_is_valid(&h));
assert_eq!(&h[..4], &HYPERSCAN_CACHE_MAGIC[..]);
assert_eq!(
u32::from_le_bytes([h[4], h[5], h[6], h[7]]),
HYPERSCAN_CACHE_VERSION
);
}
#[test]
fn any_single_byte_mutation_is_rejected() {
let base = canonical_header();
for pos in 0..base.len() {
for b in 0u8..=255 {
if b == base[pos] {
continue;
}
let mut m = base.clone();
m[pos] = b;
assert!(
!hyperscan_cache_header_is_valid(&m),
"mutated header (pos {pos} -> {b}) must be rejected: {m:?}"
);
}
}
}
#[test]
fn wrong_length_is_rejected() {
let base = canonical_header();
for len in 0..=(HYPERSCAN_CACHE_HEADER_LEN * 2) {
if len == HYPERSCAN_CACHE_HEADER_LEN {
continue;
}
let mut buf = base.clone();
buf.resize(len, 0u8);
assert!(
!hyperscan_cache_header_is_valid(&buf),
"header of length {len} (!= {HYPERSCAN_CACHE_HEADER_LEN}) must be rejected"
);
}
assert!(!hyperscan_cache_header_is_valid(&[]));
}
#[test]
fn filename_format_and_determinism() {
for k in ["", "abc", "0123456789abcdef", "a/b", "weird key .db"] {
let f = hyperscan_cache_filename(k);
assert_eq!(f, hyperscan_cache_filename(k), "must be deterministic");
assert!(f.starts_with("hs-"), "must carry the shared prefix: {f}");
assert!(f.ends_with(".db"), "must carry the shared suffix: {f}");
let inner = f
.strip_prefix("hs-")
.and_then(|s| s.strip_suffix(".db"))
.expect("filename must carry both affixes");
assert_eq!(inner, k, "affix strip must recover the shard key");
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(4_000))]
#[test]
fn only_the_exact_header_validates(bytes in prop::array::uniform8(any::<u8>())) {
let valid = hyperscan_cache_header_is_valid(&bytes);
let is_canonical = &bytes[..4] == &HYPERSCAN_CACHE_MAGIC[..]
&& u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]])
== HYPERSCAN_CACHE_VERSION;
prop_assert_eq!(valid, is_canonical);
}
#[test]
fn right_magic_wrong_version_is_rejected(
version in any::<u32>().prop_filter("not current", |v| *v != HYPERSCAN_CACHE_VERSION)
) {
let mut h = HYPERSCAN_CACHE_MAGIC.to_vec();
h.extend_from_slice(&version.to_le_bytes());
prop_assert!(!hyperscan_cache_header_is_valid(&h));
}
#[test]
fn distinct_shard_keys_give_distinct_filenames(
a in "[a-zA-Z0-9._/-]{0,32}",
b in "[a-zA-Z0-9._/-]{0,32}",
) {
prop_assume!(a != b);
prop_assert_ne!(hyperscan_cache_filename(&a), hyperscan_cache_filename(&b));
}
}