#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing,
clippy::string_slice,
clippy::arithmetic_side_effects,
)
)]
use std::fmt;
use std::io::Write as _;
use serde::{Deserialize, Serialize};
pub const LAYOUT_VERSION: u32 = 1;
pub const SNAPSHOT_OBJECT: &str = "snapshot.db.gz";
pub const SNAPSHOT_META_OBJECT: &str = "snapshot.json";
pub const MAX_SEGMENT_BYTES: u64 = 1024 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SegmentError {
MissingHeader,
BadHeader {
detail: String,
},
UnsupportedVersion {
version: u32,
},
Decompress {
detail: String,
},
DigestMismatch {
expected_len: u64,
actual_len: u64,
},
}
impl fmt::Display for SegmentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingHeader => write!(f, "segment payload has no header line"),
Self::BadHeader { detail } => write!(f, "segment header did not parse: {detail}"),
Self::UnsupportedVersion { version } => write!(
f,
"segment layout version {version} is not the one this build understands \
({LAYOUT_VERSION}) — upgrade autumn before restoring"
),
Self::Decompress { detail } => write!(f, "segment body did not decompress: {detail}"),
Self::DigestMismatch {
expected_len,
actual_len,
} => write!(
f,
"segment body failed verification (header promised {expected_len} byte(s), \
recovered {actual_len})"
),
}
}
}
impl std::error::Error for SegmentError {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SegmentHeader {
pub version: u32,
#[serde(default)]
pub index: u32,
pub seq: u64,
pub start_offset: u64,
pub end_offset: u64,
pub frame_count: u64,
pub commit_count: u64,
pub page_size: u32,
pub db_size_pages: u32,
pub salt1: u32,
pub salt2: u32,
pub created_at: String,
pub created_ms: i64,
pub sha256: String,
pub uncompressed_len: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SnapshotMeta {
pub version: u32,
pub generation: String,
pub created_at: String,
pub created_ms: i64,
pub sha256: String,
pub uncompressed_len: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GenerationInfo {
pub created_ms: i64,
pub nonce: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct SegmentRef {
pub index: u32,
pub seq: u64,
pub created_ms: i64,
}
#[must_use]
pub fn generation_id(created_ms: i64, nonce: u64) -> String {
let ms = created_ms.max(0);
format!("{ms:013}-{nonce:016x}")
}
#[must_use]
pub fn parse_generation_id(id: &str) -> Option<GenerationInfo> {
let (ms, salts) = id.split_once('-')?;
if ms.len() < 13 || salts.len() != 16 {
return None;
}
let created_ms: i64 = ms.parse().ok()?;
Some(GenerationInfo {
created_ms,
nonce: u64::from_str_radix(salts, 16).ok()?,
})
}
#[must_use]
pub fn root_prefix(prefix: Option<&str>, profile: &str) -> String {
let profile = profile.trim_matches('/');
prefix
.map(|p| p.trim_matches('/'))
.filter(|p| !p.is_empty())
.map_or_else(|| profile.to_owned(), |p| format!("{p}/{profile}"))
}
#[must_use]
pub fn generations_prefix(root: &str) -> String {
format!("{root}/generations/")
}
#[must_use]
pub fn generation_prefix(root: &str, generation: &str) -> String {
format!("{root}/generations/{generation}/")
}
#[must_use]
pub fn snapshot_key(root: &str, generation: &str) -> String {
format!("{root}/generations/{generation}/{SNAPSHOT_OBJECT}")
}
pub const HEAD_OBJECT: &str = "head.json";
#[must_use]
pub fn head_key(root: &str, generation: &str) -> String {
format!("{root}/generations/{generation}/{HEAD_OBJECT}")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct GenerationHead {
pub version: u32,
pub index: u32,
pub seq: u64,
pub created_ms: i64,
}
#[must_use]
pub fn snapshot_meta_key(root: &str, generation: &str) -> String {
format!("{root}/generations/{generation}/{SNAPSHOT_META_OBJECT}")
}
#[must_use]
pub fn segments_prefix(root: &str, generation: &str) -> String {
format!("{root}/generations/{generation}/segments/")
}
#[must_use]
pub fn segment_key(root: &str, generation: &str, index: u32, seq: u64, created_ms: i64) -> String {
let ms = created_ms.max(0);
format!("{root}/generations/{generation}/segments/{index:05}-{seq:010}-{ms:013}.seg")
}
#[must_use]
pub fn parse_segment_key(key: &str) -> Option<SegmentRef> {
let name = key.rsplit('/').next()?;
let stem = name.strip_suffix(".seg")?;
let mut parts = stem.split('-');
let index = parts.next()?.parse().ok()?;
let seq = parts.next()?.parse().ok()?;
let created_ms = parts.next()?.parse().ok()?;
if parts.next().is_some() {
return None;
}
Some(SegmentRef {
index,
seq,
created_ms,
})
}
#[must_use]
pub fn generation_of_key(root: &str, key: &str) -> Option<String> {
let rest = key.strip_prefix(&generations_prefix(root))?;
let generation = rest.split('/').next()?;
if generation.is_empty() {
None
} else {
Some(generation.to_owned())
}
}
pub const GENERATION_EPOCH_PREFIX: &str = "epoch-";
#[must_use]
pub fn generation_epoch_key(root: &str, generation: &str, epoch: u64) -> String {
format!("{root}/generations/{generation}/{GENERATION_EPOCH_PREFIX}{epoch:020}")
}
#[must_use]
pub fn parse_generation_epoch_key(root: &str, key: &str) -> Option<(String, u64)> {
let generation = generation_of_key(root, key)?;
let name = key.rsplit('/').next()?;
let epoch = name.strip_prefix(GENERATION_EPOCH_PREFIX)?.parse().ok()?;
Some((generation, epoch))
}
#[must_use]
pub fn epochs_prefix(root: &str) -> String {
format!("{root}/epochs/")
}
#[must_use]
pub fn epoch_marker_key(root: &str, epoch: u64) -> String {
format!("{root}/epochs/{epoch:020}")
}
#[must_use]
pub fn parse_epoch_marker_key(root: &str, key: &str) -> Option<u64> {
key.strip_prefix(&epochs_prefix(root))?.parse().ok()
}
#[must_use]
pub fn sha256_hex(data: &[u8]) -> String {
crate::sigv4::sha256_hex(data)
}
pub fn encode_segment(header: &SegmentHeader, raw: &[u8]) -> Result<Vec<u8>, SegmentError> {
let mut out = serde_json::to_vec(header).map_err(|e| SegmentError::BadHeader {
detail: e.to_string(),
})?;
out.push(b'\n');
let mut encoder = flate2::write::GzEncoder::new(out, flate2::Compression::default());
encoder
.write_all(raw)
.map_err(|e| SegmentError::Decompress {
detail: e.to_string(),
})?;
encoder.finish().map_err(|e| SegmentError::Decompress {
detail: e.to_string(),
})
}
pub fn decode_segment(payload: &[u8]) -> Result<(SegmentHeader, Vec<u8>), SegmentError> {
let newline = payload
.iter()
.position(|b| *b == b'\n')
.ok_or(SegmentError::MissingHeader)?;
let head = payload.get(..newline).ok_or(SegmentError::MissingHeader)?;
let body_start = newline.checked_add(1).ok_or(SegmentError::MissingHeader)?;
let body = payload
.get(body_start..)
.ok_or(SegmentError::MissingHeader)?;
let header: SegmentHeader =
serde_json::from_slice(head).map_err(|e| SegmentError::BadHeader {
detail: e.to_string(),
})?;
if header.version != LAYOUT_VERSION {
return Err(SegmentError::UnsupportedVersion {
version: header.version,
});
}
if header.uncompressed_len > MAX_SEGMENT_BYTES {
return Err(SegmentError::DigestMismatch {
expected_len: header.uncompressed_len,
actual_len: 0,
});
}
let mut raw = Vec::new();
let decoder = flate2::read::GzDecoder::new(body);
let mut decoder = std::io::Read::take(decoder, header.uncompressed_len.saturating_add(1));
std::io::copy(&mut decoder, &mut raw).map_err(|e| SegmentError::Decompress {
detail: e.to_string(),
})?;
let actual_len = raw.len() as u64;
if actual_len != header.uncompressed_len || sha256_hex(&raw) != header.sha256 {
return Err(SegmentError::DigestMismatch {
expected_len: header.uncompressed_len,
actual_len,
});
}
Ok((header, raw))
}
#[cfg(test)]
mod tests {
use super::*;
fn header(seq: u64, start: u64, end: u64, raw: &[u8]) -> SegmentHeader {
SegmentHeader {
version: LAYOUT_VERSION,
index: 0,
seq,
start_offset: start,
end_offset: end,
frame_count: 1,
commit_count: 1,
page_size: 4096,
db_size_pages: 3,
salt1: 1,
salt2: 2,
created_at: "2026-09-02T00:00:00Z".to_owned(),
created_ms: 1_788_000_000_000,
sha256: sha256_hex(raw),
uncompressed_len: raw.len() as u64,
}
}
#[test]
fn generation_ids_sort_chronologically_and_round_trip() {
let early = generation_id(1_000, 0xAABB_CCDD_0011_2233);
let late = generation_id(2_000, 1);
assert!(early < late, "{early} must sort before {late}");
assert_eq!(
parse_generation_id(&early),
Some(GenerationInfo {
created_ms: 1_000,
nonce: 0xAABB_CCDD_0011_2233,
})
);
assert_eq!(parse_generation_id("nope"), None);
assert_eq!(parse_generation_id("0000000001000-abc"), None);
}
#[test]
fn generation_id_clamps_a_pre_epoch_timestamp() {
let id = generation_id(-5, 2);
let (ms, _) = id.split_once('-').expect("id has a separator");
assert!(
ms.parse::<u64>().is_ok(),
"the timestamp component must stay unsigned: {id}"
);
assert_eq!(parse_generation_id(&id).map(|g| g.created_ms), Some(0));
assert!(
generation_id(0, 2) <= generation_id(1, 2),
"clamping must not break ordering"
);
}
#[test]
fn root_prefix_normalizes_slashes() {
assert_eq!(root_prefix(None, "prod"), "prod");
assert_eq!(root_prefix(Some(""), "prod"), "prod");
assert_eq!(root_prefix(Some("db"), "prod"), "db/prod");
assert_eq!(root_prefix(Some("/db/"), "/prod/"), "db/prod");
}
#[test]
fn segment_keys_sort_in_replication_order_and_round_trip() {
let root = root_prefix(Some("db"), "prod");
let generation = generation_id(1_000, 2);
let a = segment_key(&root, &generation, 0, 9, 1_500);
let b = segment_key(&root, &generation, 0, 10, 1_600);
let c = segment_key(&root, &generation, 1, 0, 1_700);
assert!(a < b, "seq 9 must sort before seq 10 ({a} vs {b})");
assert!(
b < c,
"index 0 must sort before index 1 whatever the seq ({b} vs {c})"
);
assert_eq!(
parse_segment_key(&b),
Some(SegmentRef {
index: 0,
seq: 10,
created_ms: 1_600
})
);
assert_eq!(
parse_segment_key(&c),
Some(SegmentRef {
index: 1,
seq: 0,
created_ms: 1_700
})
);
assert_eq!(
generation_of_key(&root, &b).as_deref(),
Some(generation.as_str())
);
assert_eq!(
parse_segment_key("db/prod/generations/g/segments/x.seg"),
None
);
assert_eq!(parse_segment_key(&snapshot_key(&root, &generation)), None);
}
#[test]
fn segment_payload_round_trips() {
let raw = b"the quick brown fox jumps over the lazy dog".repeat(64);
let encoded = encode_segment(&header(0, 0, 4120, &raw), &raw).expect("encode");
assert!(
encoded.len() < raw.len(),
"a compressible payload should shrink"
);
let (decoded_header, decoded) = decode_segment(&encoded).expect("decode");
assert_eq!(decoded, raw);
assert_eq!(decoded_header.seq, 0);
assert_eq!(decoded_header.end_offset, 4120);
}
#[test]
fn decode_refuses_a_tampered_body() {
let raw = b"payload".repeat(32);
let mut encoded = encode_segment(&header(1, 0, 32, &raw), &raw).expect("encode");
let last = encoded.len().saturating_sub(8);
encoded[last] ^= 0xFF;
let err = decode_segment(&encoded).expect_err("tampered body must be refused");
assert!(
matches!(
err,
SegmentError::DigestMismatch { .. } | SegmentError::Decompress { .. }
),
"unexpected error: {err}"
);
}
#[test]
fn decode_refuses_a_digest_that_does_not_match_the_body() {
let raw = b"payload".repeat(32);
let mut head = header(1, 0, 32, &raw);
head.sha256 = sha256_hex(b"something else");
let encoded = encode_segment(&head, &raw).expect("encode");
assert_eq!(
decode_segment(&encoded).expect_err("digest mismatch must be refused"),
SegmentError::DigestMismatch {
expected_len: raw.len() as u64,
actual_len: raw.len() as u64,
}
);
}
#[test]
fn decode_refuses_a_future_layout_version() {
let raw = b"payload".to_vec();
let mut head = header(0, 0, 8, &raw);
head.version = LAYOUT_VERSION.saturating_add(1);
let encoded = encode_segment(&head, &raw).expect("encode");
assert_eq!(
decode_segment(&encoded).expect_err("future version must be refused"),
SegmentError::UnsupportedVersion {
version: LAYOUT_VERSION + 1
}
);
}
#[test]
fn decode_refuses_a_payload_without_a_header_line() {
assert_eq!(
decode_segment(b"no newline here").expect_err("must refuse"),
SegmentError::MissingHeader
);
}
}