#[cfg(feature = "decode")]
pub(crate) fn format_extract_profile(calls: u64, bytes: u64, ns: u64) -> String {
let ms = ns as f64 / 1e6;
format!(
"extract_encoded_values: calls={calls} bytes={bytes} time={ms:.1}ms ({:.2} µs/call)",
if calls > 0 {
ms * 1000.0 / calls as f64
} else {
0.0
}
)
}
#[cfg(feature = "decode")]
pub(crate) fn extract_profile_from_typed(
metrics: &[keyhog_profile::TypedMetricRecordV2],
) -> (u64, u64, u64) {
let value = |counter: keyhog_profile::CounterId| {
metrics
.iter()
.find(|record| record.metric_id == counter.metric_id())
.map_or(0, |record| record.value)
};
(
value(keyhog_profile::CounterId::DecodeExtractCalls),
value(keyhog_profile::CounterId::DecodeExtractBytes),
value(keyhog_profile::CounterId::DecodeExtractNs),
)
}
#[derive(Clone, Debug)]
pub(crate) struct ExtractedValue {
pub(crate) value: std::sync::Arc<str>,
pub(crate) start: usize,
pub(crate) end: usize,
}
impl ExtractedValue {
pub(crate) fn new(value: impl Into<std::sync::Arc<str>>, start: usize, end: usize) -> Self {
Self {
value: value.into(),
start,
end,
}
}
pub(crate) fn span(&self) -> (usize, usize) {
(self.start, self.end)
}
}
thread_local! {
static SHARED_CANDIDATES: std::cell::RefCell<Option<(usize, usize, Vec<ExtractedValue>)>> =
const { std::cell::RefCell::new(None) };
}
pub(super) fn prime_shared_candidates(text: &str, prune_default_impossible: bool) {
let cands = extract_encoded_value_spans_raw(text, prune_default_impossible);
SHARED_CANDIDATES.with(|c| {
*c.borrow_mut() = Some((text.as_ptr() as usize, text.len(), cands));
});
}
pub(super) fn clear_shared_candidates() {
SHARED_CANDIDATES.with(|c| *c.borrow_mut() = None);
}
pub(crate) fn with_extracted_value_spans<R>(
text: &str,
f: impl FnOnce(&[ExtractedValue]) -> R,
) -> R {
SHARED_CANDIDATES.with(|c| {
let borrowed = c.borrow();
if let Some((_, _, cands)) = borrowed
.as_ref()
.filter(|(ptr, len, _)| *ptr == text.as_ptr() as usize && *len == text.len())
{
return f(cands);
}
drop(borrowed);
let cands = extract_encoded_value_spans_raw(text, false);
f(&cands)
})
}
const MIN_B64_BLOCK_LEN: usize = 16;
fn extract_encoded_value_spans_raw(
text: &str,
prune_default_impossible: bool,
) -> Vec<ExtractedValue> {
const MIN_EXTRACTED_VALUE_LEN: usize = 4;
keyhog_profile::add_counter(keyhog_profile::CounterId::DecodeExtractCalls, 1);
keyhog_profile::add_counter(
keyhog_profile::CounterId::DecodeExtractBytes,
text.len() as u64,
);
let _extract = keyhog_profile::counter_span(keyhog_profile::CounterId::DecodeExtractNs);
let mut values = Vec::new();
let mut value_intern: std::collections::HashMap<u64, std::sync::Arc<str>> =
std::collections::HashMap::new();
let mut intern_value = |raw: &str| -> std::sync::Arc<str> {
let key = hash_fast(raw.as_bytes());
if let Some(existing) = value_intern.get(&key) {
if existing.as_ref() == raw {
return std::sync::Arc::clone(existing);
}
}
let owned: std::sync::Arc<str> = std::sync::Arc::from(raw);
value_intern.insert(key, std::sync::Arc::clone(&owned));
owned
};
let mut b64_block = String::new();
let mut b64_start: Option<usize> = None;
let mut b64_end = 0usize;
let mut pct_block = String::new();
let mut pct_start: Option<usize> = None;
let mut pct_end = 0usize;
let mut pct_percent_count = 0usize;
let is_b64_char =
|ch: char| -> bool { ch.is_ascii() && crate::decode::is_base64_candidate_byte(ch as u8) };
let is_pct_run_char = |ch: char| -> bool { ch == '%' || ch.is_ascii_hexdigit() };
fn flush_b64(
values: &mut Vec<ExtractedValue>,
b64_block: &mut String,
b64_start: &mut Option<usize>,
b64_end: usize,
) {
if b64_block.len() >= MIN_B64_BLOCK_LEN {
if let Some(start) = b64_start.take() {
values.push(ExtractedValue::new(
std::mem::take(b64_block),
start,
b64_end,
));
} else {
b64_block.clear();
}
} else {
b64_block.clear();
*b64_start = None;
}
}
fn push_b64_subruns(
values: &mut Vec<ExtractedValue>,
text: &str,
container_start: usize,
container_end: usize,
) {
let container = &text[container_start..container_end];
if container.starts_with("=?") && container.ends_with("?=") {
return;
}
let mut run_start = None;
for (relative_index, byte) in text.as_bytes()[container_start..container_end]
.iter()
.copied()
.enumerate()
{
let index = container_start + relative_index;
if crate::decode::is_base64_candidate_byte(byte) {
run_start.get_or_insert(index);
continue;
}
if let Some(start) = run_start.take() {
if index.saturating_sub(start) >= MIN_B64_BLOCK_LEN
&& (start != container_start || index != container_end)
{
values.push(ExtractedValue::new(&text[start..index], start, index));
}
}
}
if let Some(start) = run_start {
if container_end.saturating_sub(start) >= MIN_B64_BLOCK_LEN && start != container_start
{
values.push(ExtractedValue::new(
&text[start..container_end],
start,
container_end,
));
}
}
}
fn flush_pct(
values: &mut Vec<ExtractedValue>,
pct_block: &mut String,
pct_start: &mut Option<usize>,
pct_end: usize,
pct_percent_count: &mut usize,
) {
const MIN_PCT_TRIPLETS: usize = 1;
if pct_block.len() >= MIN_PCT_TRIPLETS * 3 && *pct_percent_count >= MIN_PCT_TRIPLETS {
if let Some(start) = pct_start.take() {
values.push(ExtractedValue::new(
std::mem::take(pct_block),
start,
pct_end,
));
} else {
pct_block.clear();
}
} else {
pct_block.clear();
*pct_start = None;
}
*pct_percent_count = 0;
}
let mut chars = text.char_indices().peekable();
while let Some(&(idx, ch)) = chars.peek() {
if ch == '"' || ch == '\'' || ch == '`' {
flush_b64(&mut values, &mut b64_block, &mut b64_start, b64_end);
flush_pct(
&mut values,
&mut pct_block,
&mut pct_start,
pct_end,
&mut pct_percent_count,
);
let quote = ch;
chars.next();
let mut escaping = false;
let mut cleaned = String::with_capacity(32);
let mut value_start: Option<usize> = None;
let mut value_end = idx + ch.len_utf8();
while let Some(&(current_idx, current)) = chars.peek() {
chars.next();
if escaping {
value_start.get_or_insert(current_idx.saturating_sub(1));
value_end = current_idx + current.len_utf8();
cleaned.push('\\');
cleaned.push(current);
escaping = false;
} else if current == '\\' {
value_start.get_or_insert(current_idx);
value_end = current_idx + current.len_utf8();
escaping = true;
} else if current == quote {
if cleaned.len() >= MIN_EXTRACTED_VALUE_LEN {
if let Some(start) = value_start {
push_b64_subruns(&mut values, text, start, value_end);
values.push(ExtractedValue::new(
intern_value(&cleaned),
start,
value_end,
));
}
}
break;
} else if !current.is_ascii_whitespace() {
value_start.get_or_insert(current_idx);
value_end = current_idx + current.len_utf8();
cleaned.push(current);
}
}
continue;
}
let equals_is_base64_padding = ch == '='
&& b64_block.len() >= MIN_B64_BLOCK_LEN
&& match text.as_bytes().get(idx + 1).copied() {
Some(b'=') => b64_block.len() % 4 == 2,
None => b64_block.len() % 4 == 3,
Some(next) if !crate::decode::is_base64_candidate_byte(next) => {
b64_block.len() % 4 == 3
}
Some(_) => false,
};
if equals_is_base64_padding {
b64_end = idx + ch.len_utf8();
b64_block.push(ch);
chars.next();
continue;
}
if (ch == ':' || ch == '=') && !equals_is_base64_padding {
flush_b64(&mut values, &mut b64_block, &mut b64_start, b64_end);
flush_pct(
&mut values,
&mut pct_block,
&mut pct_start,
pct_end,
&mut pct_percent_count,
);
chars.next();
while chars.peek().is_some_and(|&(_, c)| c.is_ascii_whitespace()) {
chars.next();
}
let mut value_start: Option<usize> = None;
let mut value_end = idx + ch.len_utf8();
while let Some(&(current_idx, c)) = chars.peek() {
if c.is_ascii_whitespace()
|| c == ';'
|| c == ','
|| c == '"'
|| c == '\''
|| c == '`'
{
break;
}
value_start.get_or_insert(current_idx);
value_end = current_idx + c.len_utf8();
chars.next();
}
if let Some(start) = value_start {
let value = &text[start..value_end];
let default_impossible = prune_default_impossible
&& value.len() < super::super::limits::MIN_BASE64_CANDIDATE_LEN
&& value.bytes().all(|byte| byte.is_ascii_alphanumeric());
if value.len() >= MIN_EXTRACTED_VALUE_LEN && !default_impossible {
push_b64_subruns(&mut values, text, start, value_end);
values.push(ExtractedValue::new(intern_value(value), start, value_end));
}
}
continue;
}
if is_pct_run_char(ch) {
if pct_block.is_empty() && ch != '%' {
} else {
pct_start.get_or_insert(idx);
pct_end = idx + ch.len_utf8();
if ch == '%' {
pct_percent_count += 1;
}
pct_block.push(ch);
chars.next();
continue;
}
} else if !pct_block.is_empty() {
flush_pct(
&mut values,
&mut pct_block,
&mut pct_start,
pct_end,
&mut pct_percent_count,
);
}
if is_b64_char(ch) {
b64_start.get_or_insert(idx);
b64_end = idx + ch.len_utf8();
b64_block.push(ch);
} else if matches!(ch, '\r' | '\n') {
if b64_block.ends_with('=') {
flush_b64(&mut values, &mut b64_block, &mut b64_start, b64_end);
}
} else {
flush_b64(&mut values, &mut b64_block, &mut b64_start, b64_end);
}
chars.next();
}
flush_b64(&mut values, &mut b64_block, &mut b64_start, b64_end);
flush_pct(
&mut values,
&mut pct_block,
&mut pct_start,
pct_end,
&mut pct_percent_count,
);
values
}
pub(crate) use crate::util_hash::hash_fast;