use memmap2::{Mmap, MmapOptions};
use std::fs::File;
use std::path::{Path, PathBuf};
use thiserror::Error;
type BufferOffset = usize;
type BufferLength = usize;
type FileSize = u64;
trait SafeBufferAccess {
fn validate_access(&self, offset: BufferOffset, length: BufferLength) -> Result<(), IoError>;
fn get_safe_slice(&self, offset: BufferOffset, length: BufferLength) -> Result<&[u8], IoError>;
}
impl SafeBufferAccess for [u8] {
fn validate_access(&self, offset: BufferOffset, length: BufferLength) -> Result<(), IoError> {
validate_buffer_access(self.len(), offset, length)
}
fn get_safe_slice(&self, offset: BufferOffset, length: BufferLength) -> Result<&[u8], IoError> {
self.validate_access(offset, length)?;
let end_offset = offset + length; Ok(&self[offset..end_offset])
}
}
#[derive(Debug, Error)]
pub enum IoError {
#[error("Failed to open file '{path}': {source}")]
FileOpenError {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("Failed to memory-map file '{path}': {source}")]
MmapError {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("File '{path}' is empty")]
EmptyFile {
path: PathBuf,
},
#[error("File '{path}' is too large ({size} bytes, maximum {max_size} bytes)")]
FileTooLarge {
path: PathBuf,
size: FileSize,
max_size: FileSize,
},
#[error("Failed to read metadata for file '{path}': {source}")]
MetadataError {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error(
"Buffer access out of bounds: offset {offset} + length {length} > buffer size {buffer_size}"
)]
BufferOverrun {
offset: BufferOffset,
length: BufferLength,
buffer_size: BufferLength,
},
#[error("Invalid buffer access parameters: offset {offset}, length {length}")]
InvalidAccess {
offset: BufferOffset,
length: BufferLength,
},
#[error("File '{path}' is not a regular file (file type: {file_type})")]
InvalidFileType {
path: PathBuf,
file_type: String,
},
}
#[derive(Debug)]
pub struct FileBuffer {
mmap: Mmap,
path: PathBuf,
}
impl FileBuffer {
const MAX_FILE_SIZE: FileSize = 1024 * 1024 * 1024;
#[allow(dead_code)]
const MAX_CONCURRENT_MAPPINGS: usize = 100;
#[allow(dead_code)]
const SMALL_FILE_THRESHOLD: u64 = 4096;
pub fn new(path: &Path) -> Result<Self, IoError> {
let path_buf = path.to_path_buf();
let file = Self::open_file(path, &path_buf)?;
Self::validate_file_metadata(&file, &path_buf)?;
let mmap = Self::create_memory_mapping(&file, &path_buf)?;
Ok(Self {
mmap,
path: path_buf,
})
}
fn open_file(path: &Path, path_buf: &Path) -> Result<File, IoError> {
File::open(path).map_err(|source| IoError::FileOpenError {
path: path_buf.to_path_buf(),
source,
})
}
fn validate_file_metadata(_file: &File, path_buf: &Path) -> Result<(), IoError> {
let canonical_path =
std::fs::canonicalize(path_buf).map_err(|source| IoError::MetadataError {
path: path_buf.to_path_buf(),
source,
})?;
let metadata =
std::fs::metadata(&canonical_path).map_err(|source| IoError::MetadataError {
path: canonical_path.clone(),
source,
})?;
if !metadata.is_file() {
let file_type = if metadata.is_dir() {
"directory".to_string()
} else if metadata.is_symlink() {
"symlink".to_string()
} else {
Self::detect_special_file_type(&metadata)
};
return Err(IoError::InvalidFileType {
path: canonical_path,
file_type,
});
}
let file_size = metadata.len();
if file_size == 0 {
return Err(IoError::EmptyFile {
path: canonical_path,
});
}
if file_size > Self::MAX_FILE_SIZE {
return Err(IoError::FileTooLarge {
path: canonical_path,
size: file_size,
max_size: Self::MAX_FILE_SIZE,
});
}
Ok(())
}
fn detect_special_file_type(metadata: &std::fs::Metadata) -> String {
#[cfg(unix)]
{
use std::os::unix::fs::FileTypeExt;
if metadata.file_type().is_block_device() {
"block device".to_string()
} else if metadata.file_type().is_char_device() {
"character device".to_string()
} else if metadata.file_type().is_fifo() {
"FIFO/pipe".to_string()
} else if metadata.file_type().is_socket() {
"socket".to_string()
} else {
"special file".to_string()
}
}
#[cfg(windows)]
{
if metadata.file_type().is_symlink() {
"symlink".to_string()
} else {
"special file".to_string()
}
}
#[cfg(not(any(unix, windows)))]
{
"special file".to_string()
}
}
pub fn create_symlink<P: AsRef<std::path::Path>, Q: AsRef<std::path::Path>>(
original: P,
link: Q,
) -> Result<(), std::io::Error> {
#[cfg(unix)]
{
std::os::unix::fs::symlink(original, link)
}
#[cfg(windows)]
{
let original_path = original.as_ref();
if original_path.is_dir() {
std::os::windows::fs::symlink_dir(original, link)
} else {
std::os::windows::fs::symlink_file(original, link)
}
}
#[cfg(not(any(unix, windows)))]
{
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"Symlinks not supported on this platform",
))
}
}
fn create_memory_mapping(file: &File, path_buf: &Path) -> Result<Mmap, IoError> {
#[allow(unsafe_code)]
unsafe {
MmapOptions::new().map(file).map_err(|source| {
let sanitized_path = path_buf.file_name().map_or_else(
|| "<unknown>".to_string(),
|name| name.to_string_lossy().into_owned(),
);
IoError::MmapError {
path: PathBuf::from(sanitized_path),
source,
}
})
}
}
#[must_use]
pub fn as_slice(&self) -> &[u8] {
&self.mmap
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn len(&self) -> usize {
self.mmap.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.mmap.is_empty()
}
}
pub fn safe_read_bytes(
buffer: &[u8],
offset: BufferOffset,
length: BufferLength,
) -> Result<&[u8], IoError> {
buffer.get_safe_slice(offset, length)
}
pub fn safe_read_byte(buffer: &[u8], offset: BufferOffset) -> Result<u8, IoError> {
buffer.get(offset).copied().ok_or(IoError::BufferOverrun {
offset,
length: 1,
buffer_size: buffer.len(),
})
}
pub fn validate_buffer_access(
buffer_size: BufferLength,
offset: BufferOffset,
length: BufferLength,
) -> Result<(), IoError> {
if length == 0 {
return Err(IoError::InvalidAccess { offset, length });
}
if offset >= buffer_size {
return Err(IoError::BufferOverrun {
offset,
length,
buffer_size,
});
}
let end_offset = offset
.checked_add(length)
.ok_or(IoError::InvalidAccess { offset, length })?;
if end_offset > buffer_size {
return Err(IoError::BufferOverrun {
offset,
length,
buffer_size,
});
}
Ok(())
}
impl Drop for FileBuffer {
fn drop(&mut self) {
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
use std::sync::atomic::{AtomicU64, Ordering};
static TEMP_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
fn create_temp_file(content: &[u8]) -> PathBuf {
let temp_dir = std::env::temp_dir();
let id = TEMP_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
let file_path = temp_dir.join(format!("libmagic_test_{}_{id}", std::process::id()));
{
let mut file = File::create(&file_path).expect("Failed to create temp file");
file.write_all(content).expect("Failed to write temp file");
file.sync_all().expect("Failed to sync temp file");
}
file_path
}
fn cleanup_temp_file(path: &Path) {
let _ = fs::remove_file(path);
}
#[test]
fn test_file_buffer_creation_success() {
let content = b"Hello, World!";
let temp_path = create_temp_file(content);
let buffer = FileBuffer::new(&temp_path).expect("Failed to create FileBuffer");
assert_eq!(buffer.as_slice(), content);
assert_eq!(buffer.len(), content.len());
assert!(!buffer.is_empty());
assert_eq!(buffer.path(), temp_path.as_path());
cleanup_temp_file(&temp_path);
}
#[test]
fn test_file_buffer_nonexistent_file() {
let nonexistent_path = Path::new("/nonexistent/file.bin");
let result = FileBuffer::new(nonexistent_path);
assert!(result.is_err());
match result.unwrap_err() {
IoError::FileOpenError { path, .. } => {
assert_eq!(path, nonexistent_path);
}
other => panic!("Expected FileOpenError, got {other:?}"),
}
}
#[test]
fn test_file_buffer_empty_file() {
let temp_path = create_temp_file(&[]);
let result = FileBuffer::new(&temp_path);
assert!(result.is_err());
match result.unwrap_err() {
IoError::EmptyFile { path } => {
let canonical_temp_path = std::fs::canonicalize(&temp_path).unwrap();
assert_eq!(path, canonical_temp_path);
}
other => panic!("Expected EmptyFile error, got {other:?}"),
}
cleanup_temp_file(&temp_path);
}
#[test]
fn test_file_buffer_large_file() {
let content = vec![0u8; 1024]; let temp_path = create_temp_file(&content);
let buffer =
FileBuffer::new(&temp_path).expect("Failed to create FileBuffer for normal file");
assert_eq!(buffer.len(), 1024);
cleanup_temp_file(&temp_path);
}
#[test]
fn test_file_buffer_binary_content() {
let content = vec![0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD, 0xFC];
let temp_path = create_temp_file(&content);
let buffer = FileBuffer::new(&temp_path).expect("Failed to create FileBuffer");
assert_eq!(buffer.as_slice(), content.as_slice());
assert_eq!(buffer.as_slice()[0], 0x00);
assert_eq!(buffer.as_slice()[7], 0xFC);
cleanup_temp_file(&temp_path);
}
#[test]
fn test_io_error_display() {
let path = PathBuf::from("/test/path");
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
let error = IoError::FileOpenError {
path,
source: io_err,
};
let error_string = format!("{error}");
assert!(error_string.contains("/test/path"));
assert!(error_string.contains("Failed to open file"));
}
#[test]
fn test_empty_file_error_display() {
let path = PathBuf::from("/test/empty.bin");
let error = IoError::EmptyFile { path };
let error_string = format!("{error}");
assert!(error_string.contains("/test/empty.bin"));
assert!(error_string.contains("is empty"));
}
#[test]
fn test_file_too_large_error_display() {
let path = PathBuf::from("/test/large.bin");
let error = IoError::FileTooLarge {
path,
size: 2_000_000_000,
max_size: 1_000_000_000,
};
let error_string = format!("{error}");
assert!(error_string.contains("/test/large.bin"));
assert!(error_string.contains("too large"));
assert!(error_string.contains("2000000000"));
assert!(error_string.contains("1000000000"));
}
#[test]
fn test_safe_read_bytes_success() {
let buffer = b"Hello, World!";
let result = safe_read_bytes(buffer, 0, 5).expect("Failed to read bytes");
assert_eq!(result, b"Hello");
let result = safe_read_bytes(buffer, 7, 5).expect("Failed to read bytes");
assert_eq!(result, b"World");
let result = safe_read_bytes(buffer, 0, 1).expect("Failed to read bytes");
assert_eq!(result, b"H");
let result = safe_read_bytes(buffer, 0, buffer.len()).expect("Failed to read bytes");
assert_eq!(result, buffer);
let result = safe_read_bytes(buffer, buffer.len() - 1, 1).expect("Failed to read bytes");
assert_eq!(result, b"!");
}
#[test]
fn test_safe_read_bytes_out_of_bounds() {
let buffer = b"Hello";
let result = safe_read_bytes(buffer, 10, 1);
assert!(result.is_err());
match result.unwrap_err() {
IoError::BufferOverrun {
offset,
length,
buffer_size,
} => {
assert_eq!(offset, 10);
assert_eq!(length, 1);
assert_eq!(buffer_size, 5);
}
other => panic!("Expected BufferOverrun, got {other:?}"),
}
let result = safe_read_bytes(buffer, 3, 5);
assert!(result.is_err());
match result.unwrap_err() {
IoError::BufferOverrun {
offset,
length,
buffer_size,
} => {
assert_eq!(offset, 3);
assert_eq!(length, 5);
assert_eq!(buffer_size, 5);
}
other => panic!("Expected BufferOverrun, got {other:?}"),
}
let result = safe_read_bytes(buffer, 5, 1);
assert!(result.is_err());
}
#[test]
fn test_safe_read_bytes_zero_length() {
let buffer = b"Hello";
let result = safe_read_bytes(buffer, 0, 0);
assert!(result.is_err());
match result.unwrap_err() {
IoError::InvalidAccess { offset, length } => {
assert_eq!(offset, 0);
assert_eq!(length, 0);
}
other => panic!("Expected InvalidAccess, got {other:?}"),
}
}
#[test]
fn test_safe_read_bytes_overflow() {
let buffer = b"Hello";
let result = safe_read_bytes(buffer, usize::MAX, 1);
assert!(result.is_err());
match result.unwrap_err() {
IoError::BufferOverrun { .. } => {
}
other => panic!("Expected BufferOverrun, got {other:?}"),
}
let result = safe_read_bytes(buffer, 1, usize::MAX);
assert!(result.is_err());
match result.unwrap_err() {
IoError::InvalidAccess { .. } => {
}
other => panic!("Expected InvalidAccess, got {other:?}"),
}
let result = safe_read_bytes(buffer, 2, usize::MAX - 1);
assert!(result.is_err());
match result.unwrap_err() {
IoError::InvalidAccess { .. } => {
}
other => panic!("Expected InvalidAccess, got {other:?}"),
}
}
#[test]
fn test_safe_read_byte_success() {
let buffer = b"Hello";
assert_eq!(safe_read_byte(buffer, 0).unwrap(), b'H');
assert_eq!(safe_read_byte(buffer, 1).unwrap(), b'e');
assert_eq!(safe_read_byte(buffer, 4).unwrap(), b'o');
}
#[test]
fn test_safe_read_byte_out_of_bounds() {
let buffer = b"Hello";
let result = safe_read_byte(buffer, 5);
assert!(result.is_err());
match result.unwrap_err() {
IoError::BufferOverrun {
offset,
length,
buffer_size,
} => {
assert_eq!(offset, 5);
assert_eq!(length, 1);
assert_eq!(buffer_size, 5);
}
other => panic!("Expected BufferOverrun, got {other:?}"),
}
let result = safe_read_byte(buffer, 100);
assert!(result.is_err());
}
#[test]
fn test_validate_buffer_access_success() {
validate_buffer_access(100, 0, 50).expect("Should be valid");
validate_buffer_access(100, 50, 50).expect("Should be valid");
validate_buffer_access(100, 99, 1).expect("Should be valid");
validate_buffer_access(10, 0, 10).expect("Should be valid");
validate_buffer_access(1, 0, 1).expect("Should be valid");
}
#[test]
fn test_validate_buffer_access_invalid() {
let result = validate_buffer_access(100, 0, 0);
assert!(result.is_err());
let result = validate_buffer_access(100, 100, 1);
assert!(result.is_err());
let result = validate_buffer_access(100, 50, 51);
assert!(result.is_err());
let result = validate_buffer_access(100, usize::MAX, 1);
assert!(result.is_err());
let result = validate_buffer_access(100, 1, usize::MAX);
assert!(result.is_err());
}
#[test]
fn test_validate_buffer_access_edge_cases() {
let result = validate_buffer_access(0, 0, 1);
assert!(result.is_err());
let large_size = 1_000_000;
validate_buffer_access(large_size, 0, large_size).expect("Should be valid");
validate_buffer_access(large_size, large_size - 1, 1).expect("Should be valid");
let result = validate_buffer_access(large_size, large_size - 1, 2);
assert!(result.is_err());
}
#[test]
fn test_buffer_access_security_patterns() {
let buffer_size = 1024;
let overflow_patterns = vec![
(usize::MAX, 1), (buffer_size, usize::MAX), (usize::MAX - 1, 2), ];
for (offset, length) in overflow_patterns {
let result = validate_buffer_access(buffer_size, offset, length);
assert!(
result.is_err(),
"Should reject potentially dangerous access pattern: offset={offset}, length={length}"
);
}
let safe_patterns = vec![
(0, 1), (buffer_size - 1, 1), (buffer_size / 2, 1), ];
for (offset, length) in safe_patterns {
let result = validate_buffer_access(buffer_size, offset, length);
assert!(
result.is_ok(),
"Should accept safe access pattern: offset={offset}, length={length}"
);
}
}
#[test]
fn test_buffer_overrun_error_display() {
let error = IoError::BufferOverrun {
offset: 10,
length: 5,
buffer_size: 12,
};
let error_string = format!("{error}");
assert!(error_string.contains("Buffer access out of bounds"));
assert!(error_string.contains("offset 10"));
assert!(error_string.contains("length 5"));
assert!(error_string.contains("buffer size 12"));
}
#[test]
fn test_invalid_access_error_display() {
let error = IoError::InvalidAccess {
offset: 0,
length: 0,
};
let error_string = format!("{error}");
assert!(error_string.contains("Invalid buffer access parameters"));
assert!(error_string.contains("offset 0"));
assert!(error_string.contains("length 0"));
}
#[test]
fn test_invalid_file_type_error_display() {
let error = IoError::InvalidFileType {
path: std::path::PathBuf::from("/dev/null"),
file_type: "character device".to_string(),
};
let error_string = format!("{error}");
assert!(error_string.contains("is not a regular file"));
assert!(error_string.contains("/dev/null"));
assert!(error_string.contains("character device"));
}
#[test]
fn test_file_buffer_directory_rejection() {
let temp_dir = std::env::temp_dir().join("test_dir_12345");
std::fs::create_dir_all(&temp_dir).unwrap();
let result = FileBuffer::new(&temp_dir);
assert!(result.is_err());
match result.unwrap_err() {
IoError::InvalidFileType { path, file_type } => {
assert_eq!(file_type, "directory");
let canonical_temp_dir = std::fs::canonicalize(&temp_dir).unwrap();
assert_eq!(path, canonical_temp_dir);
}
IoError::FileOpenError { .. } => {
println!(
"Directory test skipped on this platform (can't open directories as files)"
);
}
other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
}
std::fs::remove_dir(&temp_dir).unwrap();
}
#[test]
fn test_file_buffer_symlink_to_directory_rejection() {
let temp_dir = std::env::temp_dir().join("test_dir_symlink_12345");
let symlink_path = std::env::temp_dir().join("test_symlink_12345");
std::fs::create_dir_all(&temp_dir).unwrap();
let symlink_result = FileBuffer::create_symlink(&temp_dir, &symlink_path);
match symlink_result {
Ok(()) => {
let result = FileBuffer::new(&symlink_path);
assert!(result.is_err());
match result.unwrap_err() {
IoError::InvalidFileType { path, file_type } => {
assert_eq!(file_type, "directory");
let canonical_temp_dir = std::fs::canonicalize(&temp_dir).unwrap();
assert_eq!(path, canonical_temp_dir);
}
IoError::FileOpenError { .. } => {
println!(
"Directory symlink test skipped on this platform (can't open directories as files)"
);
}
other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
}
let _ = std::fs::remove_file(&symlink_path);
}
Err(_) => {
println!(
"Skipping symlink test - unable to create symlink (may need admin privileges)"
);
}
}
std::fs::remove_dir(&temp_dir).unwrap();
}
#[test]
fn test_file_buffer_symlink_to_regular_file_success() {
let temp_file = std::env::temp_dir().join("test_file_symlink_12345");
let symlink_path = std::env::temp_dir().join("test_symlink_file_12345");
let content = b"test content";
std::fs::write(&temp_file, content).unwrap();
let symlink_result = FileBuffer::create_symlink(&temp_file, &symlink_path);
match symlink_result {
Ok(()) => {
let result = FileBuffer::new(&symlink_path);
assert!(result.is_ok());
let buffer = result.unwrap();
assert_eq!(buffer.as_slice(), content);
let _ = std::fs::remove_file(&symlink_path);
}
Err(_) => {
println!(
"Skipping symlink test - unable to create symlink (may need admin privileges)"
);
}
}
std::fs::remove_file(&temp_file).unwrap();
}
#[test]
fn test_file_buffer_special_files_rejection() {
#[cfg(unix)]
{
let result = FileBuffer::new(std::path::Path::new("/dev/null"));
assert!(result.is_err());
match result.unwrap_err() {
IoError::InvalidFileType { path, file_type } => {
assert_eq!(file_type, "character device");
assert_eq!(path, std::path::PathBuf::from("/dev/null"));
}
other => panic!("Expected InvalidFileType error, got {other:?}"),
}
let result = FileBuffer::new(std::path::Path::new("/dev/zero"));
assert!(result.is_err());
match result.unwrap_err() {
IoError::InvalidFileType { path, file_type } => {
assert_eq!(file_type, "character device");
assert_eq!(path, std::path::PathBuf::from("/dev/zero"));
}
other => panic!("Expected InvalidFileType error, got {other:?}"),
}
let result = FileBuffer::new(std::path::Path::new("/dev/random"));
assert!(result.is_err());
match result.unwrap_err() {
IoError::InvalidFileType { path, file_type } => {
assert_eq!(file_type, "character device");
assert_eq!(path, std::path::PathBuf::from("/dev/random"));
}
other => panic!("Expected InvalidFileType error, got {other:?}"),
}
}
#[cfg(not(unix))]
{
println!("Skipping special file tests on non-Unix platform");
}
}
#[test]
fn test_file_buffer_cross_platform_special_files() {
let temp_dir = std::env::temp_dir().join("test_special_dir_12345");
std::fs::create_dir_all(&temp_dir).unwrap();
let result = FileBuffer::new(&temp_dir);
assert!(result.is_err());
match result.unwrap_err() {
IoError::InvalidFileType { path, file_type } => {
assert_eq!(file_type, "directory");
let canonical_temp_dir = std::fs::canonicalize(&temp_dir).unwrap();
assert_eq!(path, canonical_temp_dir);
}
IoError::FileOpenError { .. } => {
println!(
"Directory test skipped on this platform (can't open directories as files)"
);
}
other => panic!("Expected InvalidFileType or FileOpenError, got {other:?}"),
}
std::fs::remove_dir(&temp_dir).unwrap();
}
#[test]
#[ignore = "FIFOs can cause hanging issues in CI environments"]
fn test_file_buffer_fifo_rejection() {
#[cfg(unix)]
{
use nix::unistd;
let fifo_path = std::env::temp_dir().join("test_fifo_12345");
match unistd::mkfifo(
&fifo_path,
nix::sys::stat::Mode::S_IRUSR | nix::sys::stat::Mode::S_IWUSR,
) {
Ok(()) => {
let result = FileBuffer::new(&fifo_path);
assert!(result.is_err());
match result.unwrap_err() {
IoError::InvalidFileType { path, file_type } => {
assert_eq!(file_type, "FIFO/pipe");
let canonical_fifo_path = std::fs::canonicalize(&fifo_path).unwrap();
assert_eq!(path, canonical_fifo_path);
}
other => panic!("Expected InvalidFileType error, got {other:?}"),
}
std::fs::remove_file(&fifo_path).unwrap();
}
Err(_) => {
println!("Skipping FIFO test - unable to create FIFO");
}
}
}
#[cfg(not(unix))]
{
println!("Skipping FIFO test on non-Unix platform");
}
}
}