use std::{
fs::File,
io::Write as _,
path::{Path, PathBuf},
};
use sha2::{Digest as _, Sha256};
pub const MAGIC: [u8; 4] = *b"THW1";
pub const FORMAT_REVISION: u8 = 1;
const PARTITION_DIGEST_LEN: usize = 32;
const INCARNATION_LEN: usize = 32;
const HEAD_LEN: usize = 8;
const FRONTIER_LEN: usize = 8;
const ROOT_LEN: usize = 32;
const LEAF_COUNT_LEN: usize = 8;
const CHECKSUM_LEN: usize = 32;
const BOUND_LEN: usize = MAGIC.len()
+ 1
+ PARTITION_DIGEST_LEN
+ INCARNATION_LEN
+ HEAD_LEN
+ FRONTIER_LEN
+ ROOT_LEN
+ LEAF_COUNT_LEN;
const RECORD_LEN: usize = BOUND_LEN + CHECKSUM_LEN;
pub const FILE_NAME: &str = "tailhighwater";
const TEMP_FILE_NAME: &str = "tailhighwater.writing";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TailHighWater {
partition_digest: [u8; PARTITION_DIGEST_LEN],
incarnation: [u8; INCARNATION_LEN],
logical_head: u64,
physical_frontier: u64,
covering_root: [u8; ROOT_LEN],
covering_leaf_count: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TailHighWaterOutcome {
Fresh,
Present(TailHighWater),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum HighWaterError {
#[error("tail high-water i/o failure: {0}")]
Io(#[from] std::io::Error),
#[error("tail high-water is {found} byte(s), not {expected}")]
WrongLength {
expected: usize,
found: usize,
},
#[error("tail high-water carries another family's magic")]
WrongMagic,
#[error("tail high-water format revision {found} is not {expected}")]
UnsupportedRevision {
expected: u8,
found: u8,
},
#[error("tail high-water fails its checksum")]
ChecksumMismatch,
#[error("tail high-water was recorded for a different partition")]
WrongPartition,
}
impl TailHighWater {
#[must_use]
pub fn new(
partition: &str,
incarnation: [u8; INCARNATION_LEN],
logical_head: u64,
physical_frontier: u64,
covering_root: [u8; ROOT_LEN],
covering_leaf_count: u64,
) -> Self {
Self {
partition_digest: digest_partition(partition),
incarnation,
logical_head,
physical_frontier,
covering_root,
covering_leaf_count,
}
}
#[must_use]
pub const fn incarnation(&self) -> [u8; INCARNATION_LEN] {
self.incarnation
}
#[must_use]
pub const fn logical_head(&self) -> u64 {
self.logical_head
}
#[must_use]
pub const fn physical_frontier(&self) -> u64 {
self.physical_frontier
}
#[must_use]
pub const fn covering_root(&self) -> [u8; ROOT_LEN] {
self.covering_root
}
#[must_use]
pub const fn covering_leaf_count(&self) -> u64 {
self.covering_leaf_count
}
fn to_bytes(self) -> [u8; RECORD_LEN] {
let mut bound = [0_u8; BOUND_LEN];
let mut at = 0;
bound[at..at + MAGIC.len()].copy_from_slice(&MAGIC);
at += MAGIC.len();
bound[at] = FORMAT_REVISION;
at += 1;
bound[at..at + PARTITION_DIGEST_LEN].copy_from_slice(&self.partition_digest);
at += PARTITION_DIGEST_LEN;
bound[at..at + INCARNATION_LEN].copy_from_slice(&self.incarnation);
at += INCARNATION_LEN;
bound[at..at + HEAD_LEN].copy_from_slice(&self.logical_head.to_be_bytes());
at += HEAD_LEN;
bound[at..at + FRONTIER_LEN].copy_from_slice(&self.physical_frontier.to_be_bytes());
at += FRONTIER_LEN;
bound[at..at + ROOT_LEN].copy_from_slice(&self.covering_root);
at += ROOT_LEN;
bound[at..at + LEAF_COUNT_LEN].copy_from_slice(&self.covering_leaf_count.to_be_bytes());
at += LEAF_COUNT_LEN;
debug_assert_eq!(at, BOUND_LEN);
let checksum = Sha256::digest(bound);
let mut record = [0_u8; RECORD_LEN];
record[..BOUND_LEN].copy_from_slice(&bound);
record[BOUND_LEN..].copy_from_slice(&checksum);
record
}
fn from_bytes(bytes: &[u8], partition: &str) -> Result<Self, HighWaterError> {
if bytes.len() != RECORD_LEN {
return Err(HighWaterError::WrongLength {
expected: RECORD_LEN,
found: bytes.len(),
});
}
let (bound, checksum) = bytes.split_at(BOUND_LEN);
let computed = Sha256::digest(bound);
if computed.as_slice() != checksum {
return Err(HighWaterError::ChecksumMismatch);
}
if bound[..MAGIC.len()] != MAGIC {
return Err(HighWaterError::WrongMagic);
}
let mut at = MAGIC.len();
let revision = bound[at];
at += 1;
if revision != FORMAT_REVISION {
return Err(HighWaterError::UnsupportedRevision {
expected: FORMAT_REVISION,
found: revision,
});
}
let partition_digest: [u8; PARTITION_DIGEST_LEN] = bound[at..at + PARTITION_DIGEST_LEN]
.try_into()
.expect("slice is exactly PARTITION_DIGEST_LEN bytes");
at += PARTITION_DIGEST_LEN;
if partition_digest != digest_partition(partition) {
return Err(HighWaterError::WrongPartition);
}
let incarnation: [u8; INCARNATION_LEN] = bound[at..at + INCARNATION_LEN]
.try_into()
.expect("slice is exactly INCARNATION_LEN bytes");
at += INCARNATION_LEN;
let logical_head = u64::from_be_bytes(
bound[at..at + HEAD_LEN]
.try_into()
.expect("slice is exactly HEAD_LEN bytes"),
);
at += HEAD_LEN;
let physical_frontier = u64::from_be_bytes(
bound[at..at + FRONTIER_LEN]
.try_into()
.expect("slice is exactly FRONTIER_LEN bytes"),
);
at += FRONTIER_LEN;
let covering_root: [u8; ROOT_LEN] = bound[at..at + ROOT_LEN]
.try_into()
.expect("slice is exactly ROOT_LEN bytes");
at += ROOT_LEN;
let covering_leaf_count = u64::from_be_bytes(
bound[at..at + LEAF_COUNT_LEN]
.try_into()
.expect("slice is exactly LEAF_COUNT_LEN bytes"),
);
at += LEAF_COUNT_LEN;
debug_assert_eq!(at, BOUND_LEN);
Ok(Self {
partition_digest,
incarnation,
logical_head,
physical_frontier,
covering_root,
covering_leaf_count,
})
}
pub fn write_atomic(self, dir: &Path) -> Result<(), HighWaterError> {
std::fs::create_dir_all(dir)?;
let bytes = self.to_bytes();
let temp = dir.join(TEMP_FILE_NAME);
{
let mut file = File::create(&temp)?;
file.write_all(&bytes)?;
file.sync_all()?;
}
std::fs::rename(&temp, dir.join(FILE_NAME))?;
File::open(dir)?.sync_all()?;
Ok(())
}
pub fn read(dir: &Path, partition: &str) -> Result<TailHighWaterOutcome, HighWaterError> {
let path: PathBuf = dir.join(FILE_NAME);
let bytes = match std::fs::read(&path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Ok(TailHighWaterOutcome::Fresh);
}
Err(error) => return Err(error.into()),
};
Self::from_bytes(&bytes, partition).map(TailHighWaterOutcome::Present)
}
}
fn digest_partition(partition: &str) -> [u8; PARTITION_DIGEST_LEN] {
Sha256::digest(partition.as_bytes()).into()
}
#[cfg(test)]
mod tests {
use super::*;
fn sample(partition: &str) -> TailHighWater {
TailHighWater::new(partition, [7_u8; 32], 42, 44, [9_u8; 32], 43)
}
#[test]
fn a_fresh_directory_reads_as_fresh_not_an_error() {
let dir = tempfile::tempdir().expect("temp dir");
let outcome = TailHighWater::read(dir.path(), "conv-1").expect("read");
assert_eq!(outcome, TailHighWaterOutcome::Fresh);
}
#[test]
fn a_written_high_water_survives_a_round_trip() {
let dir = tempfile::tempdir().expect("temp dir");
let written = sample("conv-1");
written.write_atomic(dir.path()).expect("write");
let outcome = TailHighWater::read(dir.path(), "conv-1").expect("read");
assert_eq!(outcome, TailHighWaterOutcome::Present(written));
}
#[test]
fn a_second_write_replaces_the_first_rather_than_appending() {
let dir = tempfile::tempdir().expect("temp dir");
sample("conv-1").write_atomic(dir.path()).expect("write");
let second = TailHighWater::new("conv-1", [7_u8; 32], 99, 101, [9_u8; 32], 100);
second.write_atomic(dir.path()).expect("write");
let outcome = TailHighWater::read(dir.path(), "conv-1").expect("read");
assert_eq!(outcome, TailHighWaterOutcome::Present(second));
assert!(!dir.path().join(TEMP_FILE_NAME).exists());
}
#[test]
fn a_truncated_record_is_refused_not_read_as_shorter_or_empty() {
let dir = tempfile::tempdir().expect("temp dir");
sample("conv-1").write_atomic(dir.path()).expect("write");
let mut bytes = std::fs::read(dir.path().join(FILE_NAME)).expect("read raw");
bytes.truncate(bytes.len() - 1);
std::fs::write(dir.path().join(FILE_NAME), &bytes).expect("write truncated");
let error = TailHighWater::read(dir.path(), "conv-1").expect_err("must refuse");
assert!(
matches!(error, HighWaterError::WrongLength { .. }),
"expected WrongLength, got {error}"
);
}
#[test]
fn a_flipped_checksum_byte_is_refused() {
let dir = tempfile::tempdir().expect("temp dir");
sample("conv-1").write_atomic(dir.path()).expect("write");
let mut bytes = std::fs::read(dir.path().join(FILE_NAME)).expect("read raw");
let last = bytes.len() - 1;
bytes[last] ^= 0xFF;
std::fs::write(dir.path().join(FILE_NAME), &bytes).expect("write flipped");
let error = TailHighWater::read(dir.path(), "conv-1").expect_err("must refuse");
assert!(matches!(error, HighWaterError::ChecksumMismatch));
}
#[test]
fn a_flipped_body_byte_is_refused_by_the_checksum() {
let dir = tempfile::tempdir().expect("temp dir");
sample("conv-1").write_atomic(dir.path()).expect("write");
let mut bytes = std::fs::read(dir.path().join(FILE_NAME)).expect("read raw");
bytes[10] ^= 0xFF;
std::fs::write(dir.path().join(FILE_NAME), &bytes).expect("write flipped");
let error = TailHighWater::read(dir.path(), "conv-1").expect_err("must refuse");
assert!(matches!(error, HighWaterError::ChecksumMismatch));
}
#[test]
fn another_familys_magic_is_refused() {
let dir = tempfile::tempdir().expect("temp dir");
sample("conv-1").write_atomic(dir.path()).expect("write");
let mut bytes = std::fs::read(dir.path().join(FILE_NAME)).expect("read raw");
bytes[0] ^= 0xFF;
let checksum = Sha256::digest(&bytes[..BOUND_LEN]);
bytes[BOUND_LEN..].copy_from_slice(&checksum);
std::fs::write(dir.path().join(FILE_NAME), &bytes).expect("write foreign magic");
let error = TailHighWater::read(dir.path(), "conv-1").expect_err("must refuse");
assert!(matches!(error, HighWaterError::WrongMagic));
}
#[test]
fn an_unsupported_revision_is_refused() {
let dir = tempfile::tempdir().expect("temp dir");
sample("conv-1").write_atomic(dir.path()).expect("write");
let mut bytes = std::fs::read(dir.path().join(FILE_NAME)).expect("read raw");
bytes[MAGIC.len()] = FORMAT_REVISION + 1;
let checksum = Sha256::digest(&bytes[..BOUND_LEN]);
bytes[BOUND_LEN..].copy_from_slice(&checksum);
std::fs::write(dir.path().join(FILE_NAME), &bytes).expect("write future revision");
let error = TailHighWater::read(dir.path(), "conv-1").expect_err("must refuse");
assert!(matches!(
error,
HighWaterError::UnsupportedRevision {
found: 2,
expected: 1
}
));
}
#[test]
fn a_valid_artifact_for_a_different_partition_is_refused() {
let dir = tempfile::tempdir().expect("temp dir");
sample("conv-1").write_atomic(dir.path()).expect("write");
let error = TailHighWater::read(dir.path(), "conv-2").expect_err("must refuse");
assert!(matches!(error, HighWaterError::WrongPartition));
}
#[test]
fn accessors_return_exactly_what_was_bound() {
let high_water = sample("conv-1");
assert_eq!(high_water.incarnation(), [7_u8; 32]);
assert_eq!(high_water.logical_head(), 42);
assert_eq!(high_water.physical_frontier(), 44);
assert_eq!(high_water.covering_root(), [9_u8; 32]);
assert_eq!(high_water.covering_leaf_count(), 43);
}
}