use lru::LruCache;
use parking_lot::Mutex;
use std::num::NonZeroUsize;
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 ReassembledCandidate {
pub value: Zeroizing<String>,
pub path: Option<Arc<str>>,
pub line: usize,
}
impl std::fmt::Debug for ReassembledCandidate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReassembledCandidate")
.field(
"value",
&format_args!("<redacted {} bytes>", self.value.len()),
)
.field("path", &self.path)
.field("line", &self.line)
.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 scope = fragment.path.as_deref().unwrap_or("");
let shard_idx = shard_index_of(&fragment.prefix, scope);
let mut lock = self.shards[shard_idx].lock();
let key = scoped_key(&fragment);
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 =
f1.path == f2.path && (f1.line as isize - f2.line as isize).abs() < 100;
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.sort_unstable_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
candidates
} else {
Vec::new()
}
}
pub fn record_and_reassemble_stamped(
&self,
fragment: SecretFragment,
) -> Vec<ReassembledCandidate> {
let scope = fragment.path.as_deref().unwrap_or("");
let shard_idx = shard_index_of(&fragment.prefix, scope);
let mut lock = self.shards[shard_idx].lock();
let key = scoped_key(&fragment);
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 =
f1.path == f2.path && (f1.line as isize - f2.line as isize).abs() < 100;
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(ReassembledCandidate {
value: joined,
path: f1.path.clone(),
line: f1.line,
});
}
}
}
candidates.sort_unstable_by(|a, b| {
a.value
.as_bytes()
.cmp(b.value.as_bytes())
.then_with(|| a.line.cmp(&b.line))
});
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().unwrap_or("");
format!("{}\0{}", fragment.prefix, scope)
}
#[inline]
fn shard_fold(h: usize, b: u8) -> usize {
h.wrapping_mul(31).wrapping_add(b as usize)
}
fn shard_index_of(prefix: &str, scope: &str) -> usize {
let mut h = 0usize;
for &b in prefix.as_bytes() {
h = shard_fold(h, b);
}
h = shard_fold(h, 0);
for &b in scope.as_bytes() {
h = shard_fold(h, b);
}
h % SHARD_COUNT
}
#[doc(hidden)]
pub fn shard_index_drift_probe(prefix: &str, scope: &str) -> (usize, usize) {
let joined = format!("{prefix}\0{scope}");
let joined_key_shard = joined.bytes().fold(0usize, shard_fold) % SHARD_COUNT;
(shard_index_of(prefix, scope), joined_key_shard)
}