use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use bincode::Options;
use crate::error::{DbError, Result};
use crate::temporal::replay::MaterializedState;
use crate::util::crc32::Crc32;
const SNAP_MAGIC: [u8; 4] = *b"MACR";
const SNAP_FORMAT_VERSION: u16 = 4;
const SNAP_HEADER_LEN: usize = 38;
const SNAP_CRC_OFFSET: usize = SNAP_HEADER_LEN - 4;
fn taken_at_micros(state: &MaterializedState) -> u64 {
crate::util::timestamp::parse(&state.timestamp)
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_micros() as u64)
.unwrap_or(0)
}
fn snapshot_header(
schema_version: u32,
taken_at: u64,
payload: &[u8],
plain_len: u64,
) -> [u8; SNAP_HEADER_LEN] {
let mut h = [0u8; SNAP_HEADER_LEN];
h[0..4].copy_from_slice(&SNAP_MAGIC);
h[4..6].copy_from_slice(&SNAP_FORMAT_VERSION.to_le_bytes());
h[6..10].copy_from_slice(&schema_version.to_le_bytes());
h[10..18].copy_from_slice(&taken_at.to_le_bytes());
h[18..26].copy_from_slice(&(payload.len() as u64).to_le_bytes());
h[26..34].copy_from_slice(&plain_len.to_le_bytes());
let mut crc = Crc32::new();
crc.update(&h[..SNAP_CRC_OFFSET]);
crc.update(payload);
h[SNAP_CRC_OFFSET..].copy_from_slice(&crc.finish().to_le_bytes());
h
}
fn header_taken_at(path: &Path) -> Option<u64> {
let mut file = fs::File::open(path).ok()?;
let mut head = [0u8; SNAP_HEADER_LEN];
file.read_exact(&mut head).ok()?;
if head[0..4] != SNAP_MAGIC {
return None;
}
if u16::from_le_bytes([head[4], head[5]]) != SNAP_FORMAT_VERSION {
return None;
}
let micros = u64::from_le_bytes(head[10..18].try_into().ok()?);
(micros > 0).then_some(micros)
}
const SEQ_WIDTH: usize = 19;
fn snapshot_filename(seq_anchor: i64) -> String {
format!("{seq_anchor:0SEQ_WIDTH$}.snap.zst")
}
pub(crate) fn seq_from_filename(path: &Path) -> Option<i64> {
path.file_name()?
.to_str()?
.strip_suffix(".snap.zst")?
.parse()
.ok()
}
#[cfg(unix)]
fn sync_directory(dir: &Path) -> std::io::Result<()> {
fs::File::open(dir)?.sync_all()
}
#[cfg(not(unix))]
fn sync_directory(_dir: &Path) -> std::io::Result<()> {
Ok(())
}
pub fn save_snapshot(snapshots_dir: &Path, state: &MaterializedState) -> Result<PathBuf> {
let path = snapshots_dir.join(snapshot_filename(state.seq_anchor));
let tmp_path = path.with_extension("tmp");
let fail = |what: &str, e: &dyn std::fmt::Display| DbError::SnapshotWriteFailed {
path: path.display().to_string(),
reason: format!("{what}: {e}"),
};
fs::create_dir_all(snapshots_dir)
.map_err(|e| fail("failed to create snapshot directory", &e))?;
let serialized =
bincode::serialize(state).map_err(|e| fail("failed to serialize snapshot", &e))?;
let compressed = zstd::encode_all(&serialized[..], 3)
.map_err(|e| fail("failed to compress snapshot", &e))?;
let mut file =
fs::File::create(&tmp_path).map_err(|e| fail("failed to create snapshot temp file", &e))?;
file.write_all(&snapshot_header(
crate::schema::migrations::SCHEMA_VERSION,
taken_at_micros(state),
&compressed,
serialized.len() as u64,
))
.map_err(|e| fail("failed to write snapshot header", &e))?;
file.write_all(&compressed)
.map_err(|e| fail("failed to write snapshot bytes", &e))?;
file.sync_all()
.map_err(|e| fail("failed to flush snapshot to disk", &e))?;
drop(file);
fs::rename(&tmp_path, &path).map_err(|e| {
let _ = fs::remove_file(&tmp_path);
fail("failed to publish snapshot", &e)
})?;
sync_directory(snapshots_dir)
.map_err(|e| fail("failed to make the snapshot's directory entry durable", &e))?;
Ok(path)
}
pub fn load_snapshot(path: &Path) -> Result<MaterializedState> {
let label = path.display().to_string();
let raw = fs::read(path).map_err(|e| DbError::SnapshotCorrupt {
path: label.clone(),
reason: format!("could not be read: {e}"),
})?;
parse_snapshot(&label, &raw)
}
pub(crate) fn parse_snapshot(label: &str, raw: &[u8]) -> Result<MaterializedState> {
let damaged = |reason: String| DbError::SnapshotCorrupt {
path: label.to_string(),
reason,
};
let foreign = |reason: String| DbError::SnapshotIncompatible {
path: label.to_string(),
reason,
};
if raw.len() < SNAP_HEADER_LEN || raw[0..4] != SNAP_MAGIC {
return Err(foreign(
"not a macrame snapshot, or written before the versioned container \
existed (0.5.4 and earlier)"
.to_string(),
));
}
let format = u16::from_le_bytes([raw[4], raw[5]]);
let schema = u32::from_le_bytes([raw[6], raw[7], raw[8], raw[9]]);
let expected_schema = crate::schema::migrations::SCHEMA_VERSION;
if format != SNAP_FORMAT_VERSION || schema != expected_schema {
return Err(foreign(format!(
"snapshot is format v{format}/schema v{schema}; this build reads \
format v{SNAP_FORMAT_VERSION}/schema v{expected_schema}"
)));
}
let payload_len = u64::from_le_bytes(raw[18..26].try_into().unwrap());
let plain_len = u64::from_le_bytes(raw[26..34].try_into().unwrap());
let declared_crc =
u32::from_le_bytes(raw[SNAP_CRC_OFFSET..SNAP_HEADER_LEN].try_into().unwrap());
let payload = &raw[SNAP_HEADER_LEN..];
if payload.len() as u64 != payload_len {
return Err(damaged(format!(
"the header declares {payload_len} payload bytes and the file \
carries {}: truncated, or something was appended",
payload.len()
)));
}
let mut crc = Crc32::new();
crc.update(&raw[..SNAP_CRC_OFFSET]);
crc.update(payload);
let actual_crc = crc.finish();
if actual_crc != declared_crc {
return Err(damaged(format!(
"checksum mismatch: the header declares {declared_crc:#010x} and \
the bytes hash to {actual_crc:#010x}"
)));
}
let mut decoder =
zstd::Decoder::new(payload).map_err(|e| damaged(format!("zstd rejected it: {e}")))?;
let mut plain = Vec::new();
decoder
.by_ref()
.take(plain_len.saturating_add(1))
.read_to_end(&mut plain)
.map_err(|e| damaged(format!("could not be decompressed: {e}")))?;
if plain.len() as u64 != plain_len {
return Err(damaged(format!(
"the header declares {plain_len} plaintext bytes and the payload \
decompressed to {}",
plain.len()
)));
}
let state: MaterializedState = bincode::DefaultOptions::new()
.with_fixint_encoding()
.allow_trailing_bytes()
.with_limit(plain.len() as u64)
.deserialize(&plain)
.map_err(|e| damaged(format!("could not be deserialized: {e}")))?;
Ok(state)
}
async fn save_and_prune(snapshots_dir: PathBuf, state: MaterializedState) -> Result<PathBuf> {
let seq = state.seq_anchor;
tokio::task::spawn_blocking(move || {
let path = save_snapshot(&snapshots_dir, &state)?;
cleanup_expired_snapshots(&snapshots_dir)?;
Ok(path)
})
.await
.unwrap_or_else(|e| {
Err(DbError::ReplayCorrupt {
seq,
reason: format!("the thread writing the snapshot did not finish: {e}"),
})
})
}
pub async fn write_final(
conn: &libsql::Connection,
snapshots_dir: &Path,
ts: &str,
archive_path: Option<&Path>,
) -> Result<PathBuf> {
let state =
crate::temporal::replay::reconstruct(conn, ts, archive_path, Some(snapshots_dir)).await?;
save_and_prune(snapshots_dir.to_path_buf(), state).await
}
const RETAIN: usize = 5;
const RETAIN_DAYS: i64 = 30;
const MICROS_PER_DAY: u64 = 86_400_000_000;
pub fn cleanup_expired_snapshots(snapshots_dir: &Path) -> Result<usize> {
if !snapshots_dir.exists() {
return Ok(0);
}
let read_dir = fs::read_dir(snapshots_dir).map_err(|e| DbError::ReplayCorrupt {
seq: 0,
reason: format!("failed to read snapshot dir: {e}"),
})?;
let mut snapshots: Vec<(i64, PathBuf, Option<i64>)> = Vec::new();
for entry in read_dir.flatten() {
let path = entry.path();
match path.extension().and_then(|e| e.to_str()) {
Some("tmp") => {
let _ = fs::remove_file(&path);
}
Some("zst") => match seq_from_filename(&path) {
Some(seq) => {
let day = header_taken_at(&path).map(|micros| (micros / MICROS_PER_DAY) as i64);
snapshots.push((seq, path, day));
}
None => tracing::warn!("snapshot cleanup: unparseable filename {path:?}, skipping"),
},
_ => {}
}
}
snapshots.sort_by_key(|(seq, _, _)| *seq);
let mut keep: std::collections::HashSet<&PathBuf> = snapshots
.iter()
.rev()
.take(RETAIN)
.map(|(_, path, _)| path)
.collect();
if let Some(today) = snapshots.iter().filter_map(|(_, _, day)| *day).max() {
let horizon = today - (RETAIN_DAYS - 1);
let mut newest_of_day: std::collections::BTreeMap<i64, &PathBuf> =
std::collections::BTreeMap::new();
for (_, path, day) in &snapshots {
if let Some(day) = *day {
if day >= horizon {
newest_of_day.insert(day, path);
}
}
}
keep.extend(newest_of_day.into_values());
}
let doomed: Vec<PathBuf> = snapshots
.iter()
.filter(|(_, path, _)| !keep.contains(path))
.map(|(_, path, _)| path.clone())
.collect();
let mut removed = 0;
for path in doomed {
if let Err(e) = fs::remove_file(&path) {
tracing::warn!("failed to remove expired snapshot {path:?}: {e}");
} else {
removed += 1;
}
}
Ok(removed)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SnapshotCadence {
pub every_entries: i64,
pub poll_interval: std::time::Duration,
}
impl Default for SnapshotCadence {
fn default() -> Self {
Self {
every_entries: 10_000,
poll_interval: std::time::Duration::from_secs(5),
}
}
}
fn newest_anchor_on_disk(snapshots_dir: &Path) -> i64 {
let Ok(entries) = fs::read_dir(snapshots_dir) else {
return 0;
};
entries
.flatten()
.map(|e| e.path())
.filter_map(|p| seq_from_filename(&p))
.max()
.unwrap_or(0)
}
async fn log_head(conn: &libsql::Connection) -> Result<Option<(i64, String)>> {
let mut rows = conn
.query(
"SELECT MAX(seq_id), MAX(recorded_at) FROM transaction_log",
(),
)
.await?;
let Some(row) = rows.next().await? else {
return Ok(None);
};
match (row.get::<i64>(0), row.get::<String>(1)) {
(Ok(seq), Ok(ts)) => Ok(Some((seq, ts))),
_ => Ok(None),
}
}
pub(crate) async fn run_cadence(
conn: libsql::Connection,
snapshots_dir: PathBuf,
archive_path: PathBuf,
cadence: SnapshotCadence,
mut stop: tokio::sync::watch::Receiver<bool>,
) {
let mut anchored = newest_anchor_on_disk(&snapshots_dir);
loop {
tokio::select! {
biased;
_ = stop.changed() => return,
_ = tokio::time::sleep(cadence.poll_interval) => {}
}
let head = match log_head(&conn).await {
Ok(Some(head)) => head,
Ok(None) => continue,
Err(e) => {
tracing::warn!("snapshot cadence: could not read the log head: {e}");
continue;
}
};
let (max_seq, ts) = head;
if max_seq - anchored < cadence.every_entries {
continue;
}
let archive = archive_path.exists().then_some(archive_path.as_path());
match write_final(&conn, &snapshots_dir, &ts, archive).await {
Ok(path) => {
anchored = seq_from_filename(&path).unwrap_or(max_seq);
tracing::debug!("snapshot cadence: anchored at seq {anchored} ({path:?})");
}
Err(e) => {
tracing::warn!("snapshot cadence: failed to write an anchor: {e}");
}
}
}
}
#[cfg(any(test, feature = "fuzzing"))]
pub(crate) fn wrap_plaintext(plain: &[u8]) -> Vec<u8> {
let compressed = zstd::encode_all(plain, 3).expect("in-memory zstd encode");
wrap_payload(&compressed, plain.len() as u64)
}
#[cfg(any(test, feature = "fuzzing"))]
pub(crate) fn wrap_payload(payload: &[u8], plain_len: u64) -> Vec<u8> {
let header = snapshot_header(
crate::schema::migrations::SCHEMA_VERSION,
0,
payload,
plain_len,
);
let mut out = Vec::with_capacity(header.len() + payload.len());
out.extend_from_slice(&header);
out.extend_from_slice(payload);
out
}
#[cfg(feature = "fuzzing")]
#[doc(hidden)]
pub mod fuzzing {
use super::*;
pub fn parse(raw: &[u8]) -> Result<MaterializedState> {
parse_snapshot("<fuzz>", raw)
}
pub fn wrap_plaintext(plain: &[u8]) -> Vec<u8> {
super::wrap_plaintext(plain)
}
pub fn wrap_payload(payload: &[u8], plain_len: u64) -> Vec<u8> {
super::wrap_payload(payload, plain_len)
}
pub fn payload_of(container: &[u8]) -> Option<(&[u8], u64)> {
if container.len() < SNAP_HEADER_LEN || container[0..4] != SNAP_MAGIC {
return None;
}
let plain_len = u64::from_le_bytes(container[26..34].try_into().ok()?);
Some((&container[SNAP_HEADER_LEN..], plain_len))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::temporal::as_of::NodeAttributes;
use crate::temporal::replay::EdgeBelief;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
const TS: &str = "2026-08-24T12:00:00.000000Z";
fn bulky_state(seq: i64) -> MaterializedState {
let mut concepts = HashMap::new();
for i in 0..20_000u32 {
concepts.insert(
format!("c{i}"),
NodeAttributes {
id: format!("c{i}"),
title: format!("concept number {i}"),
content: format!("{i} ").repeat(40),
embedding_model: None,
},
);
}
MaterializedState {
seq_anchor: seq,
timestamp: TS.to_string(),
concepts,
edges: Vec::new(),
predates_recorded_history: false,
}
}
#[test]
fn the_snapshot_write_does_not_hold_the_runtime() {
let dir = tempfile::tempdir().unwrap();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let ticks = rt.block_on(async {
let ticks = Arc::new(AtomicU64::new(0));
let counter = Arc::clone(&ticks);
let ticker = tokio::spawn(async move {
loop {
counter.fetch_add(1, Ordering::Relaxed);
tokio::task::yield_now().await;
}
});
save_and_prune(dir.path().to_path_buf(), bulky_state(1))
.await
.expect("the snapshot must still be written");
ticker.abort();
ticks.load(Ordering::Relaxed)
});
assert!(
ticks > 0,
"no other task ran while the snapshot was being written: the \
serialisation is back on the runtime worker (§2.4, W8.1)"
);
}
#[test]
fn a_snapshot_written_off_thread_reads_back_unchanged() {
let dir = tempfile::tempdir().unwrap();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let state = bulky_state(77);
let path = rt
.block_on(save_and_prune(dir.path().to_path_buf(), state.clone()))
.unwrap();
assert_eq!(seq_from_filename(&path), Some(77));
let loaded = load_snapshot(&path).unwrap();
assert_eq!(loaded.seq_anchor, state.seq_anchor);
assert_eq!(loaded.timestamp, state.timestamp);
assert_eq!(loaded.concepts.len(), state.concepts.len());
assert_eq!(loaded.concepts["c19999"], state.concepts["c19999"]);
}
fn forge_plain_len(path: &Path, plain_len: u64) {
let mut raw = fs::read(path).unwrap();
raw[26..34].copy_from_slice(&plain_len.to_le_bytes());
let mut crc = Crc32::new();
crc.update(&raw[..SNAP_CRC_OFFSET]);
crc.update(&raw[SNAP_HEADER_LEN..]);
let checksum = crc.finish().to_le_bytes();
raw[SNAP_CRC_OFFSET..SNAP_HEADER_LEN].copy_from_slice(&checksum);
fs::write(path, &raw).unwrap();
}
#[test]
fn a_payload_larger_than_its_declared_length_stops_at_the_bound() {
let dir = tempfile::tempdir().unwrap();
let path = save_snapshot(dir.path(), &bulky_state(3)).unwrap();
forge_plain_len(&path, 10);
match load_snapshot(&path).unwrap_err() {
DbError::SnapshotCorrupt { reason, .. } => {
assert!(
reason.contains("10 plaintext bytes") && reason.contains("11"),
"the bound must be what stopped it, and it must say so: {reason}"
);
}
other => panic!("expected SnapshotCorrupt, got {other:?}"),
}
}
#[test]
fn a_payload_smaller_than_its_declared_length_is_refused_too() {
let dir = tempfile::tempdir().unwrap();
let path = save_snapshot(dir.path(), &bulky_state(4)).unwrap();
forge_plain_len(&path, u32::MAX as u64);
match load_snapshot(&path).unwrap_err() {
DbError::SnapshotCorrupt { reason, .. } => {
assert!(reason.contains("plaintext bytes"), "{reason}");
}
other => panic!("expected SnapshotCorrupt, got {other:?}"),
}
}
#[test]
fn a_declared_length_of_u64_max_neither_wraps_nor_allocates() {
let dir = tempfile::tempdir().unwrap();
let path = save_snapshot(dir.path(), &bulky_state(5)).unwrap();
forge_plain_len(&path, u64::MAX);
match load_snapshot(&path).unwrap_err() {
DbError::SnapshotCorrupt { reason, .. } => {
assert!(
reason.contains(&format!("{} plaintext bytes", u64::MAX)),
"{reason}"
);
}
other => panic!("expected SnapshotCorrupt, got {other:?}"),
}
}
#[test]
fn doctoring_the_header_without_the_checksum_fails_earlier() {
let dir = tempfile::tempdir().unwrap();
let path = save_snapshot(dir.path(), &bulky_state(6)).unwrap();
let mut raw = fs::read(&path).unwrap();
raw[26..34].copy_from_slice(&10u64.to_le_bytes());
fs::write(&path, &raw).unwrap();
match load_snapshot(&path).unwrap_err() {
DbError::SnapshotCorrupt { reason, .. } => {
assert!(reason.contains("checksum mismatch"), "{reason}");
}
other => panic!("expected SnapshotCorrupt, got {other:?}"),
}
}
#[cfg(unix)]
#[test]
fn a_directory_handle_can_be_synced() {
let dir = tempfile::tempdir().unwrap();
sync_directory(dir.path()).expect("fsync on a directory descriptor");
}
#[cfg(not(unix))]
#[test]
fn the_directory_sync_is_inert_off_unix() {
let dir = tempfile::tempdir().unwrap();
sync_directory(&dir.path().join("no-such-directory"))
.expect("the non-unix branch has nothing that can fail");
}
#[test]
fn a_completed_save_leaves_no_temporary_behind() {
let dir = tempfile::tempdir().unwrap();
let path = save_snapshot(dir.path(), &bulky_state(7)).unwrap();
assert!(path.exists(), "the snapshot is at its final name");
let leftovers: Vec<PathBuf> = fs::read_dir(dir.path())
.unwrap()
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("tmp"))
.collect();
assert!(leftovers.is_empty(), "left behind {leftovers:?}");
}
fn modest_state(seq: i64) -> MaterializedState {
let mut concepts = HashMap::new();
for i in 0..8u32 {
concepts.insert(
format!("c{i}"),
NodeAttributes {
id: format!("c{i}"),
title: format!("concept {i}"),
content: format!("some content for {i} ").repeat(3),
embedding_model: (i % 2 == 0).then(|| "model-a".to_string()),
},
);
}
MaterializedState {
seq_anchor: seq,
timestamp: TS.to_string(),
concepts,
edges: vec![EdgeBelief {
source_id: "c0".to_string(),
target_id: "c1".to_string(),
edge_type: "relates_to".to_string(),
valid_from: TS.to_string(),
valid_to: "A".to_string(),
branch_id: crate::schema::ddl::MAIN_BRANCH.to_string(),
}],
predates_recorded_history: false,
}
}
fn assert_named_refusal(what: &str, err: DbError) {
match err {
DbError::SnapshotCorrupt { .. } | DbError::SnapshotIncompatible { .. } => {}
other => panic!("{what}: expected a named snapshot error, got {other:?}"),
}
}
#[test]
fn every_single_bit_flip_in_a_snapshot_is_refused() {
let dir = tempfile::tempdir().unwrap();
let path = save_snapshot(dir.path(), &modest_state(1)).unwrap();
let clean = fs::read(&path).unwrap();
parse_snapshot("clean", &clean)
.expect("the fixture must load, or nothing below means anything");
let mut refused = 0usize;
for byte in 0..clean.len() {
for bit in 0..8u8 {
let mut damaged = clean.clone();
damaged[byte] ^= 1 << bit;
match parse_snapshot("damaged", &damaged) {
Ok(_) => panic!("bit {bit} of byte {byte} changed and the file still loaded"),
Err(e) => {
assert_named_refusal(&format!("bit {bit} of byte {byte}"), e);
refused += 1;
}
}
}
}
assert_eq!(refused, clean.len() * 8, "every bit of the file was tried");
}
#[test]
fn every_truncation_and_every_extension_is_refused() {
let dir = tempfile::tempdir().unwrap();
let path = save_snapshot(dir.path(), &modest_state(2)).unwrap();
let clean = fs::read(&path).unwrap();
for cut in 0..clean.len() {
match parse_snapshot("cut", &clean[..cut]) {
Ok(_) => panic!("a {cut}-byte prefix loaded as a whole snapshot"),
Err(e) => assert_named_refusal(&format!("{cut}-byte prefix"), e),
}
}
for extra in [1usize, 7, 64, 4096] {
let mut grown = clean.clone();
grown.extend(std::iter::repeat_n(0u8, extra));
match parse_snapshot("grown", &grown) {
Ok(_) => panic!("{extra} appended bytes went unnoticed"),
Err(e) => assert_named_refusal(&format!("{extra} appended bytes"), e),
}
}
}
#[test]
fn arbitrary_plaintext_behind_a_valid_checksum_never_panics() {
let plain = bincode::serialize(&modest_state(3)).unwrap();
let mut answered = 0usize;
for byte in 0..plain.len() {
for bit in [0u8, 3, 7] {
let mut mutated = plain.clone();
mutated[byte] ^= 1 << bit;
match parse_snapshot("wrapped", &wrap_plaintext(&mutated)) {
Ok(_) => answered += 1,
Err(e) => {
assert_named_refusal(&format!("bit {bit} of plaintext byte {byte}"), e);
answered += 1;
}
}
}
}
assert_eq!(answered, plain.len() * 3);
for odd in [vec![], vec![0u8; 1], vec![0u8; 4096], vec![0xFFu8; 64]] {
match parse_snapshot("odd", &wrap_plaintext(&odd)) {
Ok(_) => {}
Err(e) => assert_named_refusal("an odd plaintext", e),
}
}
}
#[test]
fn a_decompression_bomb_with_a_valid_checksum_stops_at_the_declared_length() {
let bomb = zstd::encode_all(&vec![0u8; 64 * 1024 * 1024][..], 3).unwrap();
assert!(
bomb.len() < 64 * 1024,
"the fixture must actually be a bomb"
);
let container = wrap_payload(&bomb, 1024);
match parse_snapshot("bomb", &container).unwrap_err() {
DbError::SnapshotCorrupt { reason, .. } => {
assert!(
reason.contains("1024 plaintext bytes") && reason.contains("1025"),
"the reader should stop one byte past the declared length: {reason}"
);
}
other => panic!("expected SnapshotCorrupt, got {other:?}"),
}
}
}