use crate::backend::native::{
persistent_header::PersistentHeaderV2, types::NativeBackendError, types::NativeResult,
};
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path;
pub struct FileOperations;
impl FileOperations {
pub fn create_file<P: AsRef<Path>>(path: P) -> NativeResult<File> {
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(path)?;
Ok(file)
}
pub fn open_file<P: AsRef<Path>>(path: P) -> NativeResult<File> {
let file = OpenOptions::new().read(true).write(true).open(path)?;
Ok(file)
}
pub fn file_size(file: &File) -> NativeResult<u64> {
let metadata = file.metadata()?;
Ok(metadata.len())
}
pub fn read_bytes_direct(file: &mut File, offset: u64, buffer: &mut [u8]) -> NativeResult<()> {
Self::ensure_file_len_at_least(file, offset, buffer.len())?;
file.seek(SeekFrom::Start(offset))?;
file.read_exact(buffer)?;
Ok(())
}
pub fn write_bytes_direct(file: &mut File, offset: u64, data: &[u8]) -> NativeResult<()> {
file.seek(SeekFrom::Start(offset))?;
file.write_all(data)?;
Ok(())
}
pub fn sync(file: &File) -> NativeResult<()> {
file.sync_all()?;
Ok(())
}
fn ensure_file_len_at_least(file: &File, offset: u64, len: usize) -> NativeResult<()> {
let metadata = file.metadata()?;
let file_len = metadata.len();
if file_len < offset + len as u64 {
return Err(NativeBackendError::FileTooSmall {
size: file_len,
min_size: offset + len as u64,
});
}
Ok(())
}
pub fn validate_file_size(
file_size: u64,
persistent_header: &PersistentHeaderV2,
) -> NativeResult<()> {
if file_size == 0 {
return Err(NativeBackendError::CorruptNodeRecord {
node_id: -1,
reason: "Empty file detected".to_string(),
});
}
crate::backend::native::graph_file::validation::GraphFileValidator::validate_file_size(
file_size,
persistent_header,
)
}
pub fn read_and_validate_header(file: &mut File) -> NativeResult<PersistentHeaderV2> {
let mut header_bytes =
vec![0u8; crate::backend::native::persistent_header::PERSISTENT_HEADER_SIZE];
Self::read_bytes_direct(file, 0, &mut header_bytes)?;
crate::backend::native::graph_file::encoding::decode_persistent_header(&header_bytes)
}
pub fn write_header(file: &mut File, header: &PersistentHeaderV2) -> NativeResult<()> {
let header_bytes =
crate::backend::native::graph_file::encoding::encode_persistent_header(header)?;
Self::write_bytes_direct(file, 0, &header_bytes)?;
Self::sync(file)?;
Ok(())
}
pub fn open_disk_file_for_debug(file_path: &Path) -> NativeResult<File> {
std::fs::File::open(file_path).map_err(|e| NativeBackendError::CorruptNodeRecord {
node_id: -1,
reason: format!("Failed to open disk file for debug: {}", e),
})
}
pub fn read_node_slot_for_debug(
disk_file: &mut File,
node_slot_offset: u64,
) -> NativeResult<[u8; 32]> {
let mut node_bytes = [0u8; 32];
disk_file.seek(SeekFrom::Start(node_slot_offset))?;
disk_file.read_exact(&mut node_bytes)?;
Ok(node_bytes)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum IOMode {
Default,
ExclusiveStd,
ExclusiveMmap,
}
impl IOMode {
pub fn current() -> Self {
#[cfg(all(feature = "native-v2", feature = "v2_io_exclusive_mmap"))]
return IOMode::ExclusiveMmap;
#[cfg(all(feature = "native-v2", feature = "v2_io_exclusive_std"))]
return IOMode::ExclusiveStd;
#[cfg(not(any(
feature = "native-v2",
feature = "v2_io_exclusive_mmap",
feature = "v2_io_exclusive_std"
)))]
return IOMode::Default;
}
pub fn is_exclusive_mmap(&self) -> bool {
matches!(self, IOMode::ExclusiveMmap)
}
pub fn is_exclusive_std(&self) -> bool {
matches!(self, IOMode::ExclusiveStd)
}
pub fn is_default(&self) -> bool {
matches!(self, IOMode::Default)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempfile;
#[test]
fn test_file_size() {
use std::io::Write;
let mut temp_file = tempfile().unwrap();
temp_file.set_len(1024).unwrap();
temp_file.flush().unwrap();
let size = FileOperations::file_size(&temp_file).unwrap();
assert_eq!(size, 1024);
}
#[test]
fn test_read_write_bytes_direct() {
let mut temp_file = tempfile().unwrap();
let test_data = b"Hello, World!";
FileOperations::write_bytes_direct(&mut temp_file, 0, test_data).unwrap();
let mut buffer = vec![0u8; test_data.len()];
FileOperations::read_bytes_direct(&mut temp_file, 0, &mut buffer).unwrap();
assert_eq!(buffer, test_data);
}
#[test]
fn test_ensure_file_len_at_least_success() {
let mut temp_file = tempfile().unwrap();
temp_file.set_len(100).unwrap();
assert!(FileOperations::ensure_file_len_at_least(&temp_file, 50, 10).is_ok());
assert!(FileOperations::ensure_file_len_at_least(&temp_file, 100, 0).is_ok());
}
#[test]
fn test_ensure_file_len_at_least_failure() {
let mut temp_file = tempfile().unwrap();
temp_file.set_len(50).unwrap();
let result = FileOperations::ensure_file_len_at_least(&temp_file, 40, 20);
assert!(result.is_err());
}
#[test]
fn test_io_mode_detection() {
let mode = IOMode::current();
assert!(matches!(
mode,
IOMode::Default | IOMode::ExclusiveStd | IOMode::ExclusiveMmap
));
}
#[test]
fn test_validate_file_size_empty() {
let header = PersistentHeaderV2::new_v2();
let result = FileOperations::validate_file_size(0, &header);
assert!(result.is_err());
}
#[test]
fn test_validate_file_size_valid() {
let mut header = PersistentHeaderV2::new_v2();
header.node_data_offset = 1024;
let result = FileOperations::validate_file_size(2048, &header);
assert!(result.is_ok());
}
#[test]
fn test_read_node_slot_for_debug() {
use std::io::{Seek, SeekFrom, Write};
let mut temp_file = tempfile().unwrap();
let test_pattern = [0x42u8; 32];
let slot_offset = 0x1000; temp_file.seek(SeekFrom::Start(slot_offset)).unwrap();
temp_file.write_all(&test_pattern).unwrap();
temp_file.flush().unwrap();
let read_pattern =
FileOperations::read_node_slot_for_debug(&mut temp_file, slot_offset).unwrap();
assert_eq!(read_pattern, test_pattern);
}
}