use std::cell::RefCell;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub(crate) struct DecodeStructure {
pub(crate) decodable: bool,
pub(crate) decoded_len: usize,
pub(crate) printable_ratio: f32,
pub(crate) magic: Option<&'static str>,
pub(crate) protobuf_wire: bool,
}
impl DecodeStructure {
#[must_use]
pub(crate) fn is_binary_payload(&self) -> bool {
self.magic.is_some() || (self.protobuf_wire && self.decoded_len >= 8)
}
}
const MIN_DECODE_LEN: usize = 16;
#[derive(Clone, Copy, Default)]
pub(crate) struct DecodeEvidence {
structure: DecodeStructure,
decoded_is_base64_blob: bool,
decoded_hex_text_len: Option<usize>,
#[cfg(any(feature = "entropy", test))]
decoded_contains_nul_byte: bool,
decoded_contains_placeholder: bool,
}
impl DecodeEvidence {
#[must_use]
pub(crate) const fn structure(self) -> DecodeStructure {
self.structure
}
#[must_use]
pub(crate) fn is_binary_payload(self) -> bool {
self.structure.is_binary_payload()
}
#[must_use]
pub(crate) const fn decoded_is_base64_blob(self) -> bool {
self.decoded_is_base64_blob
}
#[must_use]
pub(crate) const fn decoded_hex_text_len(self) -> Option<usize> {
self.decoded_hex_text_len
}
#[cfg(any(feature = "entropy", test))]
#[must_use]
pub(crate) const fn decoded_contains_nul_byte(self) -> bool {
self.decoded_contains_nul_byte
}
#[must_use]
pub(crate) const fn decoded_contains_placeholder(self) -> bool {
self.decoded_contains_placeholder
}
}
thread_local! {
static DECODE_FACTS_CACHE: RefCell<HashMap<u64, DecodeEvidence>> =
RefCell::new(HashMap::with_capacity(256));
}
#[must_use]
pub(crate) fn is_random_base64_blob(
value: &str,
min_len: usize,
max_len: usize,
min_diversity: u32,
) -> bool {
if !(min_len..=max_len).contains(&value.len()) {
return false;
}
let Some(shape) = crate::decode::standard_base64_shape(value) else {
return false;
};
if !shape.has_padding && !shape.length_multiple_of_four {
return false;
}
shape.has_plus
|| shape.has_slash
|| shape.has_padding
|| (min_diversity != 0
&& shape.length_multiple_of_four
&& shape.distinct_alnum >= min_diversity)
}
#[must_use]
pub(crate) fn looks_like_uniform_base64_blob(value: &str) -> bool {
is_random_base64_blob(value, 44, 600, 32)
}
#[must_use]
pub(crate) fn is_byte_distribution_base64_blob(
value: &str,
min_len: usize,
max_len: usize,
) -> bool {
if !(min_len..=max_len).contains(&value.len()) {
return false;
}
let Some(shape) = crate::decode::standard_base64_shape(value) else {
return false;
};
if !shape.has_padding && !shape.length_multiple_of_four {
return false;
}
(shape.has_plus && shape.has_slash)
|| (shape.has_padding && (shape.has_plus || shape.has_slash))
}
#[must_use]
pub(crate) fn decodes_to_printable_text(candidate: &str) -> bool {
decodes_to_printable_text_inner(candidate, false)
}
#[must_use]
pub(crate) fn decodes_to_printable_text_with_strong_anchor(candidate: &str) -> bool {
decodes_to_printable_text_inner(candidate, true)
}
fn decodes_to_printable_text_inner(candidate: &str, allow_nested_base64: bool) -> bool {
let evidence = evidence(candidate);
let structure = evidence.structure();
structure.decodable
&& structure.decoded_len >= 8
&& structure.printable_ratio >= 0.85
&& !structure.is_binary_payload()
&& (allow_nested_base64 || !evidence.decoded_is_base64_blob())
}
#[must_use]
pub(crate) fn analyze(candidate: &str) -> DecodeStructure {
evidence(candidate).structure()
}
#[must_use]
pub(crate) fn evidence(candidate: &str) -> DecodeEvidence {
let key = crate::util_hash::hash_fast(candidate.as_bytes());
crate::util_hash::memoize_by_hash(
&DECODE_FACTS_CACHE,
key,
crate::util_hash::DEFAULT_MAX_CACHE_ENTRIES,
|| compute_decode_facts(candidate),
)
}
fn compute_decode_facts(candidate: &str) -> DecodeEvidence {
let trimmed = candidate.trim();
if trimmed.len() < MIN_DECODE_LEN {
return DecodeEvidence::default();
}
let Some(bytes) = decode_candidate(trimmed) else {
return DecodeEvidence::default();
};
if bytes.is_empty() {
return DecodeEvidence::default();
}
let printable = bytes
.iter()
.filter(|&&b| (32..127).contains(&b) || matches!(b, 9 | 10 | 13))
.count();
let structure = DecodeStructure {
decodable: true,
decoded_len: bytes.len(),
printable_ratio: printable as f32 / bytes.len() as f32,
magic: magic_format(&bytes),
protobuf_wire: parse_protobuf_wire(&bytes),
};
DecodeEvidence {
structure,
decoded_is_base64_blob: std::str::from_utf8(&bytes)
.is_ok_and(looks_like_uniform_base64_blob),
decoded_hex_text_len: bytes
.iter()
.all(|byte| byte.is_ascii_hexdigit())
.then_some(bytes.len()),
#[cfg(any(feature = "entropy", test))]
decoded_contains_nul_byte: bytes.contains(&0),
decoded_contains_placeholder: crate::placeholder_words::bytes_contain_placeholder_word(
&bytes,
),
}
}
fn decode_candidate(s: &str) -> Option<Vec<u8>> {
if s.as_bytes().contains(&b'_') && is_underscore_hex_candidate(s) {
return crate::decode::hex_decode(s).ok(); }
if let Ok(bytes) = crate::decode::base64_decode(s) {
return Some(bytes);
}
if s.len() >= MIN_DECODE_LEN && s.len().is_multiple_of(2) && is_plain_hex_candidate(s) {
return crate::decode::hex_decode(s).ok(); }
None
}
fn is_plain_hex_candidate(s: &str) -> bool {
s.bytes().all(|byte| byte.is_ascii_hexdigit())
}
fn is_underscore_hex_candidate(s: &str) -> bool {
let hex_len = s.bytes().filter(|&byte| byte != b'_').count();
hex_len >= MIN_DECODE_LEN
&& hex_len.is_multiple_of(2)
&& s.bytes()
.all(|byte| byte == b'_' || byte.is_ascii_hexdigit())
}
fn magic_format(b: &[u8]) -> Option<&'static str> {
const SIGS: &[(&[u8], &str)] = &[
(b"\x89PNG\r\n\x1a\n", "png"),
(b"\xff\xd8\xff", "jpeg"),
(b"GIF87a", "gif"),
(b"GIF89a", "gif"),
(b"BZh", "bzip2"),
(b"\xfd7zXZ\x00", "xz"),
(b"\x28\xb5\x2f\xfd", "zstd"),
(b"PK\x03\x04", "zip"),
(b"PK\x05\x06", "zip"),
(b"7z\xbc\xaf\x27\x1c", "7z"),
(b"Rar!\x1a\x07", "rar"),
(b"%PDF-", "pdf"),
(b"\x7fELF", "elf"),
(b"\xfe\xed\xfa\xce", "mach-o"),
(b"\xfe\xed\xfa\xcf", "mach-o"),
(b"\xcf\xfa\xed\xfe", "mach-o"),
(b"\xca\xfe\xba\xbe", "java-class"),
(b"SQLite format 3\x00", "sqlite"),
(b"OggS", "ogg"),
(b"RIFF", "riff"),
(b"\x00\x61\x73\x6d", "wasm"),
(b"\x78\x01", "zlib"),
(b"\x78\x9c", "zlib"),
(b"\x78\xda", "zlib"),
(b"\x78\x5e", "zlib"),
];
if let Some(name) = SIGS
.iter()
.find(|(sig, _)| b.starts_with(sig))
.map(|(_, name)| *name)
{
return Some(name);
}
if is_pe_image(b) {
return Some("pe");
}
if is_gzip_stream(b) {
return Some("gzip");
}
None
}
fn is_pe_image(b: &[u8]) -> bool {
if !b.starts_with(b"MZ") || b.len() < 0x40 {
return false;
}
let e_lfanew = u32::from_le_bytes([b[0x3c], b[0x3d], b[0x3e], b[0x3f]]) as usize;
e_lfanew.checked_add(4).and_then(|end| b.get(e_lfanew..end)) == Some(b"PE\x00\x00".as_slice())
}
fn is_gzip_stream(b: &[u8]) -> bool {
matches!(b, [0x1f, 0x8b, 0x08, ..])
}
pub(crate) fn parse_protobuf_wire(data: &[u8]) -> bool {
const FIXED_WIRE_WIDTHS: [usize; 8] = [0, 8, 0, 0, 0, 4, 0, 0];
let n = data.len();
if n < 8 {
return false;
}
let mut i = 0usize;
let mut fields = 0u32;
while i < n {
let Some((tag, next)) = read_varint(data, i) else {
return false;
};
i = next;
let wire = tag & 0x07;
let field_no = tag >> 3;
if field_no == 0 {
return false;
}
match wire {
0 => {
let Some((_, next)) = read_varint(data, i) else {
return false;
};
i = next;
}
1 | 5 => {
match i.checked_add(FIXED_WIRE_WIDTHS[wire as usize]) {
Some(x) if x <= n => i = x,
_ => return false,
}
}
2 => {
let Some((len, next)) = read_varint(data, i) else {
return false;
};
i = match next.checked_add(len as usize) {
Some(x) if x <= n => x,
_ => return false,
};
}
_ => return false, }
fields += 1;
}
i == n && fields >= 3
}
fn read_varint(data: &[u8], start: usize) -> Option<(u64, usize)> {
let mut value: u64 = 0;
let mut shift = 0u32;
let mut i = start;
loop {
let b = *data.get(i)?;
i += 1;
value |= u64::from(b & 0x7F) << shift;
if b & 0x80 == 0 {
return Some((value, i));
}
shift += 7;
if shift > 63 {
return None;
}
}
}