use flint_store::crc64_nvme;
pub const CHUNK_TARGET: usize = 4096;
pub const CHUNK_MIN: usize = CHUNK_TARGET / 4;
pub const CHUNK_MAX: usize = CHUNK_TARGET * 4;
fn cuts_after_with(key: &str, target: usize) -> bool {
crc64_nvme(key.as_bytes()).is_multiple_of(target as u64)
}
pub fn chunk_ranges_with(keys: &[&str], target: usize, min: usize, max: usize) -> Vec<usize> {
debug_assert!(target > 0 && min > 0 && max >= min, "degenerate chunk sizing");
let mut cuts = Vec::new();
let mut run = 0usize;
for (i, k) in keys.iter().enumerate() {
run += 1;
if (run >= min && cuts_after_with(k, target)) || run >= max {
cuts.push(i + 1);
run = 0;
}
}
if run > 0 {
cuts.push(keys.len());
}
cuts
}
pub fn chunk_ranges(keys: &[&str]) -> Vec<usize> {
chunk_ranges_with(keys, CHUNK_TARGET, CHUNK_MIN, CHUNK_MAX)
}
pub fn chunk_address(body: &[u8]) -> String {
use sha2::{Digest, Sha256};
let d = Sha256::digest(body);
d.iter().take(16).map(|b| format!("{b:02x}")).collect()
}
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::manifest::{LeanEntry, Tombstone};
use super::{LeanError, LeanResult};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChunkRef {
pub addr: String,
pub first: String,
pub n: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChunkBody {
pub entries: BTreeMap<String, LeanEntry>,
}
fn encode(body: &ChunkBody) -> LeanResult<Vec<u8>> {
serde_json::to_vec(body).map_err(|e| LeanError::State(format!("chunk encode: {e}")))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TombstoneBody {
pub tombstones: BTreeMap<String, Tombstone>,
}
pub fn encode_tombstones(tombstones: &BTreeMap<String, Tombstone>) -> LeanResult<(String, Vec<u8>)> {
let body = serde_json::to_vec(&TombstoneBody { tombstones: tombstones.clone() })
.map_err(|e| LeanError::State(format!("tombstones encode: {e}")))?;
Ok((chunk_address(&body), body))
}
pub fn decode_tombstones(bytes: &[u8], addr: &str) -> LeanResult<BTreeMap<String, Tombstone>> {
let got = chunk_address(bytes);
if got != addr {
return Err(LeanError::State(format!("tombstones {addr} do not match their address (body hashes to {got})")));
}
let body: TombstoneBody =
serde_json::from_slice(bytes).map_err(|e| LeanError::State(format!("tombstones {addr} parse: {e}")))?;
Ok(body.tombstones)
}
pub fn split_with(
entries: &BTreeMap<String, LeanEntry>,
target: usize,
min: usize,
max: usize,
) -> LeanResult<Vec<(ChunkRef, Vec<u8>)>> {
let keys: Vec<&str> = entries.keys().map(|s| s.as_str()).collect();
let cuts = chunk_ranges_with(&keys, target, min, max);
let mut out = Vec::with_capacity(cuts.len());
let mut start = 0usize;
for c in cuts {
let slice: BTreeMap<String, LeanEntry> = keys[start..c]
.iter()
.map(|k| ((*k).to_string(), entries[*k].clone()))
.collect();
let bytes = encode(&ChunkBody { entries: slice })?;
out.push((
ChunkRef {
addr: chunk_address(&bytes),
first: keys[start].to_string(),
n: c - start,
},
bytes,
));
start = c;
}
Ok(out)
}
pub fn split(entries: &BTreeMap<String, LeanEntry>) -> LeanResult<Vec<(ChunkRef, Vec<u8>)>> {
split_with(entries, CHUNK_TARGET, CHUNK_MIN, CHUNK_MAX)
}
pub fn assemble(refs: &[ChunkRef], bodies: &[Vec<u8>]) -> LeanResult<BTreeMap<String, LeanEntry>> {
if refs.len() != bodies.len() {
return Err(LeanError::State(format!(
"chunk list names {} chunks but {} bodies were fetched",
refs.len(),
bodies.len()
)));
}
let mut out: BTreeMap<String, LeanEntry> = BTreeMap::new();
for (i, (r, b)) in refs.iter().zip(bodies).enumerate() {
if i > 0 && refs[i - 1].first >= r.first {
return Err(LeanError::State(format!(
"chunk list is not in strictly increasing key order: {:?} then {:?}",
refs[i - 1].first, r.first
)));
}
let got = chunk_address(b);
if got != r.addr {
return Err(LeanError::State(format!(
"chunk {} does not match its address (body hashes to {got})",
r.addr
)));
}
let body: ChunkBody = serde_json::from_slice(b)
.map_err(|e| LeanError::State(format!("chunk {} parse: {e}", r.addr)))?;
if body.entries.len() != r.n {
return Err(LeanError::State(format!(
"chunk {} holds {} entries, the pointer says {}",
r.addr,
body.entries.len(),
r.n
)));
}
match body.entries.keys().next() {
Some(k) if *k == r.first => {}
Some(k) => {
return Err(LeanError::State(format!(
"chunk {} starts at {k:?}, the pointer says {:?}",
r.addr, r.first
)))
}
None => {
return Err(LeanError::State(format!(
"chunk {} is empty; an empty chunk cannot be addressed by a first key",
r.addr
)))
}
}
if let (Some(next), Some(last)) = (refs.get(i + 1), body.entries.keys().next_back()) {
if *last >= next.first {
return Err(LeanError::State(format!(
"chunk {} runs to {last:?}, past the next chunk's first key {:?} — the \
chunks overlap and one shadows the other",
r.addr, next.first
)));
}
}
for (k, v) in body.entries {
if out.insert(k.clone(), v).is_some() {
return Err(LeanError::State(format!(
"key {k:?} appears in more than one chunk"
)));
}
}
}
let expect: usize = refs.iter().map(|r| r.n).sum();
if out.len() != expect {
return Err(LeanError::State(format!(
"assembled {} entries, the chunk list accounts for {expect}",
out.len()
)));
}
Ok(out)
}