use std::fs::{File, OpenOptions};
use std::path::Path;
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum IoMode {
#[default]
Buffered,
Direct,
}
pub(crate) fn open_page_file(path: &Path, mode: IoMode) -> Result<File> {
match mode {
IoMode::Buffered => open_buffered(path),
IoMode::Direct => open_direct(path),
}
}
fn open_buffered(path: &Path) -> Result<File> {
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)
.map_err(Error::from)
}
#[cfg(target_os = "linux")]
fn open_direct(path: &Path) -> Result<File> {
use std::os::unix::fs::OpenOptionsExt;
const O_DIRECT: i32 = 0x4000;
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.custom_flags(O_DIRECT)
.open(path)
.map_err(Error::from)
}
#[cfg(target_os = "macos")]
fn open_direct(path: &Path) -> Result<File> {
use std::os::unix::io::AsRawFd;
extern "C" {
fn fcntl(
fd: std::ffi::c_int,
cmd: std::ffi::c_int,
arg: std::ffi::c_int,
) -> std::ffi::c_int;
}
const F_NOCACHE: std::ffi::c_int = 48;
let file = open_buffered(path)?;
let fd = file.as_raw_fd();
let result = unsafe { fcntl(fd, F_NOCACHE, 1) };
if result < 0 {
return Err(Error::Io(std::io::Error::last_os_error()));
}
Ok(file)
}
#[cfg(target_os = "windows")]
fn open_direct(path: &Path) -> Result<File> {
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_NO_BUFFERING: u32 = 0x2000_0000;
const FILE_FLAG_WRITE_THROUGH: u32 = 0x8000_0000;
OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.custom_flags(FILE_FLAG_NO_BUFFERING | FILE_FLAG_WRITE_THROUGH)
.open(path)
.map_err(Error::from)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn open_direct(_path: &Path) -> Result<File> {
Err(Error::InvalidConfig(
"direct I/O is not implemented on this platform; use IoMode::Buffered",
))
}
#[cfg(test)]
mod tests {
use super::{open_page_file, IoMode};
fn tmp_path(name: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0_u128, |d| d.as_nanos());
p.push(format!("emdb-v4-io-{name}-{nanos}.bin"));
p
}
#[test]
fn buffered_open_creates_file() {
let path = tmp_path("buffered");
let opened = open_page_file(&path, IoMode::Buffered);
assert!(opened.is_ok());
let _removed = std::fs::remove_file(&path);
}
#[test]
fn buffered_round_trip_through_seek_and_write() {
use std::io::{Read, Seek, SeekFrom, Write};
let path = tmp_path("buffered-rw");
let mut file = match open_page_file(&path, IoMode::Buffered) {
Ok(f) => f,
Err(err) => panic!("open should succeed: {err}"),
};
let payload = b"emdb v4 io".to_vec();
let _ = file.write_all(&payload);
let _ = file.sync_data();
let _ = file.seek(SeekFrom::Start(0));
let mut readback = vec![0_u8; payload.len()];
let _ = file.read_exact(&mut readback);
assert_eq!(readback, payload);
let _removed = std::fs::remove_file(&path);
}
#[test]
fn io_mode_default_is_buffered() {
assert_eq!(IoMode::default(), IoMode::Buffered);
}
#[test]
fn direct_open_does_not_panic() {
let path = tmp_path("direct");
let result = open_page_file(&path, IoMode::Direct);
match result {
Ok(_file) => {}
Err(crate::Error::Io(_)) => {}
Err(crate::Error::InvalidConfig(_)) => {}
Err(other) => panic!("unexpected error variant: {other:?}"),
}
let _removed = std::fs::remove_file(&path);
}
}