use crate::types::ScannerPreprocessedText;
pub(crate) mod parsers;
const MAX_STRUCTURED_PARSE_BYTES: usize = 2 * 1024 * 1024;
const SYNTHETIC_PAIR_SEPARATOR: &str = ": ";
pub(crate) struct ExtractedPair {
pub context: String,
pub value: String,
pub line: usize,
pub transport_decoded: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StructuredFormat {
Env,
K8sSecret,
DockerCompose,
Tfstate,
Hcl,
Jupyter,
}
impl StructuredFormat {
fn uses_decode_through(self) -> bool {
matches!(
self,
Self::K8sSecret | Self::DockerCompose | Self::Tfstate | Self::Jupyter
)
}
}
pub(crate) fn oversize_skip_is_counted(
text: &str,
path: Option<&str>,
decode_derived: bool,
) -> bool {
!decode_derived
&& detect_format(text, path, false).is_some_and(StructuredFormat::uses_decode_through)
}
pub(crate) fn preprocess<'a>(
text: &str,
path: Option<&str>,
decode_derived: bool,
) -> Option<ScannerPreprocessedText<'a>> {
if text.len() > MAX_STRUCTURED_PARSE_BYTES {
if oversize_skip_is_counted(text, path, decode_derived) {
crate::telemetry::record_structured_oversize_skip();
tracing::warn!(
bytes = text.len(),
cap = MAX_STRUCTURED_PARSE_BYTES,
path = path.map_or("<unknown>", |path| path),
"structured decode-through skipped: file exceeds the structured-parse \
size cap, so base64-encoded values (e.g. a k8s `data:` block) were NOT \
decoded; the raw text was still scanned"
);
}
return None;
}
let pairs = detect_and_parse(text, path, decode_derived)?;
if pairs.is_empty() {
return None;
}
Some(build_preprocessed_text(text, pairs))
}
fn trim_quoted_yaml_scalar(value: &str) -> &str {
let value = value.trim();
if value.len() >= 2
&& matches!(
(value.as_bytes()[0], value.as_bytes()[value.len() - 1]),
(b'\'', b'\'') | (b'"', b'"')
)
{
&value[1..value.len() - 1]
} else {
value
}
}
fn contains_k8s_secret_field_hint(text: &str) -> bool {
text.lines().any(|line| {
if line.as_bytes().first().is_some_and(u8::is_ascii_whitespace) {
return false;
}
let line = line.trim();
if line.starts_with('#') {
return false;
}
let Some((key, value)) = line.split_once(':') else {
return false;
};
if trim_quoted_yaml_scalar(key) != "kind" {
return false;
}
let value = value.split_once(" #").map_or(value, |(value, _)| value);
trim_quoted_yaml_scalar(value) == "Secret"
})
}
pub(crate) fn detects_k8s_secret_document(text: &str, path: Option<&str>) -> bool {
detect_format(text, path, true) == Some(StructuredFormat::K8sSecret)
}
fn detect_format(text: &str, path: Option<&str>, parse_yaml: bool) -> Option<StructuredFormat> {
let path_bytes = path.map(str::as_bytes).unwrap_or(&[]); let ends_ci = |suffix: &[u8]| -> bool {
path_bytes.len() >= suffix.len()
&& path_bytes[path_bytes.len() - suffix.len()..].eq_ignore_ascii_case(suffix)
};
let last_sep = path_bytes
.iter()
.rposition(|&b| b == b'/' || b == b'\\')
.map(|i| i + 1)
.unwrap_or(0); let file_bytes = &path_bytes[last_sep..];
let file_starts_ci = |prefix: &[u8]| -> bool {
file_bytes.len() >= prefix.len() && file_bytes[..prefix.len()].eq_ignore_ascii_case(prefix)
};
let file_ends_ci = |suffix: &[u8]| -> bool {
file_bytes.len() >= suffix.len()
&& file_bytes[file_bytes.len() - suffix.len()..].eq_ignore_ascii_case(suffix)
};
let file_contains_ci = |needle: &[u8]| -> bool {
if needle.is_empty() || needle.len() > file_bytes.len() {
return false;
}
file_bytes
.windows(needle.len())
.any(|w| w.eq_ignore_ascii_case(needle))
};
if file_starts_ci(b".env") || file_ends_ci(b".env") {
return Some(StructuredFormat::Env);
}
if (ends_ci(b".yaml") || ends_ci(b".yml"))
&& if parse_yaml {
match parsers::contains_k8s_secret_document(text) {
Ok(is_secret) => is_secret,
Err(()) => contains_k8s_secret_field_hint(text),
}
} else {
contains_k8s_secret_field_hint(text)
}
{
return Some(StructuredFormat::K8sSecret);
}
if (file_contains_ci(b"docker-compose") || file_contains_ci(b"compose"))
&& (ends_ci(b".yaml") || ends_ci(b".yml"))
{
return Some(StructuredFormat::DockerCompose);
}
if ends_ci(b".tfstate") {
return Some(StructuredFormat::Tfstate);
}
if ends_ci(b".tf") || ends_ci(b".tfvars") || ends_ci(b".hcl") {
return Some(StructuredFormat::Hcl);
}
if ends_ci(b".ipynb") {
return Some(StructuredFormat::Jupyter);
}
None
}
fn detect_and_parse(
text: &str,
path: Option<&str>,
decode_derived: bool,
) -> Option<Vec<ExtractedPair>> {
Some(match detect_format(text, path, true)? {
StructuredFormat::Env => parsers::parse_env(text),
StructuredFormat::K8sSecret => parsers::parse_k8s_secret(text, decode_derived),
StructuredFormat::DockerCompose => parsers::parse_docker_compose(text, decode_derived),
StructuredFormat::Tfstate => parsers::parse_tfstate(text, decode_derived),
StructuredFormat::Hcl => parsers::parse_hcl(text),
StructuredFormat::Jupyter => parsers::parse_jupyter(text, decode_derived),
})
}
struct SynthMapping {
line_number: usize,
start_offset: usize,
end_offset: usize,
original_start_offset: usize,
transport_decoded: bool,
}
fn synthetic_mapping_origin(
text: &str,
source_line_offsets: &[usize],
pair: &ExtractedPair,
) -> usize {
let line_start = source_line_start(source_line_offsets, pair.line);
let line_end = source_line_offsets
.get(pair.line)
.copied()
.unwrap_or(text.len()) .min(text.len());
let Some(line) = text.get(line_start..line_end) else {
return line_start;
};
let Some(context_start) = line.find(&pair.context) else {
return line_start;
};
let after_context = context_start + pair.context.len();
let Some(delimiter_offset) = line.as_bytes()[after_context..]
.iter()
.position(|byte| matches!(*byte, b':' | b'='))
else {
return line_start;
};
let mut value_column = after_context + delimiter_offset + 1;
while line
.as_bytes()
.get(value_column)
.is_some_and(|byte| byte.is_ascii_whitespace() || matches!(*byte, b'\'' | b'"'))
{
value_column += 1;
}
let synthetic_value_column = pair.context.len() + SYNTHETIC_PAIR_SEPARATOR.len();
line_start
.saturating_add(value_column)
.saturating_sub(synthetic_value_column)
}
fn synthesize_preprocessed(text: &str, pairs: Vec<ExtractedPair>) -> (String, Vec<SynthMapping>) {
let original_end = text.len();
let appended_len: usize = pairs
.iter()
.map(|p| p.context.len() + SYNTHETIC_PAIR_SEPARATOR.len() + p.value.len() + 1)
.sum();
let mut final_text = String::with_capacity(original_end + 1 + appended_len);
final_text.push_str(text);
let source_line_offsets = crate::compute_line_offsets(text);
let line_count = source_line_offsets.len();
let mut mappings: Vec<SynthMapping> = Vec::with_capacity(line_count + pairs.len());
for line_idx in 0..line_count {
let start = source_line_offsets[line_idx];
let end = source_line_offsets
.get(line_idx + 1)
.copied()
.map_or(original_end, |end| end)
.min(original_end);
mappings.push(SynthMapping {
line_number: line_idx + 1,
start_offset: start,
end_offset: end,
original_start_offset: start,
transport_decoded: false,
});
}
final_text.push('\n');
let mut current_offset = original_end + 1;
for pair in pairs {
let line_len = pair.context.len() + SYNTHETIC_PAIR_SEPARATOR.len() + pair.value.len();
mappings.push(SynthMapping {
line_number: pair.line,
start_offset: current_offset,
end_offset: current_offset + line_len,
original_start_offset: synthetic_mapping_origin(text, &source_line_offsets, &pair),
transport_decoded: pair.transport_decoded,
});
final_text.push_str(&pair.context);
final_text.push_str(SYNTHETIC_PAIR_SEPARATOR);
final_text.push_str(&pair.value);
final_text.push('\n');
current_offset += line_len + 1;
}
(final_text, mappings)
}
#[cfg(feature = "multiline")]
fn build_preprocessed_text<'a>(
text: &str,
pairs: Vec<ExtractedPair>,
) -> ScannerPreprocessedText<'a> {
use crate::multiline::LineMapping;
let original_end = text.len();
let (final_text, synth) = synthesize_preprocessed(text, pairs);
let mappings = synth
.into_iter()
.map(|m| LineMapping {
line_number: m.line_number,
start_offset: m.start_offset,
end_offset: m.end_offset,
original_start_offset: m.original_start_offset,
transport_decoded: m.transport_decoded,
})
.collect();
crate::multiline::PreprocessedText {
text: std::borrow::Cow::Owned(final_text),
original_end,
mappings,
}
}
#[cfg(not(feature = "multiline"))]
fn build_preprocessed_text<'a>(
text: &str,
pairs: Vec<ExtractedPair>,
) -> ScannerPreprocessedText<'a> {
use crate::types::LineMapping;
let (final_text, synth) = synthesize_preprocessed(text, pairs);
let mappings = synth
.into_iter()
.map(|m| LineMapping {
line_number: m.line_number,
start_offset: m.start_offset,
end_offset: m.end_offset,
original_start_offset: m.original_start_offset,
transport_decoded: m.transport_decoded,
})
.collect();
crate::types::PreprocessedText {
text: std::borrow::Cow::Owned(final_text),
mappings,
}
}
fn source_line_start(line_offsets: &[usize], one_based_line: usize) -> usize {
line_offsets
.get(one_based_line.saturating_sub(1))
.copied()
.unwrap_or(0) }