use keyhog_core::{
hyperscan_cache_filename, hyperscan_cache_header_is_valid, write_hyperscan_cache_header,
HYPERSCAN_CACHE_FILE_BYTES, HYPERSCAN_CACHE_HEADER_LEN, HYPERSCAN_CACHE_MAGIC,
HYPERSCAN_CACHE_VERSION,
};
#[test]
fn constants_have_exact_documented_values() {
assert_eq!(HYPERSCAN_CACHE_MAGIC, b"KHHS");
assert_eq!(*HYPERSCAN_CACHE_MAGIC, [0x4B, 0x48, 0x48, 0x53]);
assert_eq!(HYPERSCAN_CACHE_VERSION, 2_u32);
assert_eq!(HYPERSCAN_CACHE_HEADER_LEN, 8_usize);
assert_eq!(HYPERSCAN_CACHE_FILE_BYTES, 128 * 1024 * 1024);
assert_eq!(HYPERSCAN_CACHE_FILE_BYTES, 134_217_728_u64);
}
#[test]
fn header_len_is_magic_plus_version_width() {
assert_eq!(
HYPERSCAN_CACHE_HEADER_LEN,
HYPERSCAN_CACHE_MAGIC.len() + std::mem::size_of::<u32>()
);
assert_eq!(HYPERSCAN_CACHE_HEADER_LEN, 8);
}
#[test]
fn write_emits_exact_header_bytes() {
let mut out: Vec<u8> = Vec::new();
write_hyperscan_cache_header(&mut out);
assert_eq!(out.len(), 8);
assert_eq!(out, vec![0x4B, 0x48, 0x48, 0x53, 0x02, 0x00, 0x00, 0x00]);
}
#[test]
fn written_header_round_trips_valid() {
let mut out: Vec<u8> = Vec::new();
write_hyperscan_cache_header(&mut out);
assert!(hyperscan_cache_header_is_valid(&out));
assert_eq!(hyperscan_cache_header_is_valid(&out), true);
}
#[test]
fn write_appends_after_existing_bytes() {
let mut out: Vec<u8> = vec![0xAA, 0xBB];
write_hyperscan_cache_header(&mut out);
assert_eq!(out.len(), 10);
assert_eq!(&out[..2], &[0xAA, 0xBB]);
assert_eq!(&out[2..], &[0x4B, 0x48, 0x48, 0x53, 0x02, 0x00, 0x00, 0x00]);
let header_tail = out[2..].to_vec();
assert_eq!(hyperscan_cache_header_is_valid(&header_tail), true);
}
#[test]
fn wrong_magic_is_rejected() {
let bad: [u8; 8] = [0x4B, 0x48, 0x48, 0x54, 0x02, 0x00, 0x00, 0x00]; assert_eq!(hyperscan_cache_header_is_valid(&bad), false);
}
#[test]
fn foreign_magic_is_rejected() {
let bad: [u8; 8] = [b'X', b'X', b'X', b'X', 0x02, 0x00, 0x00, 0x00];
assert_eq!(hyperscan_cache_header_is_valid(&bad), false);
}
#[test]
fn wrong_version_v1_is_rejected() {
let bad: [u8; 8] = [0x4B, 0x48, 0x48, 0x53, 0x01, 0x00, 0x00, 0x00];
assert_eq!(hyperscan_cache_header_is_valid(&bad), false);
}
#[test]
fn future_version_v3_is_rejected() {
let bad: [u8; 8] = [0x4B, 0x48, 0x48, 0x53, 0x03, 0x00, 0x00, 0x00];
assert_eq!(hyperscan_cache_header_is_valid(&bad), false);
}
#[test]
fn big_endian_version_is_rejected() {
let bad: [u8; 8] = [0x4B, 0x48, 0x48, 0x53, 0x00, 0x00, 0x00, 0x02];
assert_eq!(hyperscan_cache_header_is_valid(&bad), false);
}
#[test]
fn truncated_header_is_rejected() {
let full: [u8; 8] = [0x4B, 0x48, 0x48, 0x53, 0x02, 0x00, 0x00, 0x00];
for len in 0..HYPERSCAN_CACHE_HEADER_LEN {
let slice = &full[..len];
assert_eq!(
hyperscan_cache_header_is_valid(slice),
false,
"length {len} must be rejected"
);
}
}
#[test]
fn overlong_header_is_rejected() {
let over: [u8; 9] = [0x4B, 0x48, 0x48, 0x53, 0x02, 0x00, 0x00, 0x00, 0x00];
assert_eq!(over.len(), HYPERSCAN_CACHE_HEADER_LEN + 1);
assert_eq!(hyperscan_cache_header_is_valid(&over), false);
}
#[test]
fn empty_header_is_rejected() {
let empty: [u8; 0] = [];
assert_eq!(hyperscan_cache_header_is_valid(&empty), false);
}
#[test]
fn all_zero_header_is_rejected() {
let zeros = [0_u8; 8];
assert_eq!(hyperscan_cache_header_is_valid(&zeros), false);
}
#[test]
fn hand_built_valid_header_accepts() {
let good: [u8; 8] = [0x4B, 0x48, 0x48, 0x53, 0x02, 0x00, 0x00, 0x00];
assert_eq!(hyperscan_cache_header_is_valid(&good), true);
}
#[test]
fn cache_filename_composes_exact_prefix_and_suffix() {
assert_eq!(hyperscan_cache_filename("deadbeef"), "hs-deadbeef.db");
assert_eq!(hyperscan_cache_filename(""), "hs-.db");
let key = "0a1b2c3d4e5f60718293a4b5c6d7e8f9";
let name = hyperscan_cache_filename(key);
assert!(
name.starts_with("hs-"),
"writer filename must carry the hardening-recognised prefix"
);
assert!(
name.ends_with(".db"),
"writer filename must carry the hardening-recognised suffix"
);
let stripped = name.strip_prefix("hs-").and_then(|s| s.strip_suffix(".db"));
assert_eq!(stripped, Some(key));
}