use crate::types::*;
use keyhog_core::Chunk;
use std::borrow::Cow;
const ML_CONTEXT_WINDOW_BYTES: usize = 8 * 1024;
pub(crate) fn local_context_window(text: &str, line: usize, radius: usize) -> &str {
let bytes = text.as_bytes();
let lines_before = line.saturating_sub(radius).saturating_sub(1);
let mut start = 0usize;
for _ in 0..lines_before {
match memchr::memchr(b'\n', &bytes[start..]) {
Some(pos) => start = start + pos + 1,
None => return "",
}
}
let cap = (start + ML_CONTEXT_WINDOW_BYTES).min(bytes.len());
let window_lines = radius.saturating_mul(2).saturating_add(1);
let mut end = start;
for n in 0..window_lines {
if end >= cap {
break;
}
match memchr::memchr(b'\n', &bytes[end..cap]) {
Some(pos) => {
end = if n + 1 == window_lines {
end + pos
} else {
end + pos + 1
};
if n + 1 == window_lines {
break;
}
}
None => {
end = cap;
break;
}
}
}
let end = crate::engine::floor_char_boundary(text, end);
&text[start..end]
}
pub(crate) fn local_context_window_from_offsets<'a>(
text: &'a str,
line_offsets: &[usize],
line: usize,
radius: usize,
) -> &'a str {
let start_line = line.saturating_sub(radius).saturating_sub(1);
let Some(&start) = line_offsets.get(start_line) else {
return "";
};
if start > text.len() {
return "";
}
let window_lines = radius.saturating_mul(2).saturating_add(1);
let end_line = start_line.saturating_add(window_lines);
let uncapped_end = match line_offsets.get(end_line) {
Some(&next_line_start) => next_line_start.saturating_sub(1),
None => text.len(),
};
let end = uncapped_end
.min(start.saturating_add(ML_CONTEXT_WINDOW_BYTES))
.min(text.len());
let end = crate::engine::floor_char_boundary(text, end);
&text[start..end]
}
pub fn compute_line_offsets(text: &str) -> Vec<usize> {
let bytes = text.as_bytes();
let estimated_lines = bytes.len() / 40 + 1;
let mut offsets = Vec::with_capacity(estimated_lines);
offsets.push(0);
for pos in memchr::memchr_iter(b'\n', bytes) {
offsets.push(pos + 1);
}
offsets
}
pub(crate) fn match_line_number(
preprocessed: &ScannerPreprocessedText<'_>,
line_offsets: &[usize],
offset: usize,
) -> usize {
match preprocessed.line_for_offset(offset) {
Some(line) => line,
None => {
line_offsets.partition_point(|&lo| lo <= offset)
}
}
}
pub(crate) fn normalize_scannable_chunk<'a>(
chunk: &'a Chunk,
owned: &'a mut Option<Chunk>,
) -> &'a Chunk {
let normalized = crate::normalize_chunk_data(&chunk.data);
if let Cow::Owned(data) = normalized {
*owned = Some(Chunk {
data: data.into(),
metadata: chunk.metadata.clone(),
});
owned.as_ref().unwrap_or(chunk) } else {
chunk
}
}
pub(crate) fn find_companion(
preprocessed: &ScannerPreprocessedText<'_>,
primary_line: usize,
primary_start: usize,
primary_end: usize,
primary_value: &str,
companion: &CompiledCompanion,
) -> Option<String> {
const MAX_COMPANION_MATCH_BYTES: usize = 4096;
let (window_start, window_end) = companion_search_window(
preprocessed,
primary_line,
primary_start,
primary_end,
companion,
)?;
let haystack = preprocessed.text.get(window_start..window_end)?;
let group = companion.capture_group.unwrap_or(FIRST_CAPTURE_GROUP_INDEX); let regex = companion.regex.get();
if companion.capture_group.is_none() {
for matched in regex.find_iter(haystack) {
if matched.len() > MAX_COMPANION_MATCH_BYTES {
continue;
}
let absolute_start = window_start + matched.start();
let absolute_end = window_start + matched.end();
if evidence_relation_accepts(
companion,
primary_start,
primary_end,
primary_value,
absolute_start,
absolute_end,
matched.as_str(),
) {
return Some(matched.as_str().to_string());
}
}
return None;
}
let mut locations = regex.capture_locations();
let mut cursor = 0usize;
while cursor <= haystack.len() {
let Some(whole) = regex.captures_read_at(&mut locations, haystack, cursor) else {
break;
};
cursor = crate::engine::ceil_char_boundary(
haystack,
if whole.end() == cursor {
cursor + 1
} else {
whole.end()
},
);
let Some((start, end)) = locations.get(group) else {
continue;
};
if end.saturating_sub(start) > MAX_COMPANION_MATCH_BYTES {
continue;
}
let Some(captured) = haystack.get(start..end) else {
continue;
};
let absolute_start = window_start + start;
let absolute_end = window_start + end;
if evidence_relation_accepts(
companion,
primary_start,
primary_end,
primary_value,
absolute_start,
absolute_end,
captured,
) {
return Some(captured.to_string());
}
}
None
}
fn companion_search_window(
preprocessed: &ScannerPreprocessedText<'_>,
primary_line: usize,
primary_start: usize,
primary_end: usize,
companion: &CompiledCompanion,
) -> Option<(usize, usize)> {
use keyhog_core::EvidenceScope;
if primary_start > primary_end || primary_end > preprocessed.text.len() {
return None;
}
let start_line = primary_line
.saturating_sub(companion.within_lines)
.max(FIRST_LINE_NUMBER);
let end_line = primary_line.saturating_add(companion.within_lines);
let line_window = line_window_offsets(preprocessed, start_line, end_line)?;
let scope_window = match companion.scope {
EvidenceScope::Window => line_window,
EvidenceScope::SameLine => line_window_offsets(preprocessed, primary_line, primary_line)?,
EvidenceScope::SameRecord => {
record_scope_offsets(preprocessed.text.as_ref(), primary_start)?
}
EvidenceScope::SameObject => {
object_scope_offsets(preprocessed.text.as_ref(), primary_start, primary_end)?
}
};
let start = line_window.0.max(scope_window.0);
let end = line_window.1.min(scope_window.1);
(start <= primary_start && primary_end <= end && start < end).then_some((start, end))
}
fn evidence_relation_accepts(
companion: &CompiledCompanion,
primary_start: usize,
primary_end: usize,
primary_value: &str,
evidence_start: usize,
evidence_end: usize,
evidence_value: &str,
) -> bool {
use keyhog_core::{EvidenceDirection, EvidenceValueRelation};
let direction_matches = match companion.direction {
EvidenceDirection::Either => true,
EvidenceDirection::Before => evidence_end <= primary_start,
EvidenceDirection::After => evidence_start >= primary_end,
};
if !direction_matches {
return false;
}
if let Some(max_gap) = companion.within_bytes {
let gap = if evidence_end <= primary_start {
primary_start - evidence_end
} else if primary_end <= evidence_start {
evidence_start - primary_end
} else {
0
};
if gap > max_gap {
return false;
}
}
match companion.value_relation {
EvidenceValueRelation::Present => true,
EvidenceValueRelation::EqualsPrimary => evidence_value == primary_value,
EvidenceValueRelation::DiffersFromPrimary => evidence_value != primary_value,
}
}
fn record_scope_offsets(text: &str, primary_start: usize) -> Option<(usize, usize)> {
if primary_start > text.len() || !text.is_char_boundary(primary_start) {
return None;
}
let mut start = text[..primary_start]
.rfind('\n')
.map_or(0, |newline| newline + 1);
while start > 0 {
let previous_end = start - 1;
let previous_start = text[..previous_end]
.rfind('\n')
.map_or(0, |newline| newline + 1);
if text[previous_start..previous_end].trim().is_empty() {
break;
}
start = previous_start;
}
let mut end = text[primary_start..]
.find('\n')
.map_or(text.len(), |newline| primary_start + newline);
while end < text.len() {
let next_start = end + 1;
let next_end = text[next_start..]
.find('\n')
.map_or(text.len(), |newline| next_start + newline);
if text[next_start..next_end].trim().is_empty() {
break;
}
end = next_end;
}
Some((start, end))
}
fn object_scope_offsets(
text: &str,
primary_start: usize,
primary_end: usize,
) -> Option<(usize, usize)> {
if primary_start > primary_end || primary_end > text.len() {
return None;
}
let bytes = text.as_bytes();
let mut stack: Vec<(u8, usize)> = Vec::new();
let mut quoted = None;
let mut escaped = false;
let mut smallest = None;
for (index, byte) in bytes.iter().copied().enumerate() {
if let Some(quote) = quoted {
if escaped {
escaped = false;
} else if byte == b'\\' {
escaped = true;
} else if byte == quote {
quoted = None;
}
continue;
}
if byte == b'"' || byte == b'\'' {
quoted = Some(byte);
continue;
}
match byte {
b'{' | b'[' => stack.push((byte, index)),
b'}' | b']' => {
let expected = if byte == b'}' { b'{' } else { b'[' };
let Some((open, start)) = stack.pop() else {
continue;
};
if open != expected {
stack.clear();
continue;
}
let end = index + 1;
if start <= primary_start
&& primary_end <= end
&& smallest.is_none_or(|(current_start, current_end)| {
end - start < current_end - current_start
})
{
smallest = Some((start, end));
}
}
_ => {}
}
}
smallest
}
pub(crate) fn line_window_offsets(
preprocessed: &ScannerPreprocessedText<'_>,
start_line: usize,
end_line: usize,
) -> Option<(usize, usize)> {
let mappings = &preprocessed.mappings;
let prefix_len = monotonic_prefix_len(preprocessed);
let prefix = &mappings[..prefix_len];
let prefix_start_idx = prefix.partition_point(|m| m.line_number < start_line);
let mut start_offset = prefix.get(prefix_start_idx).map(|m| m.start_offset);
let prefix_end_idx = prefix.partition_point(|m| m.line_number <= end_line);
let mut end_offset = (prefix_end_idx > 0).then(|| prefix[prefix_end_idx - 1].end_offset);
for mapping in &mappings[prefix_len..] {
if start_offset.is_none() && mapping.line_number >= start_line {
start_offset = Some(mapping.start_offset);
}
if mapping.line_number <= end_line {
end_offset = Some(mapping.end_offset);
}
}
Some((start_offset?, end_offset?))
}
#[cfg(feature = "multiline")]
fn monotonic_prefix_len(preprocessed: &ScannerPreprocessedText<'_>) -> usize {
preprocessed
.mappings
.partition_point(|m| m.start_offset < preprocessed.original_end)
}
#[cfg(not(feature = "multiline"))]
fn monotonic_prefix_len(preprocessed: &ScannerPreprocessedText<'_>) -> usize {
preprocessed.mappings.len()
}