use crate::BaleError;
use fs4::fs_std::FileExt;
use std::fs::File;
use std::path::Path;
pub struct MappedArchiveMut {
file: File,
mmap: memmap2::MmapMut,
len: usize,
committed_len: usize,
}
impl MappedArchiveMut {
pub const DEFAULT_CHUNK_SIZE: usize = 1024 * 1024;
pub fn create(path: impl AsRef<Path>) -> Result<Self, BaleError> {
Self::create_with_capacity(path, Self::DEFAULT_CHUNK_SIZE)
}
pub fn create_with_capacity(
path: impl AsRef<Path>,
capacity: usize,
) -> Result<Self, BaleError> {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(path.as_ref())?;
file.lock_exclusive()?;
file.set_len(capacity as u64)?;
#[allow(unsafe_code)]
let mmap = unsafe { memmap2::MmapMut::map_mut(&file)? };
Ok(Self {
file,
mmap,
len: 0,
committed_len: 0,
})
}
pub fn open(path: impl AsRef<Path>) -> Result<Self, BaleError> {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path.as_ref())?;
file.lock_exclusive()?;
let len = file.metadata()?.len() as usize;
#[allow(unsafe_code)]
let mmap = unsafe { memmap2::MmapMut::map_mut(&file)? };
Ok(Self {
file,
mmap,
len,
committed_len: len,
})
}
#[must_use]
pub fn len(&self) -> usize {
self.len
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
pub fn capacity(&self) -> usize {
self.mmap.len()
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.mmap[..self.len]
}
#[must_use]
pub fn as_bytes_mut(&mut self) -> &mut [u8] {
&mut self.mmap[..self.len]
}
#[must_use]
pub fn file(&self) -> &File {
&self.file
}
pub fn reserve(&mut self, additional: usize) -> Result<(), BaleError> {
let required = self.len + additional;
if required <= self.capacity() {
return Ok(());
}
let new_capacity = ((required / Self::DEFAULT_CHUNK_SIZE) + 1) * Self::DEFAULT_CHUNK_SIZE;
log::trace!(
"MappedArchiveMut::reserve: len={}, additional={}, capacity={} -> {}",
self.len,
additional,
self.capacity(),
new_capacity
);
self.resize_file(new_capacity)
}
fn resize_file(&mut self, new_capacity: usize) -> Result<(), BaleError> {
self.mmap.flush_async_range(0, self.len)?;
self.file.set_len(new_capacity as u64)?;
#[allow(unsafe_code)]
let new_mmap = unsafe { memmap2::MmapMut::map_mut(&self.file)? };
self.mmap = new_mmap;
Ok(())
}
pub fn extend(&mut self, data: &[u8]) -> Result<(), BaleError> {
self.reserve(data.len())?;
self.mmap[self.len..self.len + data.len()].copy_from_slice(data);
self.len += data.len();
Ok(())
}
pub fn set_len(&mut self, new_len: usize) -> Result<(), BaleError> {
if new_len > self.capacity() {
self.reserve(new_len - self.len)?;
}
if new_len > self.len {
self.mmap[self.len..new_len].fill(0);
}
self.len = new_len;
Ok(())
}
pub fn sync(&mut self) -> Result<(), BaleError> {
log::trace!(
"MappedArchiveMut::sync: len={}, committed={}, capacity={}",
self.len,
self.committed_len,
self.capacity()
);
self.mmap.flush_async_range(0, self.len)?;
self.file.set_len(self.len as u64)?;
self.committed_len = self.len;
#[allow(unsafe_code)]
let new_mmap = unsafe { memmap2::MmapMut::map_mut(&self.file)? };
self.mmap = new_mmap;
Ok(())
}
}
impl Drop for MappedArchiveMut {
fn drop(&mut self) {
let final_len = self.len.max(self.committed_len);
log::trace!(
"MappedArchiveMut::drop: len={}, committed={}, final={}",
self.len,
self.committed_len,
final_len
);
if let Err(e) = self.mmap.flush_async_range(0, final_len) {
log::error!("MappedArchiveMut::drop: flush failed: {e}");
return;
}
if let Err(e) = self.file.set_len(final_len as u64) {
log::error!("MappedArchiveMut::drop: set_len failed: {e}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn create_mut_archive() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.bale");
let archive = MappedArchiveMut::create(&path).unwrap();
assert!(archive.is_empty());
assert_eq!(archive.len(), 0);
assert_eq!(archive.capacity(), MappedArchiveMut::DEFAULT_CHUNK_SIZE);
}
#[test]
fn extend_mut_archive() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.bale");
let mut archive = MappedArchiveMut::create(&path).unwrap();
archive.extend(b"hello").unwrap();
assert_eq!(archive.len(), 5);
assert_eq!(archive.as_bytes(), b"hello");
archive.extend(b" world").unwrap();
assert_eq!(archive.len(), 11);
assert_eq!(archive.as_bytes(), b"hello world");
}
#[test]
fn open_mut_existing() {
let mut file = NamedTempFile::new().unwrap();
file.write_all(b"existing data").unwrap();
file.flush().unwrap();
let archive = MappedArchiveMut::open(file.path()).unwrap();
assert_eq!(archive.len(), 13);
assert_eq!(archive.as_bytes(), b"existing data");
}
#[test]
fn reserve_expands_capacity() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.bale");
let mut archive = MappedArchiveMut::create_with_capacity(&path, 100).unwrap();
assert_eq!(archive.capacity(), 100);
archive.reserve(200).unwrap();
assert!(archive.capacity() >= 200);
}
#[test]
fn sync_truncates_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.bale");
{
let mut archive = MappedArchiveMut::create(&path).unwrap();
archive.extend(b"test data").unwrap();
archive.sync().unwrap();
}
let metadata = std::fs::metadata(&path).unwrap();
assert_eq!(metadata.len(), 9);
}
}