static EXTRACT_CALLS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static EXTRACT_BYTES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static EXTRACT_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
#[derive(Clone, Debug)]
pub(crate) struct ExtractedValue {
pub(crate) value: String,
pub(crate) start: usize,
pub(crate) end: usize,
}
impl ExtractedValue {
pub(crate) fn new(value: String, start: usize, end: usize) -> Self {
Self { value, start, end }
}
pub(crate) fn span(&self) -> (usize, usize) {
(self.start, self.end)
}
}
fn extract_prof_enabled() -> bool {
crate::scan_profile::enabled()
}
pub(crate) fn extract_profile_dump() {
use std::sync::atomic::Ordering::Relaxed;
let calls = EXTRACT_CALLS.swap(0, Relaxed);
let bytes = EXTRACT_BYTES.swap(0, Relaxed);
let ms = EXTRACT_NS.swap(0, Relaxed) as f64 / 1e6;
if calls == 0 && bytes == 0 && ms == 0.0 {
return;
}
eprintln!(
"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
}
);
}
pub(crate) fn extract_profile_reset() {
use std::sync::atomic::Ordering::Relaxed;
EXTRACT_CALLS.store(0, Relaxed);
EXTRACT_BYTES.store(0, Relaxed);
EXTRACT_NS.store(0, Relaxed);
}
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) {
let cands = extract_encoded_value_spans_raw(text);
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);
f(&cands)
})
}
const MIN_B64_BLOCK_LEN: usize = 16;
fn extract_encoded_value_spans_raw(text: &str) -> Vec<ExtractedValue> {
const MIN_EXTRACTED_VALUE_LEN: usize = 4;
let _prof = extract_prof_enabled().then(|| {
use std::sync::atomic::Ordering::Relaxed;
EXTRACT_CALLS.fetch_add(1, Relaxed);
EXTRACT_BYTES.fetch_add(text.len() as u64, Relaxed);
std::time::Instant::now()
});
let _guard = ExtractTimer(_prof);
struct ExtractTimer(Option<std::time::Instant>);
impl Drop for ExtractTimer {
fn drop(&mut self) {
if let Some(t) = self.0 {
EXTRACT_NS.fetch_add(
t.elapsed().as_nanos() as u64,
std::sync::atomic::Ordering::Relaxed,
);
}
}
}
let mut values = Vec::new();
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].to_string(),
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].to_string(),
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(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 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, 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();
cleaned.push(c);
chars.next();
}
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(cleaned, 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;