use crate::{
Memory,
hash::{FNV_OFFSET, fnv64},
};
use super::{Error, StableKey, StorageConfig};
pub(super) const PAGE_BYTES: u64 = 65_536;
pub(super) const UNIT_PAGES: u64 = 8;
pub(super) const UNIT_BYTES: u64 = PAGE_BYTES * UNIT_PAGES;
pub(super) const ROOT_BYTES: usize = 96;
pub(super) const ENTRY_BYTES: usize = 168;
pub(super) const UNIT_ROW_BYTES: usize = 24;
const MAGIC: &[u8; 8] = b"STORNAM1";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct Limits {
pub allocations: u64,
pub extents: u64,
pub pages: u64,
}
impl Limits {
pub fn new(config: StorageConfig) -> Result<Self, Error> {
Ok(Self {
allocations: u64::try_from(config.allocations)
.map_err(|_| Error::ArithmeticOverflow)?,
extents: u64::try_from(config.extents).map_err(|_| Error::ArithmeticOverflow)?,
pages: config.pages,
})
}
pub fn intersect(self, capacity: Self) -> Self {
Self {
allocations: self.allocations.min(capacity.allocations),
extents: self.extents.min(capacity.extents),
pages: self.pages.min(capacity.pages),
}
}
}
#[derive(Clone, Copy)]
pub(super) struct Layout {
pub metadata_pages: u64,
pub payload_page: u64,
units_offset: u64,
}
impl Layout {
pub fn new(limits: Limits) -> Result<Self, Error> {
let entry_bytes = limits
.allocations
.checked_mul(ENTRY_BYTES as u64)
.ok_or(Error::ArithmeticOverflow)?;
let unit_bytes = limits
.extents
.checked_mul(UNIT_ROW_BYTES as u64)
.ok_or(Error::ArithmeticOverflow)?;
let table_bytes = entry_bytes
.checked_add(unit_bytes)
.ok_or(Error::ArithmeticOverflow)?;
let metadata_pages = table_bytes.div_ceil(PAGE_BYTES);
let payload_page = metadata_pages
.checked_add(1)
.ok_or(Error::ArithmeticOverflow)?;
if payload_page > limits.pages {
return Err(Error::Budget);
}
limits
.pages
.checked_mul(PAGE_BYTES)
.ok_or(Error::ArithmeticOverflow)?;
let units_offset = PAGE_BYTES
.checked_add(entry_bytes)
.ok_or(Error::ArithmeticOverflow)?;
Ok(Self {
metadata_pages,
payload_page,
units_offset,
})
}
pub fn entry_offset(index: usize) -> Result<u64, Error> {
(index as u64)
.checked_mul(ENTRY_BYTES as u64)
.and_then(|offset| offset.checked_add(PAGE_BYTES))
.ok_or(Error::ArithmeticOverflow)
}
pub fn unit_offset(self, index: u64) -> Result<u64, Error> {
index
.checked_mul(UNIT_ROW_BYTES as u64)
.and_then(|offset| offset.checked_add(self.units_offset))
.ok_or(Error::ArithmeticOverflow)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) struct Root {
pub capacities: Limits,
pub generation: u64,
pub entries: u64,
pub units: u64,
pub digest: u64,
}
impl Root {
pub fn physical_pages(self, layout: Layout) -> Result<u64, Error> {
self.units
.checked_mul(UNIT_PAGES)
.and_then(|pages| pages.checked_add(layout.payload_page))
.ok_or(Error::ArithmeticOverflow)
}
pub fn encode(self, layout: Layout) -> [u8; ROOT_BYTES] {
let mut bytes = [0; ROOT_BYTES];
bytes[..8].copy_from_slice(MAGIC);
for (index, value) in [
1,
self.capacities.allocations,
self.capacities.extents,
self.capacities.pages,
self.generation,
self.entries,
self.units,
layout.metadata_pages,
self.physical_pages(layout)
.expect("preflighted physical size"),
self.digest,
]
.into_iter()
.enumerate()
{
put(&mut bytes, (index + 1) * 8, value);
}
seal(&mut bytes);
bytes
}
pub fn decode(bytes: &[u8; ROOT_BYTES]) -> Result<Self, Error> {
if &bytes[..8] != MAGIC || get(bytes, 8) != 1 {
return Err(Error::Invalid("unsupported directory framing"));
}
check(bytes)?;
let root = Self {
capacities: Limits {
allocations: get(bytes, 16),
extents: get(bytes, 24),
pages: get(bytes, 32),
},
generation: get(bytes, 40),
entries: get(bytes, 48),
units: get(bytes, 56),
digest: get(bytes, 80),
};
let layout = Layout::new(root.capacities)?;
if get(bytes, 64) != layout.metadata_pages
|| get(bytes, 72) != root.physical_pages(layout)?
{
return Err(Error::Invalid("invalid directory geometry"));
}
Ok(root)
}
pub fn admit(self, limits: Limits, layout: Layout, physical_pages: u64) -> Result<(), Error> {
if self.entries > limits.allocations
|| self.units > limits.extents
|| physical_pages > limits.pages
{
return Err(Error::Budget);
}
if physical_pages != self.physical_pages(layout)? {
return Err(Error::Invalid("unexplained backing size"));
}
Ok(())
}
}
pub(super) fn scan<M: Memory, const N: usize>(
memory: &M,
start: u64,
count: u64,
mut visit: impl FnMut(u64, &[u8; N]) -> Result<(), Error>,
) -> Result<(), Error> {
let mut scratch = [0; 4032];
let mut done = 0_u64;
while done < count {
let rows = (count - done).min((scratch.len() / N) as u64);
let length = usize::try_from(rows).expect("row count is bounded by scratch capacity") * N;
let offset = done
.checked_mul(N as u64)
.and_then(|offset| start.checked_add(offset))
.ok_or(Error::ArithmeticOverflow)?;
memory.read(offset, &mut scratch[..length]);
for (index, row) in scratch[..length].as_chunks::<N>().0.iter().enumerate() {
visit(done + index as u64, row)?;
}
done += rows;
}
Ok(())
}
pub(super) const fn units_for(pages: u64) -> u64 {
pages.div_ceil(UNIT_PAGES)
}
pub(super) fn encode_entry(
index: usize,
key: &StableKey,
pages: u64,
units: u64,
) -> [u8; ENTRY_BYTES] {
let mut row = [0; ENTRY_BYTES];
put(&mut row, 0, index as u64 + 1);
put(&mut row, 8, pages);
put(&mut row, 16, units);
let name = key.as_str().as_bytes();
put(&mut row, 24, name.len() as u64);
row[32..32 + name.len()].copy_from_slice(name);
seal(&mut row);
row
}
pub(super) fn decode_entry(
row: &[u8; ENTRY_BYTES],
index: usize,
) -> Result<(StableKey, u64, u64, u64), Error> {
check(row)?;
if get(row, 0) != index as u64 + 1 {
return Err(Error::Invalid("invalid allocation identity"));
}
let length = usize::try_from(get(row, 24)).map_err(|_| Error::ArithmeticOverflow)?;
if length > 128 || row[32 + length..160].iter().any(|byte| *byte != 0) {
return Err(Error::Invalid("invalid allocation name encoding"));
}
let name = std::str::from_utf8(&row[32..32 + length])
.map_err(|_| Error::Invalid("invalid allocation name encoding"))?;
let key = StableKey::parse(name).map_err(|_| Error::Invalid("invalid allocation name"))?;
Ok((key, get(row, 8), get(row, 16), checksum(row)))
}
pub(super) fn encode_unit(physical: u64, owner: u64, logical: u64) -> [u8; UNIT_ROW_BYTES] {
let mut row = [0; UNIT_ROW_BYTES];
put(&mut row, 0, owner);
put(&mut row, 8, logical);
let digest = fnv64(fnv64(FNV_OFFSET, &physical.to_le_bytes()), &row[..16]);
put(&mut row, 16, digest);
row
}
pub(super) fn decode_unit(
row: &[u8; UNIT_ROW_BYTES],
physical: u64,
) -> Result<(u64, u64, u64), Error> {
if fnv64(fnv64(FNV_OFFSET, &physical.to_le_bytes()), &row[..16]) != checksum(row) {
return Err(Error::Invalid("damaged ownership control"));
}
Ok((get(row, 0), get(row, 8), checksum(row)))
}
pub(super) fn checksum(bytes: &[u8]) -> u64 {
get(bytes, bytes.len() - 8)
}
fn get(bytes: &[u8], offset: usize) -> u64 {
u64::from_le_bytes(
bytes[offset..offset + 8]
.try_into()
.expect("fixed-width control"),
)
}
fn put(bytes: &mut [u8], offset: usize, value: u64) {
bytes[offset..offset + 8].copy_from_slice(&value.to_le_bytes());
}
fn seal(bytes: &mut [u8]) {
let end = bytes.len() - 8;
let digest = fnv64(FNV_OFFSET, &bytes[..end]);
put(bytes, end, digest);
}
fn check(bytes: &[u8]) -> Result<(), Error> {
if fnv64(FNV_OFFSET, &bytes[..bytes.len() - 8]) != checksum(bytes) {
return Err(Error::Invalid("damaged directory control"));
}
Ok(())
}