use lru::LruCache;
use parking_lot::Mutex;
use std::cell::RefCell;
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(crate) 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()
}
}
#[cfg(any(feature = "simd", test))]
pub(crate) struct ReassembledCandidate {
pub value: Zeroizing<String>,
pub path: Option<Arc<str>>,
pub line: usize,
}
#[cfg(any(feature = "simd", test))]
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()
}
}
fn evict_one(cluster: &mut Vec<SecretFragment>) {
if let Some(idx) = cluster
.iter()
.enumerate()
.min_by_key(|(_, fragment)| fragment_eviction_key(fragment))
.map(|(i, _)| i)
{
cluster.remove(idx);
}
}
fn fragment_eviction_key(fragment: &SecretFragment) -> (usize, &[u8]) {
(fragment.line, fragment.value.as_bytes())
}
pub(crate) struct FragmentCache {
shards: [Mutex<LruCache<String, Vec<SecretFragment>>>; SHARD_COUNT],
}
impl FragmentCache {
pub(crate) 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))),
}
}
fn record_and_collect<T>(
&self,
fragment: SecretFragment,
make: impl Fn(&SecretFragment, &SecretFragment) -> T,
) -> Vec<T> {
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 cluster = with_scoped_key(&fragment.prefix, scope, |key| {
lock.get_or_insert_mut_ref(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 {
evict_one(cluster);
}
}
let mut candidates = Vec::new();
if cluster.len() >= 2 {
for i in 0..cluster.len() {
for j in 0..cluster.len() {
if i == j {
continue;
}
let f1 = &cluster[i];
let f2 = &cluster[j];
if f1.path == f2.path && f1.line.abs_diff(f2.line) < 100 {
candidates.push(make(f1, f2));
}
}
}
}
candidates
}
pub(crate) fn record_and_reassemble(&self, fragment: SecretFragment) -> Vec<Zeroizing<String>> {
let mut candidates = self.record_and_collect(fragment, |f1, f2| {
let mut joined = Zeroizing::new(String::with_capacity(f1.value.len() + f2.value.len()));
joined.push_str(f1.value.as_str());
joined.push_str(f2.value.as_str());
joined
});
candidates.sort_unstable_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
candidates
}
#[cfg(any(feature = "simd", test))]
pub(crate) fn record_and_reassemble_stamped(
&self,
fragment: SecretFragment,
) -> Vec<ReassembledCandidate> {
let mut candidates = self.record_and_collect(fragment, |f1, f2| {
let mut joined = Zeroizing::new(String::with_capacity(f1.value.len() + f2.value.len()));
joined.push_str(f1.value.as_str());
joined.push_str(f2.value.as_str());
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
}
pub(crate) fn clear(&self) {
for shard in &self.shards {
shard.lock().clear();
}
}
}
thread_local! {
static SCOPED_KEY_SCRATCH: RefCell<String> = const { RefCell::new(String::new()) };
}
fn with_scoped_key<R>(prefix: &str, scope: &str, f: impl FnOnce(&str) -> R) -> R {
SCOPED_KEY_SCRATCH.with(|scratch| {
let mut key = scratch.borrow_mut();
key.clear();
key.reserve(prefix.len() + 1 + scope.len());
key.push_str(prefix);
key.push('\0');
key.push_str(scope);
f(key.as_str())
})
}
#[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(crate) fn shard_index_drift_probe(prefix: &str, scope: &str) -> (usize, usize) {
with_scoped_key(prefix, scope, |joined| {
let joined_key_shard = joined.bytes().fold(0usize, shard_fold) % SHARD_COUNT;
(shard_index_of(prefix, scope), joined_key_shard)
})
}