use yo_common::{Addr, Code, Error, Result, Space};
pub const CHUNK: usize = 64 * 1024;
pub const FANOUT: usize = CHUNK / 8;
pub const MAX_LEN: u64 = (FANOUT * CHUNK) as u64;
pub trait Blocks {
fn put(&mut self, bytes: &[u8]) -> Result<Addr>;
fn get(&self, at: Addr) -> Result<&[u8]>;
fn bytes(&self) -> u64;
fn release(&mut self) {}
}
pub type Store = Box<dyn Blocks + Send>;
impl Blocks for Store {
fn put(&mut self, bytes: &[u8]) -> Result<Addr> {
(**self).put(bytes)
}
fn get(&self, at: Addr) -> Result<&[u8]> {
(**self).get(at)
}
fn bytes(&self) -> u64 {
(**self).bytes()
}
fn release(&mut self) {
(**self).release();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Chain {
pub at: Addr,
pub len: u64,
}
#[must_use]
pub const fn chunks_for(len: u64) -> u64 {
if len == 0 {
1
} else {
len.div_ceil(CHUNK as u64)
}
}
pub struct Scratch {
dir: Vec<u8>,
}
impl Scratch {
#[must_use]
pub fn new() -> Scratch {
Scratch {
dir: Vec::with_capacity(CHUNK),
}
}
#[must_use]
pub fn memory_bytes(&self) -> usize {
self.dir.capacity()
}
}
impl Default for Scratch {
fn default() -> Scratch {
Scratch::new()
}
}
pub fn write<B: Blocks>(blocks: &mut B, value: &[u8], scratch: &mut Scratch) -> Result<Chain> {
let len = value.len() as u64;
if len > MAX_LEN {
return Err(Error::fmt(
Code::Full,
format_args!("a value of {len} bytes is longer than a chain holds"),
));
}
if value.len() <= CHUNK {
return Ok(Chain {
at: blocks.put(value)?,
len,
});
}
scratch.dir.clear();
for piece in value.chunks(CHUNK) {
let at = blocks.put(piece)?;
scratch.dir.extend_from_slice(&at.to_bits().to_le_bytes());
}
let at = blocks.put(&scratch.dir)?;
Ok(Chain { at, len })
}
pub struct Reader<'a, B: Blocks> {
blocks: &'a B,
len: u64,
dir: Dir<'a>,
}
enum Dir<'a> {
One(Addr),
Many(&'a [u8]),
}
impl<'a, B: Blocks> Reader<'a, B> {
pub fn open(blocks: &'a B, chain: Chain) -> Result<Reader<'a, B>> {
let want = chunks_for(chain.len);
let dir = if want == 1 {
Dir::One(chain.at)
} else {
let bytes = blocks.get(chain.at)?;
if bytes.len() as u64 != want * 8 {
return Err(Error::fmt(
Code::Corrupt,
format_args!(
"a chain of {} bytes wants {want} addresses and its directory has {}",
chain.len,
bytes.len() / 8
),
));
}
Dir::Many(bytes)
};
Ok(Reader {
blocks,
len: chain.len,
dir,
})
}
#[must_use]
pub const fn len(&self) -> u64 {
self.len
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.len == 0
}
#[must_use]
pub const fn chunks(&self) -> u64 {
chunks_for(self.len)
}
pub fn chunk(&self, i: u64) -> Result<&'a [u8]> {
let at = match self.dir {
Dir::One(at) if i == 0 => at,
Dir::One(_) => {
return Err(Error::new(Code::Invalid, "there is only one chunk"));
}
Dir::Many(bytes) => {
let start = (i as usize)
.checked_mul(8)
.filter(|s| s + 8 <= bytes.len())
.ok_or_else(|| Error::new(Code::Invalid, "no such chunk"))?;
let mut bits = [0u8; 8];
bits.copy_from_slice(&bytes[start..start + 8]);
Addr::from_bits(u64::from_le_bytes(bits))
}
};
if at.space() != Some(Space::Log) {
return Err(Error::fmt(
Code::Corrupt,
format_args!("chunk {i} is not in the log"),
));
}
self.blocks.get(at)
}
pub fn range(&self, from: u64, to: u64) -> Pieces<'a, '_, B> {
let to = to.min(self.len);
let from = from.min(to);
Pieces {
reader: self,
at: from,
end: to,
}
}
}
pub struct Pieces<'a, 'r, B: Blocks> {
reader: &'r Reader<'a, B>,
at: u64,
end: u64,
}
impl<'a, B: Blocks> Iterator for Pieces<'a, '_, B> {
type Item = Result<&'a [u8]>;
fn next(&mut self) -> Option<Result<&'a [u8]>> {
if self.at >= self.end {
return None;
}
let chunk = self.at / CHUNK as u64;
let start = (self.at % CHUNK as u64) as usize;
let take = (self.end - self.at).min(CHUNK as u64 - start as u64) as usize;
self.at += take as u64;
Some(match self.reader.chunk(chunk) {
Ok(bytes) if start + take <= bytes.len() => Ok(&bytes[start..start + take]),
Ok(bytes) => Err(Error::fmt(
Code::Corrupt,
format_args!(
"chunk {chunk} is {} bytes and the range wants {}",
bytes.len(),
start + take
),
)),
Err(e) => Err(e),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Mem {
blobs: Vec<Vec<u8>>,
reads: std::cell::Cell<usize>,
}
impl Mem {
fn new() -> Mem {
Mem {
blobs: Vec::new(),
reads: std::cell::Cell::new(0),
}
}
fn reads(&self) -> usize {
self.reads.get()
}
}
impl Blocks for Mem {
fn put(&mut self, bytes: &[u8]) -> Result<Addr> {
self.blobs.push(bytes.to_vec());
Ok(Addr::new(Space::Log, (self.blobs.len() - 1) as u64))
}
fn get(&self, at: Addr) -> Result<&[u8]> {
self.reads.set(self.reads.get() + 1);
self.blobs
.get(at.offset() as usize)
.map(Vec::as_slice)
.ok_or_else(|| Error::new(Code::NotFound, "no such block"))
}
fn bytes(&self) -> u64 {
self.blobs.iter().map(|b| b.len() as u64).sum()
}
}
fn pattern(len: usize) -> Vec<u8> {
(0..len).map(|i| (i % 251) as u8).collect()
}
fn whole<B: Blocks>(r: &Reader<'_, B>) -> Vec<u8> {
let mut out = Vec::new();
for piece in r.range(0, r.len()) {
out.extend_from_slice(piece.expect("a piece the value has"));
}
out
}
#[test]
fn a_value_that_fits_in_one_chunk_has_no_directory() {
let mut m = Mem::new();
let value = pattern(1000);
let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
assert_eq!(
m.blobs.len(),
1,
"a directory was written and should not be"
);
assert_eq!(chain.len, 1000);
let r = Reader::open(&m, chain).expect("opened");
assert_eq!(r.chunks(), 1);
assert_eq!(whole(&r), value);
assert_eq!(
m.reads(),
1,
"reading a short value took more than one read"
);
}
#[test]
fn exactly_one_chunk_still_has_no_directory() {
let mut m = Mem::new();
let value = pattern(CHUNK);
let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
assert_eq!(m.blobs.len(), 1);
let r = Reader::open(&m, chain).expect("opened");
assert_eq!(r.chunks(), 1);
assert_eq!(whole(&r), value);
}
#[test]
fn one_byte_more_than_a_chunk_is_two_chunks_and_a_directory() {
let mut m = Mem::new();
let value = pattern(CHUNK + 1);
let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
assert_eq!(m.blobs.len(), 3, "two chunks and a directory");
let r = Reader::open(&m, chain).expect("opened");
assert_eq!(r.chunks(), 2);
assert_eq!(r.chunk(1).expect("the second chunk").len(), 1);
assert_eq!(whole(&r), value);
}
#[test]
fn an_empty_value_is_one_empty_chunk() {
let mut m = Mem::new();
let chain = write(&mut m, b"", &mut Scratch::new()).expect("written");
let r = Reader::open(&m, chain).expect("opened");
assert!(r.is_empty());
assert_eq!(r.chunks(), 1, "a chain always points at something");
assert_eq!(whole(&r), b"");
}
#[test]
fn a_range_inside_one_chunk_only_fetches_that_chunk() {
let mut m = Mem::new();
let value = pattern(10 * CHUNK);
let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
let r = Reader::open(&m, chain).expect("opened");
let before = m.reads();
let mut got = Vec::new();
let (from, to) = (7 * CHUNK as u64 + 100, 7 * CHUNK as u64 + 300);
for piece in r.range(from, to) {
got.extend_from_slice(piece.expect("a piece"));
}
assert_eq!(got, value[from as usize..to as usize]);
assert_eq!(
m.reads() - before,
1,
"a range inside one chunk of a ten chunk value should be one fetch"
);
}
#[test]
fn a_range_across_a_boundary_comes_back_in_two_pieces() {
let mut m = Mem::new();
let value = pattern(3 * CHUNK);
let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
let r = Reader::open(&m, chain).expect("opened");
let (from, to) = (CHUNK as u64 - 5, CHUNK as u64 + 5);
let pieces: Vec<usize> = r
.range(from, to)
.map(|p| p.expect("a piece").len())
.collect();
assert_eq!(
pieces,
vec![5, 5],
"the boundary was not where it should be"
);
}
#[test]
fn a_range_past_the_end_stops_at_the_end() {
let mut m = Mem::new();
let value = pattern(100);
let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
let r = Reader::open(&m, chain).expect("opened");
let mut got = Vec::new();
for piece in r.range(50, 1_000_000) {
got.extend_from_slice(piece.expect("a piece"));
}
assert_eq!(got, value[50..]);
assert_eq!(r.range(200, 300).count(), 0, "there is nothing out there");
assert_eq!(r.range(80, 20).count(), 0, "a backwards range is empty");
}
#[test]
fn every_chunk_but_the_last_is_full() {
let mut m = Mem::new();
let value = pattern(2 * CHUNK + 7);
let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
let r = Reader::open(&m, chain).expect("opened");
assert_eq!(r.chunks(), 3);
assert_eq!(r.chunk(0).expect("chunk 0").len(), CHUNK);
assert_eq!(r.chunk(1).expect("chunk 1").len(), CHUNK);
assert_eq!(r.chunk(2).expect("chunk 2").len(), 7);
assert!(r.chunk(3).is_err(), "there is no fourth chunk");
}
#[test]
fn a_multi_chunk_value_is_two_reads_and_not_more() {
let mut m = Mem::new();
let value = pattern(5 * CHUNK);
let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
let before = m.reads();
let r = Reader::open(&m, chain).expect("opened");
assert_eq!(m.reads() - before, 1);
r.chunk(4).expect("the last chunk");
assert_eq!(m.reads() - before, 2);
}
#[test]
fn a_directory_that_does_not_match_the_length_is_refused() {
let mut m = Mem::new();
let value = pattern(2 * CHUNK);
let chain = write(&mut m, &value, &mut Scratch::new()).expect("written");
let lying = Chain {
at: chain.at,
len: 9 * CHUNK as u64,
};
assert!(
Reader::open(&m, lying).is_err(),
"a directory that is the wrong size was accepted"
);
}
#[test]
fn a_value_longer_than_a_chain_holds_is_refused_rather_than_truncated() {
assert_eq!(MAX_LEN, 512 * 1024 * 1024);
assert_eq!(chunks_for(MAX_LEN), FANOUT as u64);
assert_eq!(chunks_for(MAX_LEN + 1), FANOUT as u64 + 1);
}
#[test]
fn the_scratch_is_reused_and_does_not_grow_with_every_write() {
let mut m = Mem::new();
let mut scratch = Scratch::new();
let value = pattern(4 * CHUNK);
for _ in 0..8 {
write(&mut m, &value, &mut scratch).expect("written");
}
assert_eq!(
scratch.memory_bytes(),
CHUNK,
"the directory buffer grew, so a command path is allocating"
);
}
#[test]
fn what_went_in_comes_back_at_every_awkward_size() {
let mut m = Mem::new();
let mut scratch = Scratch::new();
for len in [
0,
1,
CHUNK - 1,
CHUNK,
CHUNK + 1,
2 * CHUNK - 1,
2 * CHUNK,
2 * CHUNK + 1,
3 * CHUNK + 123,
] {
let value = pattern(len);
let chain = write(&mut m, &value, &mut scratch).expect("written");
let r = Reader::open(&m, chain).expect("opened");
assert_eq!(r.len(), len as u64);
assert_eq!(whole(&r), value, "a value of {len} bytes came back wrong");
}
}
}