use std::mem::{MaybeUninit, size_of};
use doublets::mem::RawMem;
use doublets::mem::unit::LinkPart;
use link_cli::storage::PersistentFileMapped;
use super::StorageError;
#[cfg(test)]
const DOUBLETS_BOOTSTRAP_ITEMS: usize = 8 * 1024;
pub(super) struct LoadedFileMapped {
inner: PersistentFileMapped<LinkPart<usize>>,
minimum_capacity: usize,
}
impl LoadedFileMapped {
pub(super) fn new(file: std::fs::File) -> Result<Self, StorageError> {
let bytes = usize::try_from(file.metadata()?.len())
.map_err(|_| std::io::Error::other("mapped file is too large for this platform"))?;
if bytes % size_of::<LinkPart<usize>>() != 0 && bytes >= 4096 {
return Err(StorageError::Codec(
"mapped file length is not aligned to a doublets link part".into(),
));
}
let mut inner = PersistentFileMapped::new(file)?;
let items = bytes.max(4096) / size_of::<LinkPart<usize>>();
#[allow(unsafe_code)]
unsafe {
inner
.grow_assumed(items)
.map_err(|error| StorageError::Codec(format!("restore capacity: {error}")))?;
}
Ok(Self {
inner,
minimum_capacity: items,
})
}
}
impl RawMem for LoadedFileMapped {
type Item = LinkPart<usize>;
fn allocated(&self) -> &[Self::Item] {
self.inner.allocated()
}
fn allocated_mut(&mut self) -> &mut [Self::Item] {
self.inner.allocated_mut()
}
#[allow(unsafe_code)]
unsafe fn grow(
&mut self,
addition: usize,
fill: impl FnOnce(usize, (&mut [Self::Item], &mut [MaybeUninit<Self::Item>])),
) -> doublets::mem::Result<&mut [Self::Item]> {
unsafe {
self.inner.grow(addition, fill)?;
}
Ok(self.inner.allocated_mut())
}
fn shrink(&mut self, count: usize) -> doublets::mem::Result<()> {
if self.inner.allocated().len().saturating_sub(count) < self.minimum_capacity {
return Ok(());
}
self.inner.shrink(count)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_existing_mapping_never_shrinks_below_its_file_capacity() {
let directory = tempfile::tempdir().expect("temporary directory");
let path = directory.path().join("tokens.bin");
let existing_items = DOUBLETS_BOOTSTRAP_ITEMS + 37;
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create_new(true)
.open(&path)
.expect("create mapping");
file.set_len((existing_items * size_of::<LinkPart<usize>>()) as u64)
.expect("size mapping");
let mut mapping = LoadedFileMapped::new(file).expect("load mapping");
mapping
.shrink(existing_items - DOUBLETS_BOOTSTRAP_ITEMS)
.expect("ignore bootstrap shrink");
mapping.shrink(1).expect("ignore inclusive-address shrink");
assert_eq!(
mapping.allocated().len(),
existing_items,
"initialization must retain the slot at the highest allocated address"
);
assert_eq!(
std::fs::metadata(path).expect("mapping metadata").len(),
(existing_items * size_of::<LinkPart<usize>>()) as u64
);
}
}