use crate::error::Error;
use std::fs;
use std::io;
use std::path::Path;
#[cfg(feature = "cli")]
use std::io::Read;
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
pub const MAX_FILE_SIZE: u64 = 100 * 1024 * 1024;
const _: () = assert!(
MAX_FILE_SIZE <= usize::MAX as u64,
"MAX_FILE_SIZE must fit in usize for buffer allocation"
);
pub fn open_file_safe(path: &Path) -> crate::Result<fs::File> {
let mut options = fs::OpenOptions::new();
let _ = options.read(true);
#[cfg(unix)]
{
let _ =
options.custom_flags(luff_sys::constants::O_NOFOLLOW | luff_sys::constants::O_NONBLOCK);
}
let file_result = options.open(path);
let file = match file_result {
Ok(f) => f,
Err(e) => {
if e.kind() == io::ErrorKind::NotFound {
return Err(Error::FileNotFound {
path: path.to_path_buf(),
});
}
#[cfg(unix)]
if e.raw_os_error() == Some(luff_sys::constants::ELOOP) {
return Err(Error::NotARegularFile {
path: path.to_path_buf(),
});
}
return Err(Error::Io(e));
}
};
let metadata = file.metadata()?;
if !metadata.is_file() {
return Err(Error::NotARegularFile {
path: path.to_path_buf(),
});
}
let file_size = metadata.len();
if file_size > MAX_FILE_SIZE {
return Err(Error::FileTooLarge {
path: path.to_path_buf(),
size: file_size,
max_size: MAX_FILE_SIZE,
});
}
let file = luff_sys::drop_nonblock(file).map_err(Error::Io)?;
Ok(file)
}
#[cfg(feature = "cli")]
pub fn read_file_safe(path: &Path) -> crate::Result<String> {
let path_buf = path.to_path_buf();
let result = super::timeout::with_timeout(move || -> crate::Result<String> {
let file = open_file_safe(&path_buf)?;
let file_size = file.metadata().map_err(Error::Io)?.len();
let mut limited = file.take(MAX_FILE_SIZE + 1);
let capacity = usize::try_from(file_size).unwrap_or(usize::MAX);
let mut contents = String::with_capacity(capacity);
match limited.read_to_string(&mut contents) {
Ok(bytes_read) => {
let max_size = usize::try_from(MAX_FILE_SIZE).unwrap_or(usize::MAX);
if bytes_read > max_size {
Err(Error::FileTooLarge {
path: path_buf,
size: bytes_read as u64,
max_size: MAX_FILE_SIZE,
})
} else {
Ok(contents)
}
}
Err(e) if e.kind() == io::ErrorKind::InvalidData => {
Err(Error::InvalidUtf8 { path: path_buf })
}
Err(e) => Err(Error::Io(e)),
}
});
match result {
Ok(inner) => inner,
Err(io_err) => Err(Error::Io(io_err)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[cfg(feature = "cli")]
#[test]
fn test_read_file_safe() {
let temp = TempDir::new().unwrap();
let file_path = temp.path().join("test.txt");
fs::write(&file_path, "Hello, World!").unwrap();
let contents = read_file_safe(&file_path).unwrap();
assert_eq!(contents, "Hello, World!");
}
#[cfg(feature = "cli")]
#[test]
fn test_read_file_safe_nonexistent() {
let path = Path::new("/nonexistent/file.txt");
let err = read_file_safe(path).unwrap_err();
assert!(matches!(err, Error::FileNotFound { .. }));
}
#[cfg(feature = "cli")]
#[test]
fn test_read_file_safe_rejects_directory() {
let temp = TempDir::new().unwrap();
let result = read_file_safe(temp.path());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
Error::Io(_) | Error::NotARegularFile { .. }
));
}
#[cfg(feature = "cli")]
#[test]
fn test_read_file_safe_reports_size_error() {
let temp = TempDir::new().unwrap();
let large_path = temp.path().join("large.bin");
#[allow(clippy::cast_possible_truncation)]
fs::write(
&large_path,
vec![0u8; usize::try_from(MAX_FILE_SIZE + 1).unwrap()],
)
.unwrap();
let result = read_file_safe(&large_path);
assert!(result.is_err());
if let Err(Error::FileTooLarge {
path,
size,
max_size,
}) = result
{
assert_eq!(path, large_path);
assert_eq!(size, MAX_FILE_SIZE + 1);
assert_eq!(max_size, MAX_FILE_SIZE);
} else {
panic!("Expected FileTooLarge error");
}
}
#[test]
fn test_max_file_size_constant() {
assert_eq!(MAX_FILE_SIZE, 100 * 1024 * 1024);
}
#[test]
fn test_open_file_safe() {
let temp = TempDir::new().unwrap();
let file_path = temp.path().join("test.txt");
fs::write(&file_path, "content").unwrap();
let file = open_file_safe(&file_path).unwrap();
assert!(file.metadata().unwrap().is_file());
}
}