#[cfg(feature = "multiline")]
const MAX_MULTILINE_PREPROCESS_BYTES: usize = 2 * 1024 * 1024;
#[cfg(feature = "multiline")]
const MAX_MULTILINE_LINE_BYTES: usize = 64 * 1024;
#[cfg(feature = "multiline")]
const LARGE_FILE_KEYWORD_GATE_BYTES: usize = 4096;
pub(crate) const DEFAULT_MAX_JOIN_LINES: usize = 64;
#[cfg(feature = "multiline")]
use crate::types::source_offset_from_mapping;
#[cfg(feature = "multiline")]
pub(crate) use crate::types::LineMapping;
#[cfg(feature = "multiline")]
pub(super) fn source_line_offset_or_record_gap(
source_line_offsets: &[usize],
zero_based_line_index: usize,
) -> usize {
if let Some(offset) = source_line_offsets.get(zero_based_line_index).copied() {
return offset;
}
crate::telemetry::record_line_offset_mapping_mismatch();
match source_line_offsets.last().copied() {
Some(offset) => offset,
None => 0, }
}
#[cfg(feature = "multiline")]
#[derive(Debug, Clone)]
pub(crate) struct PreprocessedText<'a> {
pub(crate) text: std::borrow::Cow<'a, str>,
pub(crate) original_end: usize,
pub(crate) mappings: Vec<LineMapping>,
}
#[cfg(feature = "multiline")]
impl<'a> PreprocessedText<'a> {
pub(crate) fn line_for_offset(&self, offset: usize) -> Option<usize> {
let idx = self.mappings.partition_point(|m| m.start_offset <= offset);
if idx == 0 {
return None;
}
let m = &self.mappings[idx - 1];
if offset < m.end_offset {
Some(m.line_number)
} else {
None
}
}
pub(crate) fn source_offset_for_match(
&self,
source: &str,
offset: usize,
credential: &str,
) -> usize {
let idx = self.mappings.partition_point(|m| m.start_offset <= offset);
if idx == 0 {
return offset.min(source.len().saturating_sub(1));
}
let m = &self.mappings[idx - 1];
if offset >= m.end_offset {
return offset.min(source.len().saturating_sub(1));
}
source_offset_from_mapping(source, m, offset, credential)
}
pub(crate) fn transport_decoded_for_offset(&self, offset: usize) -> bool {
crate::types::transport_decoded_for_offset(&self.mappings, offset)
}
pub(crate) fn passthrough(text: impl Into<std::borrow::Cow<'a, str>>) -> Self {
let text: std::borrow::Cow<'a, str> = text.into();
let mut mappings = Vec::new();
let mut offset = 0;
for (line_idx, line) in text.split('\n').enumerate() {
let end = offset + line.len();
mappings.push(LineMapping {
line_number: line_idx + 1,
start_offset: offset,
end_offset: end + 1,
original_start_offset: offset,
transport_decoded: false,
});
offset = end + 1;
}
if let Some(last) = mappings.last_mut() {
last.end_offset = text.len();
}
let original_end = text.len();
Self {
text,
original_end,
mappings,
}
}
}
#[derive(Debug, Clone)]
pub struct MultilineConfig {
pub max_join_lines: usize,
pub python_implicit: bool,
pub backslash_continuation: bool,
pub plus_concatenation: bool,
pub dot_concatenation: bool,
pub template_literals: bool,
}
impl Default for MultilineConfig {
fn default() -> Self {
Self {
max_join_lines: DEFAULT_MAX_JOIN_LINES,
python_implicit: true,
backslash_continuation: true,
plus_concatenation: true,
dot_concatenation: true,
template_literals: true,
}
}
}
#[cfg(feature = "multiline")]
pub(crate) fn has_function_concat_marker(s: &str) -> bool {
FUNCTION_CONCAT_MARKERS
.iter()
.any(|marker| s.contains(marker))
}
#[cfg(feature = "multiline")]
const FUNCTION_CONCAT_MARKERS: &[&str] = &["paste0(", "paste(", "concat!("];
#[cfg(feature = "multiline")]
const IMPLICIT_CONCAT_MARKERS: &[&[u8]] = &[
b"\" \"", b"' '", b"\"\n\"", b"\"\n ", b"\"\n\t", b"'\n'", b"'\n ", b"'\n\t",
];
#[cfg(feature = "multiline")]
const CONCAT_OPERATOR_MARKERS: &[&[u8]] = &[b"+", b".", b"`", b"\\"];
#[cfg(feature = "multiline")]
fn has_concat_candidate_bytes(bytes: &[u8]) -> bool {
static AC: std::sync::LazyLock<aho_corasick::AhoCorasick> = std::sync::LazyLock::new(|| {
aho_corasick::AhoCorasick::new(
CONCAT_OPERATOR_MARKERS
.iter()
.copied()
.chain(
FUNCTION_CONCAT_MARKERS
.iter()
.map(|marker| marker.as_bytes()),
)
.chain(IMPLICIT_CONCAT_MARKERS.iter().copied()),
)
.unwrap_or_else(|error| {
panic!("static multiline concatenation marker index is invalid: {error}")
})
});
AC.find(bytes).is_some()
}
#[cfg(feature = "multiline")]
pub(crate) fn has_concatenation_indicators(text: &str) -> bool {
has_concatenation_indicators_with_keyword_gate(text, |bytes| {
use crate::ascii_ci::ci_find;
ci_find(bytes, b"secret")
|| ci_find(bytes, b"token")
|| ci_find(bytes, b"password")
|| ci_find(bytes, b"api_key")
|| ci_find(bytes, b"credential")
})
}
#[cfg(feature = "multiline")]
pub(crate) fn has_concatenation_indicators_with_keyword_gate(
text: &str,
large_file_has_keyword: impl FnOnce(&[u8]) -> bool,
) -> bool {
let trimmed = text.trim_start();
if trimmed.starts_with("<?xml") || trimmed.starts_with('<') {
return false;
}
let starts_structured = trimmed.starts_with('{') || trimmed.starts_with('[');
let bytes = text.as_bytes();
if bytes.len() > LARGE_FILE_KEYWORD_GATE_BYTES {
if !large_file_has_keyword(bytes) {
return false;
}
}
if !has_concat_candidate_bytes(bytes) {
return false;
}
let has_explicit_concat = text.contains("\" +") || text.contains("' +");
let has_dot_concat = has_dot_concat_shape(text);
let has_backslash_cont = text.contains("\" \\") || text.contains("' \\");
let has_template = memchr::memchr(b'`', bytes).is_some();
let has_paste = has_function_concat_marker(text);
let has_empty_array_join = text.contains('[') && has_empty_string_join_marker(text);
let has_implicit = has_implicit_concat_marker(bytes);
let has_var_ref_concat =
memchr::memchr(b'+', bytes).is_some() && has_var_ref_concatenation(text);
if !has_explicit_concat
&& !has_dot_concat
&& !has_backslash_cont
&& !has_template
&& !has_paste
&& !has_empty_array_join
&& !has_implicit
&& !has_var_ref_concat
{
return false;
}
if starts_structured && parses_as_strict_json(trimmed) {
return false;
}
for line in text.lines() {
let trimmed = line.trim();
if trimmed.ends_with('+')
|| trimmed.starts_with('+')
|| trimmed.starts_with("+ ")
|| has_function_concat_marker(trimmed)
|| has_empty_string_join_marker(trimmed)
|| starts_parenthesized_implicit_block(trimmed)
|| trimmed.contains("\" +")
|| trimmed.contains("' +")
|| trimmed.contains("+ \"")
|| trimmed.contains("+ '")
|| has_dot_concat_shape(trimmed)
|| (trimmed.ends_with('\\') && !trimmed.ends_with("\\\\"))
|| trimmed.contains("\" \"")
|| trimmed.contains("' '")
|| has_var_ref_concat_line(trimmed)
|| (trimmed.ends_with('`') && trimmed.matches('`').count() == 1)
|| trimmed.contains("${\"")
|| trimmed.contains("${'")
|| trimmed.contains("}${")
{
return true;
}
}
false
}
#[cfg(feature = "multiline")]
pub(super) fn has_empty_string_join_marker(text: &str) -> bool {
let mut remaining = text;
while let Some(index) = remaining.find(".join") {
remaining = &remaining[index + ".join".len()..];
let Some(arguments) = remaining.trim_start().strip_prefix('(') else {
continue;
};
let arguments = arguments.trim_start();
let after_empty = arguments
.strip_prefix("''")
.or_else(|| arguments.strip_prefix("\"\""))
.or_else(|| arguments.strip_prefix("``"));
if after_empty.is_some_and(|rest| rest.trim_start().starts_with(')')) {
return true;
}
}
false
}
#[cfg(feature = "multiline")]
fn parses_as_strict_json(text: &str) -> bool {
serde_json::from_str::<serde::de::IgnoredAny>(text).is_ok()
}
#[cfg(feature = "multiline")]
fn has_dot_concat_shape(text: &str) -> bool {
let bytes = text.as_bytes();
let mut quote: Option<u8> = None;
let mut escaped = false;
let mut prev_nonspace_closed_quote = false;
let mut i = 0usize;
while i < bytes.len() {
let b = bytes[i];
i += 1;
if let Some(q) = quote {
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == q {
quote = None;
prev_nonspace_closed_quote = true;
}
continue;
}
match b {
b'"' | b'\'' => {
quote = Some(b);
prev_nonspace_closed_quote = false;
}
b' ' | b'\t' => { }
b'.' if prev_nonspace_closed_quote => {
let mut j = i;
while j < bytes.len() && matches!(bytes[j], b' ' | b'\t') {
j += 1;
}
let right_is_quote = j < bytes.len() && matches!(bytes[j], b'"' | b'\'');
let right_is_eol = j >= bytes.len() || matches!(bytes[j], b'\n' | b'\r');
if right_is_quote || right_is_eol {
return true;
}
prev_nonspace_closed_quote = false;
}
_ => prev_nonspace_closed_quote = false,
}
}
false
}
#[cfg(feature = "multiline")]
pub(super) fn starts_parenthesized_implicit_block(line: &str) -> bool {
let Some(assign_idx) = line.find(['=', ':']) else {
return false;
};
line[assign_idx + 1..].trim() == "("
}
#[cfg(feature = "multiline")]
fn has_implicit_concat_marker(bytes: &[u8]) -> bool {
IMPLICIT_CONCAT_MARKERS
.iter()
.any(|marker| memchr::memmem::find(bytes, marker).is_some())
}
#[cfg(feature = "multiline")]
fn has_var_ref_concatenation(text: &str) -> bool {
text.lines().any(has_var_ref_concat_line)
}
#[cfg(feature = "multiline")]
fn has_var_ref_concat_line(line: &str) -> bool {
if !line.contains('+') {
return false;
}
super::structural::CONCAT_RE.is_match(line)
}
#[cfg(feature = "multiline")]
pub(crate) fn should_passthrough(text: &str) -> bool {
exceeds_multiline_limits(text) || !has_concatenation_indicators(text)
}
#[cfg(feature = "multiline")]
pub(crate) fn exceeds_multiline_limits(text: &str) -> bool {
text.len() > MAX_MULTILINE_PREPROCESS_BYTES
|| text
.lines()
.any(|line| line.len() > MAX_MULTILINE_LINE_BYTES)
}