use lru::LruCache;
use parking_lot::Mutex;
use std::num::NonZeroUsize;
use std::path::Path;
use std::sync::Arc;
use zeroize::Zeroizing;
const SHARD_COUNT: usize = 64;
const MAX_FRAGMENTS_PER_SCOPE: usize = 8;
#[derive(Clone)]
pub struct SecretFragment {
pub prefix: String,
pub var_name: String,
pub value: Zeroizing<String>,
pub line: usize,
pub path: Option<Arc<str>>,
}
impl std::fmt::Debug for SecretFragment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SecretFragment")
.field("prefix", &self.prefix)
.field("var_name", &self.var_name)
.field(
"value",
&format_args!("<redacted {} bytes>", self.value.len()),
)
.field("line", &self.line)
.field("path", &self.path)
.finish()
}
}
pub struct FragmentCache {
shards: [Mutex<LruCache<String, Vec<SecretFragment>>>; SHARD_COUNT],
}
impl FragmentCache {
pub fn new(capacity: usize) -> Self {
let per_shard = (capacity / SHARD_COUNT).max(1);
let nz = NonZeroUsize::new(per_shard).unwrap_or(NonZeroUsize::MIN);
Self {
shards: std::array::from_fn(|_| Mutex::new(LruCache::new(nz))),
}
}
pub fn record_and_reassemble(&self, fragment: SecretFragment) -> Vec<Zeroizing<String>> {
let key = scoped_key(&fragment);
let shard_idx = shard_index(&key);
let mut lock = self.shards[shard_idx].lock();
let cluster = lock.get_or_insert_mut(key, Vec::new);
if !cluster.iter().any(|f| {
f.path == fragment.path && f.line == fragment.line && **f.value == **fragment.value
}) {
cluster.push(fragment);
if cluster.len() > MAX_FRAGMENTS_PER_SCOPE {
cluster.remove(0);
}
}
if cluster.len() >= 2 {
let mut candidates = Vec::new();
for i in 0..cluster.len() {
for j in 0..cluster.len() {
if i == j {
continue;
}
let f1 = &cluster[i];
let f2 = &cluster[j];
let near = if f1.path == f2.path {
(f1.line as isize - f2.line as isize).abs() < 100
} else {
true
};
if near {
let mut joined = Zeroizing::new(String::new());
joined.push_str(f1.value.as_str());
joined.push_str(f2.value.as_str());
candidates.push(joined);
}
}
}
candidates
} else {
Vec::new()
}
}
pub fn clear(&self) {
for shard in &self.shards {
shard.lock().clear();
}
}
}
fn scoped_key(fragment: &SecretFragment) -> String {
let scope = fragment
.path
.as_deref()
.and_then(|path| Path::new(path).parent())
.and_then(Path::to_str)
.unwrap_or("");
format!("{}\0{}", fragment.prefix, scope)
}
fn shard_index(key: &str) -> usize {
key.bytes()
.fold(0usize, |h, b| h.wrapping_mul(31).wrapping_add(b as usize))
% SHARD_COUNT
}