use std::path::Path;
use kernel::Seq;
use super::{ChainSnapshot, SnapshotFit, StoredSnapshot, read_snapshot};
use crate::error::{StorageError, io_err};
use crate::jsonl::{claimed_seq, complete_lines, ledger_segments_at, segment_first_seq};
use crate::real_fs::RealFs;
use crate::vfs::Vfs;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SnapshotStart {
Resume {
snapshot: ChainSnapshot,
tail: Vec<Vec<u8>>,
},
Whole(WholeFold),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WholeFold {
NoSnapshot,
Damaged(String),
OtherFoldVersion {
found: u32,
},
Stale,
Missing,
}
pub fn start_from_snapshot(
ledger_dir: &Path,
snapshot_dir: &Path,
fold_version: u32,
) -> Result<SnapshotStart, StorageError> {
start_through(&RealFs::new(), ledger_dir, snapshot_dir, fold_version)
}
pub(crate) fn start_through(
vfs: &dyn Vfs,
ledger_dir: &Path,
snapshot_dir: &Path,
fold_version: u32,
) -> Result<SnapshotStart, StorageError> {
let because = match read_snapshot(snapshot_dir)? {
StoredSnapshot::Absent => WholeFold::NoSnapshot,
StoredSnapshot::Damaged(reason) => WholeFold::Damaged(reason),
StoredSnapshot::Present(snapshot) if snapshot.fold_version() != fold_version => {
WholeFold::OtherFoldVersion {
found: snapshot.fold_version(),
}
}
StoredSnapshot::Present(snapshot) => return tail_through(vfs, ledger_dir, snapshot),
};
Ok(SnapshotStart::Whole(because))
}
pub fn tail_after(
ledger_dir: &Path,
snapshot: ChainSnapshot,
) -> Result<SnapshotStart, StorageError> {
tail_through(&RealFs::new(), ledger_dir, snapshot)
}
fn tail_through(
vfs: &dyn Vfs,
ledger_dir: &Path,
snapshot: ChainSnapshot,
) -> Result<SnapshotStart, StorageError> {
let because = match lines_from_cut(vfs, ledger_dir, snapshot.seq())? {
None => WholeFold::Missing,
Some(Cut { line_at_seq, tail }) => match snapshot.fit(&line_at_seq) {
SnapshotFit::Fits => return Ok(SnapshotStart::Resume { snapshot, tail }),
SnapshotFit::Stale => WholeFold::Stale,
},
};
Ok(SnapshotStart::Whole(because))
}
struct Cut {
line_at_seq: Vec<u8>,
tail: Vec<Vec<u8>>,
}
fn lines_from_cut(vfs: &dyn Vfs, dir: &Path, seq: Seq) -> Result<Option<Cut>, StorageError> {
let segments = ledger_segments_at(dir)?;
let Some((at, first)) = segments.iter().enumerate().rev().find_map(|(at, segment)| {
first_seq_of(segment)
.filter(|first| *first <= seq)
.map(|first| (at, first))
}) else {
return Ok(None);
};
let mut onward = segments.iter().skip(at);
let Some(holding) = onward.next() else {
return Ok(None);
};
let bytes = vfs.read(holding).map_err(io_err("read segment", holding))?;
let Some(mut cut) = from_the_end(&bytes, seq).or_else(|| from_the_start(&bytes, first, seq))
else {
return Ok(None);
};
for segment in onward {
let bytes = vfs.read(segment).map_err(io_err("read segment", segment))?;
cut.tail.extend(
complete_lines(&bytes)
.0
.into_iter()
.filter(|line| !line.is_empty())
.map(<[u8]>::to_vec),
);
}
Ok(Some(cut))
}
fn from_the_end(bytes: &[u8], seq: Seq) -> Option<Cut> {
let end = bytes.iter().rposition(|byte| *byte == b'\n')?;
let mut lines = bytes
.get(..end)?
.rsplit(|byte| *byte == b'\n')
.filter(|line| !line.is_empty());
let newest = lines.clone().next()?;
let back = claimed_seq(newest)?.value().checked_sub(seq.value())?;
let back = usize::try_from(back).ok()?;
let mut tail: Vec<Vec<u8>> = lines.by_ref().take(back).map(<[u8]>::to_vec).collect();
if tail.len() != back {
return None;
}
let line_at_seq = lines.next()?.to_vec();
tail.reverse();
Some(Cut { line_at_seq, tail })
}
fn from_the_start(bytes: &[u8], first: Seq, seq: Seq) -> Option<Cut> {
let skip = usize::try_from(seq.value().checked_sub(first.value())?).ok()?;
let mut lines = complete_lines(bytes)
.0
.into_iter()
.filter(|line| !line.is_empty())
.skip(skip);
let line_at_seq = lines.next()?.to_vec();
let tail = lines.map(<[u8]>::to_vec).collect();
Some(Cut { line_at_seq, tail })
}
fn first_seq_of(segment: &Path) -> Option<Seq> {
segment
.file_name()
.and_then(|name| name.to_str())
.and_then(segment_first_seq)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::indexing_slicing,
clippy::arithmetic_side_effects,
clippy::as_conversions,
reason = "test code"
)]
mod tests;