use memmap2::MmapOptions;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use super::decode::decode_text_file_owned_or_bytes;
use super::MMAP_TOCTOU_SANITY_CAP_BYTES;
pub(in crate::filesystem) enum BufferedFileRead {
Text(String),
Bytes(Vec<u8>),
Mmap(memmap2::Mmap),
}
const MAX_EXACT_SIZED_READ_PREALLOC_BYTES: u64 = 16 * 1024 * 1024;
pub(super) const MAX_BUFFERED_READ_BYTES: u64 = MMAP_TOCTOU_SANITY_CAP_BYTES;
pub(in crate::filesystem) fn read_file_buffered(
path: &Path,
size_hint: u64,
) -> Option<BufferedFileRead> {
let bytes = match read_file_safe(path, size_hint) {
Ok(b) => b,
Err(error) => {
tracing::warn!(
path = %path.display(),
%error,
"cannot read file; skipping"
);
let skip = if error.kind() == std::io::ErrorKind::InvalidData {
crate::SourceSkipEvent::OverMaxSize
} else {
crate::SourceSkipEvent::Unreadable
};
let _event = crate::record_skip_event(skip);
return None;
}
};
match decode_text_file_owned_or_bytes(bytes) {
Ok(text) => Some(BufferedFileRead::Text(text)),
Err(bytes) => Some(BufferedFileRead::Bytes(bytes)),
}
}
pub(crate) fn open_file_safe(path: &Path) -> std::io::Result<File> {
let mut options = std::fs::OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
}
#[cfg(windows)]
{
let meta = std::fs::symlink_metadata(path)?;
if meta.file_type().is_symlink() {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"refusing to follow symlink (Windows safety guard)",
));
}
}
let file = options.open(path)?;
if !file.metadata()?.is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"refusing to read a non-regular file (FIFO, socket, or device)",
));
}
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
if unsafe { libc::flock(fd, libc::LOCK_SH | libc::LOCK_NB) } != 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"file is locked by another process",
));
}
}
Ok(file)
}
pub(in crate::filesystem) fn read_file_prefix_safe(
path: &Path,
buf: &mut [u8],
) -> std::io::Result<usize> {
let mut file = open_file_safe(path)?;
let mut filled = 0;
while filled < buf.len() {
match file.read(&mut buf[filled..]) {
Ok(0) => break,
Ok(n) => filled += n,
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
Ok(filled)
}
pub(in crate::filesystem) fn read_file_safe(
path: &Path,
size_hint: u64,
) -> std::io::Result<Vec<u8>> {
let file = open_file_safe(path)?;
#[cfg(target_os = "linux")]
{
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
unsafe { libc::posix_fadvise(fd, 0, 0, libc::POSIX_FADV_SEQUENTIAL) };
}
let cap = size_hint.min(MAX_BUFFERED_READ_BYTES);
if cap == 0 {
let read = crate::capped_read::read_to_cap(file, MAX_BUFFERED_READ_BYTES, None)?;
if read.truncated {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!(
"filesystem buffered read exceeded {} byte cap",
MAX_BUFFERED_READ_BYTES
),
));
}
return Ok(read.bytes);
}
if cap <= MAX_EXACT_SIZED_READ_PREALLOC_BYTES {
return read_exact_stat_sized_with_growth_probe(file, cap);
}
let read = crate::capped_read::read_to_cap(file, cap, Some(cap))?;
if read.truncated {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
buffered_read_exceeded_cap_message(size_hint, cap),
));
}
Ok(read.bytes)
}
fn read_exact_stat_sized_with_growth_probe(mut file: File, cap: u64) -> std::io::Result<Vec<u8>> {
let cap_usize = usize::try_from(cap).map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("filesystem buffered read cap is not addressable on this platform: {error}"),
)
})?;
let mut bytes = vec![0u8; cap_usize];
let mut filled = 0;
while filled < cap_usize {
match file.read(&mut bytes[filled..]) {
Ok(0) => break,
Ok(n) => filled += n,
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
bytes.truncate(filled);
if filled == cap_usize {
let mut sentinel = [0u8; 1];
loop {
match file.read(&mut sentinel) {
Ok(0) => break,
Ok(_) => {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
buffered_read_exceeded_cap_message(cap, cap),
));
}
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
}
Ok(bytes)
}
fn buffered_read_exceeded_cap_message(size_hint: u64, cap: u64) -> String {
if size_hint <= MAX_BUFFERED_READ_BYTES {
format!("filesystem buffered read exceeded stat-time {size_hint} byte cap")
} else {
format!(
"filesystem buffered read exceeded {} byte sanity cap after stat-time size {size_hint}",
cap
)
}
}
pub(in crate::filesystem) fn read_file_mmap(path: &Path) -> Option<BufferedFileRead> {
let mut file = match open_file_safe(path) {
Ok(f) => f,
Err(error) => {
tracing::warn!(
path = %path.display(),
%error,
"cannot open file for mmap; skipping"
);
let _event = crate::record_skip_event(crate::SourceSkipEvent::Unreadable);
return None;
}
};
let meta = match file.metadata() {
Ok(meta) => meta,
Err(error) => {
tracing::warn!(
path = %path.display(),
%error,
"cannot stat opened file for mmap sanity cap; skipping"
);
let _event = crate::record_skip_event(crate::SourceSkipEvent::Unreadable);
return None;
}
};
let live_size_hint = Some(meta.len());
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 mmap file: live size exceeds sanity cap (likely TOCTOU growth)"
);
let _event = crate::record_skip_event(crate::SourceSkipEvent::OverMaxSize);
return None;
}
let mmap = match unsafe { MmapOptions::new().map(&file) } {
Ok(m) => m,
Err(error) => {
tracing::warn!(
path = %path.display(),
%error,
"cannot mmap file; falling back to buffered read"
);
match crate::capped_read::read_to_cap(
&mut file,
MMAP_TOCTOU_SANITY_CAP_BYTES,
live_size_hint,
) {
Ok(read) => {
if read.truncated {
tracing::warn!(
path = %path.display(),
cap = MMAP_TOCTOU_SANITY_CAP_BYTES,
"file grew beyond mmap fallback sanity cap while reading"
);
let _event = crate::record_skip_event(crate::SourceSkipEvent::OverMaxSize);
return None;
}
return Some(match decode_text_file_owned_or_bytes(read.bytes) {
Ok(text) => BufferedFileRead::Text(text),
Err(bytes) => BufferedFileRead::Bytes(bytes),
});
}
Err(error) => {
tracing::warn!(
path = %path.display(),
%error,
"cannot read file after mmap failure; skipping"
);
let _event = crate::record_skip_event(crate::SourceSkipEvent::Unreadable);
return None;
}
}
}
};
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 file length for mmap sanity cap; skipping"
);
let _event = crate::record_skip_event(crate::SourceSkipEvent::OverMaxSize);
return None;
}
};
if mapped_len > MMAP_TOCTOU_SANITY_CAP_BYTES {
tracing::warn!(
path = %path.display(),
live_size = mapped_len,
cap = MMAP_TOCTOU_SANITY_CAP_BYTES,
"refusing to mmap file: mapped length exceeds sanity cap (likely TOCTOU growth)"
);
let _event = crate::record_skip_event(crate::SourceSkipEvent::OverMaxSize);
return None;
}
#[cfg(unix)]
{
unsafe {
libc::madvise(
mmap.as_ptr() as *mut libc::c_void,
mmap.len(),
libc::MADV_SEQUENTIAL,
);
}
}
let result = match super::decode::decode_text_file(&mmap) {
Some(text) => BufferedFileRead::Text(text),
None => BufferedFileRead::Mmap(mmap),
};
#[cfg(unix)]
{
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
unsafe { libc::flock(fd, libc::LOCK_UN) };
}
Some(result)
}