use keyhog_scanner::multiline::fragment_cache::{
shard_index_drift_probe, FragmentCache, SecretFragment,
};
use std::sync::Arc;
use zeroize::Zeroizing;
fn frag(prefix: &str, var: &str, value: &str, line: usize, path: &str) -> SecretFragment {
SecretFragment {
prefix: prefix.to_string(),
var_name: var.to_string(),
value: Zeroizing::new(value.to_string()),
line,
path: Some(Arc::from(path)),
}
}
#[test]
fn same_file_fragments_within_window_reassemble() {
let cache = FragmentCache::new(64);
let dir = "/repo/.env.d";
let first = cache.record_and_reassemble(frag(
"awskey",
"AWS_ACCESS_KEY_PART1",
"AKIA0000000000000000", 10,
&format!("{dir}/keys.env"),
));
assert!(
first.is_empty(),
"single-fragment scope must not yield candidates, got {} candidates",
first.len()
);
let joined = cache.record_and_reassemble(frag(
"awskey",
"AWS_ACCESS_KEY_PART2",
"BBBBBBBBBBBBBBBBBBBB",
20,
&format!("{dir}/keys.env"),
));
let glued: Vec<String> = joined.iter().map(|z| z.to_string()).collect();
assert!(
glued
.iter()
.any(|g| g == "AKIA0000000000000000BBBBBBBBBBBBBBBBBBBB"), "expected forward AKIA||BBBB reassembly in {:?}",
glued
);
assert!(
glued
.iter()
.any(|g| g == "BBBBBBBBBBBBBBBBBBBBAKIA0000000000000000"), "expected reverse BBBB||AKIA reassembly in {:?}",
glued
);
assert_eq!(
glued.len(),
2,
"exactly two ordered pairs expected, got {}: {:?}",
glued.len(),
glued
);
}
#[test]
fn cross_file_fragments_do_not_reassemble() {
let cache = FragmentCache::new(64);
let dir = "/repo/.env.d";
let _ = cache.record_and_reassemble(frag(
"awskey",
"AWS_ACCESS_KEY",
"AKIA0000000000000000", 6,
&format!("{dir}/file_a.yaml"),
));
let cross = cache.record_and_reassemble(frag(
"awskey",
"AWS_ACCESS_KEY",
"BBBBBBBBBBBBBBBBBBBB",
6,
&format!("{dir}/file_b.sh"),
));
assert!(
cross.is_empty(),
"cross-file reassembly must be suppressed, got {} candidates: {:?}",
cross.len(),
cross.iter().map(|z| z.to_string()).collect::<Vec<_>>()
);
}
#[test]
fn same_file_fragments_outside_window_do_not_reassemble() {
let cache = FragmentCache::new(64);
let path = "/repo/huge.env";
let _ = cache.record_and_reassemble(frag(
"awskey",
"AWS_ACCESS_KEY_A",
"AKIA0000000000000000", 1,
path,
));
let far = cache.record_and_reassemble(frag(
"awskey",
"AWS_ACCESS_KEY_B",
"BBBBBBBBBBBBBBBBBBBB",
500,
path,
));
assert!(
far.is_empty(),
"out-of-window same-file reassembly must be suppressed, got {:?}",
far.iter().map(|z| z.to_string()).collect::<Vec<_>>()
);
}
#[test]
fn shard_index_of_matches_joined_key_hash() {
let cases = [
("", ""),
("awskey", ""),
("", "/repo/.env"),
("awskey", "/repo/.env.d/keys.env"),
("gh\0pat", "/a/b\0c/d"),
("prefix-with-emoji-\u{1f511}", "/path/\u{e9}t\u{e9}/clef"),
("a", "b"),
];
for (prefix, scope) in cases {
let (slice_pair, joined_key) = shard_index_drift_probe(prefix, scope);
assert_eq!(
slice_pair, joined_key,
"shard hash drift for prefix={prefix:?} scope={scope:?}"
);
}
}