use keyhog_core::SourceError;
use memmap2::MmapOptions;
use std::fs::File;
use std::path::Path;
use super::raw::open_file_safe;
use super::MMAP_TOCTOU_SANITY_CAP_BYTES;
pub(in crate::filesystem) struct FileWindow {
pub offset: usize,
pub base_line: usize,
pub text: String,
}
pub(in crate::filesystem) enum WindowedMmapOutcome {
Consumed,
Fallback(File),
}
pub(in crate::filesystem) fn read_file_windowed_mmap(
path: &Path,
window_size: usize,
overlap: usize,
) -> Option<Vec<FileWindow>> {
let mut windows = Vec::new();
let mut terminal_error = false;
match for_each_file_windowed_mmap(path, window_size, overlap, |row| match row {
Ok(window) => {
windows.push(window);
true
}
Err(_error) => {
terminal_error = true;
true
}
}) {
WindowedMmapOutcome::Consumed => {}
WindowedMmapOutcome::Fallback(_) => return None,
}
if terminal_error {
return Some(Vec::new());
}
Some(windows)
}
pub(in crate::filesystem) fn for_each_file_windowed_mmap(
path: &Path,
window_size: usize,
overlap: usize,
mut emit: impl FnMut(Result<FileWindow, SourceError>) -> bool,
) -> WindowedMmapOutcome {
debug_assert!(window_size > overlap, "window must exceed overlap");
let file = match open_file_safe(path) {
Ok(file) => file,
Err(error) => {
tracing::warn!(
path = %path.display(),
%error,
"cannot open large file for windowed mmap; skipping"
);
let _event = crate::record_skip_event(crate::SourceSkipEvent::Unreadable);
let _continue_scan = emit(Err(windowed_mmap_error(
path,
format!("cannot open large file for windowed mmap ({error})"),
)));
return WindowedMmapOutcome::Consumed;
}
};
let meta = match file.metadata() {
Ok(meta) => meta,
Err(error) => {
tracing::warn!(
path = %path.display(),
%error,
"cannot stat opened large file for windowed mmap sanity cap; skipping"
);
let _event = crate::record_skip_event(crate::SourceSkipEvent::Unreadable);
let _continue_scan = emit(Err(windowed_mmap_error(
path,
format!("cannot stat opened large file for windowed mmap ({error})"),
)));
return WindowedMmapOutcome::Consumed;
}
};
if meta.len() > MMAP_TOCTOU_SANITY_CAP_BYTES {
tracing::warn!(
path = %path.display(),
live_size = meta.len(),
cap = MMAP_TOCTOU_SANITY_CAP_BYTES,
"refusing to windowed-mmap file: live size exceeds sanity cap (likely TOCTOU growth)"
);
let _event = crate::record_skip_event(crate::SourceSkipEvent::OverMaxSize);
let _continue_scan = emit(Err(windowed_mmap_error(
path,
format!(
"live size {} exceeded the {}-byte windowed mmap sanity cap",
meta.len(),
MMAP_TOCTOU_SANITY_CAP_BYTES
),
)));
return WindowedMmapOutcome::Consumed;
}
let mmap = match unsafe { MmapOptions::new().map(&file) } {
Ok(m) => m,
Err(error) => {
tracing::warn!(
path = %path.display(),
%error,
"cannot windowed-mmap file; falling back to buffered read"
);
return WindowedMmapOutcome::Fallback(file);
}
};
let mapped_len = match u64::try_from(mmap.len()) {
Ok(len) => len,
Err(error) => {
tracing::warn!(
path = %path.display(),
mapped_len = mmap.len(),
%error,
"cannot represent mapped length for windowed mmap sanity cap; skipping"
);
let _event = crate::record_skip_event(crate::SourceSkipEvent::OverMaxSize);
let _continue_scan = emit(Err(windowed_mmap_error(
path,
"mapped length is not representable for windowed mmap",
)));
return WindowedMmapOutcome::Consumed;
}
};
if mapped_len > MMAP_TOCTOU_SANITY_CAP_BYTES {
tracing::warn!(
path = %path.display(),
live_size = mapped_len,
cap = MMAP_TOCTOU_SANITY_CAP_BYTES,
"refusing windowed mmap: mapped length exceeds sanity cap"
);
let _event = crate::record_skip_event(crate::SourceSkipEvent::OverMaxSize);
let _continue_scan = emit(Err(windowed_mmap_error(
path,
format!(
"mapped length {} exceeds the {}-byte windowed mmap sanity cap; file was not scanned",
mapped_len,
MMAP_TOCTOU_SANITY_CAP_BYTES
),
)));
return WindowedMmapOutcome::Consumed;
}
#[cfg(unix)]
{
unsafe {
libc::madvise(
mmap.as_ptr() as *mut libc::c_void,
mmap.len(),
libc::MADV_SEQUENTIAL,
);
}
}
for_each_window(&mmap, window_size, overlap, |window| emit(Ok(window)));
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_UN) };
}
WindowedMmapOutcome::Consumed
}
fn windowed_mmap_error(path: &Path, reason: impl std::fmt::Display) -> SourceError {
SourceError::Other(format!(
"failed to scan large file '{}': {reason}; file was not scanned",
path.display()
))
}
#[inline]
fn bytecount_newlines(slice: &[u8]) -> usize {
memchr::memchr_iter(b'\n', slice).count()
}
pub(in crate::filesystem) fn slice_into_windows(
bytes: &[u8],
window_size: usize,
overlap: usize,
) -> Vec<FileWindow> {
let mut out = Vec::with_capacity(
bytes
.len()
.div_ceil(window_size.saturating_sub(overlap).max(1)),
);
for_each_window(bytes, window_size, overlap, |window| {
out.push(window);
true
});
out
}
fn for_each_window(
bytes: &[u8],
window_size: usize,
overlap: usize,
mut emit: impl FnMut(FileWindow) -> bool,
) -> bool {
assert!(window_size > overlap, "window must exceed overlap");
if bytes.is_empty() {
return true;
}
let stride = window_size - overlap;
let total = bytes.len();
let mut offset = 0usize;
let mut base_line = 0usize;
while offset < total {
let end = (offset + window_size).min(total);
let slice = &bytes[offset..end];
let text = String::from_utf8_lossy(slice).into_owned();
if !emit(FileWindow {
offset,
base_line,
text,
}) {
return false;
}
if end >= total {
return true;
}
let next = offset + stride;
base_line += bytecount_newlines(&bytes[offset..next]);
offset = next;
}
true
}