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()
}
pub fn is_chunk_address(addr: &str) -> bool {
addr.len() == 32 && addr.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
}
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 check_list_order(refs: &[ChunkRef]) -> LeanResult<()> {
for w in refs.windows(2) {
if w[0].first >= w[1].first {
return Err(LeanError::State(format!(
"chunk list is not in strictly increasing key order: {:?} then {:?}",
w[0].first, w[1].first
)));
}
}
Ok(())
}
pub fn parse_chunk(refs: &[ChunkRef], i: usize, b: &[u8]) -> LeanResult<BTreeMap<String, LeanEntry>> {
let r = &refs[i];
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
)));
}
}
Ok(body.entries)
}
enum Step {
Key(String, LeanEntry, bool),
Skip,
End,
}
pub fn rechunk_with<F>(
old: &[ChunkRef],
mut entries_of: F,
changes: &BTreeMap<String, Option<LeanEntry>>,
target: usize,
min: usize,
max: usize,
) -> LeanResult<Vec<(ChunkRef, Option<Vec<u8>>)>>
where
F: FnMut(usize) -> LeanResult<std::sync::Arc<BTreeMap<String, LeanEntry>>>,
{
debug_assert!(target > 0 && min > 0 && max >= min, "degenerate chunk sizing");
let changes: Vec<(&String, &Option<LeanEntry>)> = changes.iter().collect();
let home = |p: &str| old.partition_point(|r| r.first.as_str() <= p).saturating_sub(1);
let mut load = |m: usize| -> LeanResult<Vec<(String, LeanEntry)>> {
Ok(entries_of(m)?.iter().map(|(k, e)| (k.clone(), e.clone())).collect())
};
let mut out: Vec<(ChunkRef, Option<Vec<u8>>)> = Vec::with_capacity(old.len() + 1);
let mut c = 0usize; let mut i = 0usize; while c < changes.len() {
let start = home(changes[c].0);
debug_assert!(old.is_empty() || start >= i, "a change before the old chunk the last region synced to");
let start = start.max(i);
out.extend(old[i..start.min(old.len())].iter().map(|r| (r.clone(), None)));
let mut m = start;
let mut cur = if m < old.len() { load(m)? } else { vec![] };
let mut pos = 0usize;
let mut run: BTreeMap<String, LeanEntry> = BTreeMap::new();
let mut first: Option<String> = None;
loop {
let bound = old.get(m + 1).map(|r| r.first.as_str());
let change = changes.get(c).copied().filter(|(k, _)| bound.is_none_or(|b| k.as_str() < b));
if pos >= cur.len() && change.is_none() && m + 1 < old.len() {
m += 1;
cur = load(m)?;
pos = 0;
continue;
}
let step = match (cur.get(pos), change) {
(None, None) => Step::End,
(Some((ok, _)), Some((ck, cv))) if ck.as_str() < ok.as_str() => {
c += 1;
cv.clone().map_or(Step::Skip, |e| Step::Key(ck.clone(), e, false))
}
(Some((ok, _)), Some((ck, cv))) if ck.as_str() == ok.as_str() => {
c += 1;
pos += 1;
cv.clone().map_or(Step::Skip, |e| Step::Key(ck.clone(), e, pos == cur.len()))
}
(Some((ok, oe)), _) => {
pos += 1;
Step::Key(ok.clone(), oe.clone(), pos == cur.len())
}
(None, Some((ck, cv))) => {
c += 1;
cv.clone().map_or(Step::Skip, |e| Step::Key(ck.clone(), e, false))
}
};
let (k, e, old_last) = match step {
Step::Key(k, e, last) => (k, e, last),
Step::Skip => continue,
Step::End => {
if let Some(f) = first.take() {
out.push(encode_chunk(f, std::mem::take(&mut run))?);
}
i = old.len();
break;
}
};
first.get_or_insert_with(|| k.clone());
run.insert(k.clone(), e);
if (run.len() >= min && cuts_after_with(&k, target)) || run.len() >= max {
let f = first.take().expect("a run holds its first key");
out.push(encode_chunk(f, std::mem::take(&mut run))?);
let synced = old_last
&& match (changes.get(c), old.get(m + 1)) {
(None, _) => true,
(Some((ck, _)), Some(next)) => ck.as_str() >= next.first.as_str(),
(Some(_), None) => false,
};
if synced {
i = m + 1;
break;
}
}
}
}
out.extend(old[i.min(old.len())..].iter().map(|r| (r.clone(), None)));
Ok(out)
}
fn encode_chunk(first: String, entries: BTreeMap<String, LeanEntry>) -> LeanResult<(ChunkRef, Option<Vec<u8>>)> {
let n = entries.len();
let bytes = encode(&ChunkBody { entries })?;
Ok((ChunkRef { addr: chunk_address(&bytes), first, n }, Some(bytes)))
}
pub fn check_address_of(body: &[u8], addr: &str) -> LeanResult<()> {
let got = chunk_address(body);
if got != addr {
return Err(LeanError::State(format!(
"chunk {addr} does not match its address (body hashes to {got})"
)));
}
Ok(())
}
pub fn assemble(refs: &[ChunkRef], bodies: &[Vec<u8>]) -> LeanResult<BTreeMap<String, LeanEntry>> {
assemble_checked(refs, bodies, true)
}
pub fn assemble_verified<B: AsRef<[u8]>>(refs: &[ChunkRef], bodies: &[B]) -> LeanResult<BTreeMap<String, LeanEntry>> {
assemble_checked(refs, bodies, false)
}
fn assemble_checked<B: AsRef<[u8]>>(
refs: &[ChunkRef],
bodies: &[B],
check_address: bool,
) -> 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()
)));
}
check_list_order(refs)?;
let mut out: BTreeMap<String, LeanEntry> = BTreeMap::new();
for (i, b) in bodies.iter().enumerate() {
let b = b.as_ref();
if check_address {
check_address_of(b, &refs[i].addr)?;
}
let body = ChunkBody { entries: parse_chunk(refs, i, b)? };
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)
}