use std::fs::{File, OpenOptions};
use std::path::Path;
use crate::error::{DbError, DbResult};
use crate::io::aligned_buf::AlignedBuf;
fn aligned_read_exact(
file: &File,
buf: &mut [u8],
aligned_offset: u64,
needed: usize,
) -> DbResult<()> {
use std::os::unix::fs::FileExt;
const SECTOR: usize = 4096;
let mut total_read = 0usize;
while total_read < needed {
let aligned_cursor = total_read & !(SECTOR - 1);
let r = file.read_at(
&mut buf[aligned_cursor..],
aligned_offset + aligned_cursor as u64,
)?;
if r == 0 {
break; }
let new_total = aligned_cursor + r;
if new_total <= total_read {
break;
}
total_read = new_total;
}
if total_read < needed {
return Err(DbError::Io(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"failed to read full value",
)));
}
Ok(())
}
#[cfg(target_os = "macos")]
unsafe extern "C" {
fn fcntl(fd: std::os::raw::c_int, cmd: std::os::raw::c_int, ...) -> std::os::raw::c_int;
}
pub fn pread_value(file: &File, offset: u64, len: usize) -> DbResult<Vec<u8>> {
let sector_size = 4096;
let aligned_offset = offset & !(sector_size - 1);
let diff = (offset - aligned_offset) as usize;
let aligned_len = (diff + len + sector_size as usize - 1) & !(sector_size as usize - 1);
let mut buf = AlignedBuf::zeroed(aligned_len);
aligned_read_exact(file, &mut buf, aligned_offset, diff + len)?;
let mut result = vec![0u8; len];
result.copy_from_slice(&buf[diff..diff + len]);
Ok(result)
}
#[cfg(feature = "var-collections")]
pub fn pread_block(file: &File, block_offset: u64) -> DbResult<(AlignedBuf, usize)> {
use std::os::unix::fs::FileExt;
debug_assert!(
block_offset & 4095 == 0,
"block_offset must be 4096-aligned"
);
let mut buf = AlignedBuf::zeroed(4096);
let n = file.read_at(&mut buf, block_offset)?;
Ok((buf, n))
}
pub fn pwrite_at(file: &File, data: &[u8], offset: u64) -> DbResult<()> {
use std::os::unix::fs::FileExt;
file.write_all_at(data, offset)?;
Ok(())
}
pub fn fsync(file: &File) -> DbResult<()> {
file.sync_data()?;
Ok(())
}
pub fn open_serving_read(path: &Path, #[allow(unused_variables)] direct: bool) -> DbResult<File> {
#[cfg(target_os = "linux")]
if direct {
match open_direct_linux(path, false) {
Ok(f) => return Ok(f),
Err(e) if is_einval(&e) => {
tracing::warn!(
?path,
"O_DIRECT read open rejected by filesystem; using buffered"
);
}
Err(e) => return Err(e),
}
}
let file = OpenOptions::new().read(true).open(path)?;
#[cfg(target_os = "macos")]
{
use std::os::unix::io::AsRawFd;
unsafe {
fcntl(file.as_raw_fd(), 48 , 1);
}
}
Ok(file)
}
pub fn open_serving_write(path: &Path, #[allow(unused_variables)] direct: bool) -> DbResult<File> {
#[cfg(target_os = "linux")]
if direct {
match open_direct_linux(path, true) {
Ok(f) => return Ok(f),
Err(e) if is_einval(&e) => {
tracing::warn!(
?path,
"O_DIRECT write open rejected by filesystem; using buffered"
);
}
Err(e) => return Err(e),
}
}
let file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(path)?;
#[cfg(target_os = "macos")]
{
use std::os::unix::io::AsRawFd;
unsafe {
fcntl(file.as_raw_fd(), 48 , 1);
}
}
Ok(file)
}
pub fn open_bulk_read(path: &Path) -> DbResult<File> {
let file = OpenOptions::new().read(true).open(path)?;
#[cfg(target_os = "linux")]
fadvise_sequential(&file);
Ok(file)
}
pub fn open_bulk_write(path: &Path) -> DbResult<File> {
let file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(path)?;
#[cfg(target_os = "linux")]
fadvise_sequential(&file);
Ok(file)
}
#[allow(dead_code)]
pub fn probe_direct_io(dir: &Path) -> bool {
#[cfg(target_os = "linux")]
{
let path = dir.join(format!(".armdb_odirect_probe_{}", std::process::id()));
let result = (|| -> DbResult<()> {
let file = open_direct_linux(&path, true)?;
let buf = AlignedBuf::zeroed(4096);
pwrite_at(&file, &buf, 0)?;
let mut rbuf = AlignedBuf::zeroed(4096);
use std::os::unix::fs::FileExt;
file.read_at(&mut rbuf, 0)?;
Ok(())
})();
let _ = std::fs::remove_file(&path);
result.is_ok()
}
#[cfg(not(target_os = "linux"))]
{
let _ = dir;
false
}
}
pub fn fadvise_dontneed(file: &File, offset: u64, len: u64) {
#[cfg(target_os = "linux")]
{
use std::num::NonZeroU64;
use std::os::unix::io::AsFd;
let len = NonZeroU64::new(len);
let _ = rustix::fs::fadvise(file.as_fd(), offset, len, rustix::fs::Advice::DontNeed);
}
#[cfg(not(target_os = "linux"))]
{
let _ = (file, offset, len);
}
}
#[cfg(target_os = "linux")]
fn fadvise_sequential(file: &File) {
use std::os::unix::io::AsFd;
let _ = rustix::fs::fadvise(file.as_fd(), 0, None, rustix::fs::Advice::Sequential);
}
#[cfg(target_os = "linux")]
fn open_direct_linux(path: &Path, write: bool) -> DbResult<File> {
use std::os::unix::fs::OpenOptionsExt;
let flag = rustix::fs::OFlags::DIRECT.bits() as i32;
let mut opts = OpenOptions::new();
opts.read(true).custom_flags(flag);
if write {
opts.create(true).write(true).truncate(false);
}
Ok(opts.open(path)?)
}
#[cfg(target_os = "linux")]
fn is_einval(e: &DbError) -> bool {
matches!(
e,
DbError::Io(io)
if io.raw_os_error() == Some(rustix::io::Errno::INVAL.raw_os_error())
)
}
#[cfg(feature = "encryption")]
pub fn pread_value_encrypted(
file: &File,
tag_file: &crate::io::tags::TagFile,
cipher: &crate::crypto::PageCipher,
file_id: u32,
offset: u64,
len: usize,
) -> DbResult<Vec<u8>> {
let sector_size: u64 = 4096;
let aligned_offset = offset & !(sector_size - 1);
let diff = (offset - aligned_offset) as usize;
let aligned_len = (diff + len + sector_size as usize - 1) & !(sector_size as usize - 1);
let mut buf = AlignedBuf::zeroed(aligned_len);
aligned_read_exact(file, &mut buf, aligned_offset, diff + len)?;
let start_page = aligned_offset / sector_size;
let num_pages = aligned_len / sector_size as usize;
let tags = tag_file.read_tags(start_page, num_pages)?;
#[allow(clippy::needless_range_loop)]
for i in 0..num_pages {
let page_start = i * sector_size as usize;
let page = &mut buf[page_start..page_start + sector_size as usize];
cipher.decrypt_page(file_id, start_page + i as u64, page, &tags[i])?;
}
let mut result = vec![0u8; len];
result.copy_from_slice(&buf[diff..diff + len]);
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aligned_read_fills_full_range_across_short_reads() {
let dir = std::env::temp_dir().join(format!("armdb_aligned_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("a.data");
let f = open_bulk_write(&path).unwrap();
let mut data = vec![0u8; 4096 * 3];
for (i, b) in data.iter_mut().enumerate() {
*b = (i % 251) as u8;
}
pwrite_at(&f, &data, 0).unwrap();
fsync(&f).unwrap();
drop(f);
let r = open_bulk_read(&path).unwrap();
let got = pread_value(&r, 100, 4096 * 2 + 7).unwrap();
assert_eq!(&got[..], &data[100..100 + 4096 * 2 + 7]);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn aligned_read_terminates_on_short_file() {
let dir = std::env::temp_dir().join(format!("armdb_aligned_eof_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("short.data");
let f = open_bulk_write(&path).unwrap();
pwrite_at(&f, &vec![0xABu8; 5000], 0).unwrap();
fsync(&f).unwrap();
drop(f);
let r = open_bulk_read(&path).unwrap();
let res = pread_value(&r, 0, 8000);
assert!(
matches!(&res, Err(DbError::Io(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof),
"expected UnexpectedEof on short file, got {res:?}"
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn probe_and_bulk_open_roundtrip() {
let dir = std::env::temp_dir().join(format!("armdb_direct_probe_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let _supported = probe_direct_io(&dir);
let path = dir.join("t.data");
let f = open_bulk_write(&path).unwrap();
pwrite_at(&f, b"hello", 0).unwrap();
fsync(&f).unwrap();
drop(f);
let r = open_bulk_read(&path).unwrap();
let got = pread_value(&r, 0, 5).unwrap();
assert_eq!(&got, b"hello");
let _s = open_serving_read(&path, false).unwrap();
fadvise_dontneed(&r, 0, 5);
std::fs::remove_dir_all(&dir).ok();
}
}