use std::fs::File;
use std::path::Path;
use memmap2::Mmap;
use super::{BlockDevice, BlockId, StorageError};
fn io_err(e: std::io::Error) -> StorageError {
StorageError::Io {
kind: e.kind() as u8,
}
}
pub struct MmapBlockDevice {
_file: File,
mmap: Mmap,
block_count: u64,
}
impl MmapBlockDevice {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, StorageError> {
let file = File::open(path).map_err(io_err)?;
let mmap = unsafe { Mmap::map(&file) }.map_err(io_err)?;
let block_count = mmap.len() as u64 / Self::BLOCK_SIZE as u64;
Ok(Self {
_file: file,
mmap,
block_count,
})
}
pub fn page_ref(&self, block_id: BlockId) -> Result<&[u8; Self::BLOCK_SIZE], StorageError> {
if block_id >= self.block_count {
return Err(StorageError::OutOfBounds {
block_id,
block_count: self.block_count,
});
}
let start = block_id as usize * Self::BLOCK_SIZE;
let end = start + Self::BLOCK_SIZE;
self.mmap[start..end]
.try_into()
.map_err(|_| StorageError::ShortRead {
got: self.mmap.len() - start,
expected: Self::BLOCK_SIZE,
})
}
}
impl BlockDevice for MmapBlockDevice {
fn read_block(&self, block_id: BlockId, buffer: &mut [u8]) -> Result<(), StorageError> {
if buffer.len() != Self::BLOCK_SIZE {
return Err(StorageError::ShortRead {
got: buffer.len(),
expected: Self::BLOCK_SIZE,
});
}
buffer.copy_from_slice(self.page_ref(block_id)?);
Ok(())
}
fn write_block(&mut self, _block_id: BlockId, _data: &[u8]) -> Result<(), StorageError> {
Err(StorageError::Unsupported)
}
fn sync(&mut self) -> Result<(), StorageError> {
Ok(())
}
fn block_count(&self) -> u64 {
self.block_count
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::storage::Database;
fn temp_path(name: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
p.push(format!(
"tpt-archon-core-mmap-{}-{}.bin",
name,
std::process::id()
));
p
}
#[test]
fn page_ref_is_stable_and_zero_copy() {
let path = temp_path("stable");
{
let mut db = Database::create(&path, 4).unwrap();
db.put(1, &[0xEEu8; MmapBlockDevice::BLOCK_SIZE]).unwrap();
}
let dev = MmapBlockDevice::open(&path).unwrap();
let a = dev.page_ref(1).unwrap();
let b = dev.page_ref(1).unwrap();
assert!(core::ptr::eq(a.as_ptr(), b.as_ptr()));
let _ = std::fs::remove_file(&path);
}
#[test]
fn written_then_committed_then_mmap_reads_it() {
let path = temp_path("roundtrip");
{
let mut db = Database::create(&path, 4).unwrap();
db.put(2, &[0xAB; MmapBlockDevice::BLOCK_SIZE]).unwrap();
}
let dev = MmapBlockDevice::open(&path).unwrap();
assert_eq!(dev.page_ref(2).unwrap()[0], 0xAB);
let _ = std::fs::remove_file(&path);
}
#[test]
fn rejects_out_of_bounds() {
let path = temp_path("oob");
{
let _ = Database::create(&path, 1).unwrap();
}
let dev = MmapBlockDevice::open(&path).unwrap();
assert!(matches!(
dev.page_ref(5),
Err(StorageError::OutOfBounds { .. })
));
let _ = std::fs::remove_file(&path);
}
#[test]
fn write_block_is_unsupported() {
let path = temp_path("readonly");
{
let _ = Database::create(&path, 1).unwrap();
}
let mut dev = MmapBlockDevice::open(&path).unwrap();
assert_eq!(
dev.write_block(0, &[0u8; MmapBlockDevice::BLOCK_SIZE]),
Err(StorageError::Unsupported)
);
let _ = std::fs::remove_file(&path);
}
}