use super::SnapshotRegion;
pub const SNAPSHOT_MAGIC: [u8; 4] = *b"PRNS";
pub const SNAPSHOT_VERSION: u8 = 1;
pub const SNAPSHOT_HEADER_LEN: usize = SNAPSHOT_MAGIC.len() + 1 + 1 + 4;
pub const SNAPSHOT_CHECKSUM_LEN: usize = 4;
pub const SNAPSHOT_OVERHEAD_LEN: usize = SNAPSHOT_HEADER_LEN + SNAPSHOT_CHECKSUM_LEN;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SnapshotSealError {
BufferTooShort,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SnapshotOpenError {
Truncated,
BadMagic,
UnsupportedVersion { found: u8 },
WrongRegion { found: u8 },
ChecksumMismatch,
}
pub fn seal_snapshot(
region: SnapshotRegion,
payload: &[u8],
out: &mut [u8],
) -> Result<usize, SnapshotSealError> {
let Some(total_len) = SNAPSHOT_OVERHEAD_LEN.checked_add(payload.len()) else {
return Err(SnapshotSealError::BufferTooShort);
};
if out.len() < total_len {
return Err(SnapshotSealError::BufferTooShort);
}
out[SNAPSHOT_HEADER_LEN..SNAPSHOT_HEADER_LEN + payload.len()].copy_from_slice(payload);
seal_snapshot_in_place(region, payload.len(), out)
}
pub fn seal_snapshot_in_place(
region: SnapshotRegion,
payload_len: usize,
out: &mut [u8],
) -> Result<usize, SnapshotSealError> {
let Some(total_len) = SNAPSHOT_OVERHEAD_LEN.checked_add(payload_len) else {
return Err(SnapshotSealError::BufferTooShort);
};
if out.len() < total_len {
return Err(SnapshotSealError::BufferTooShort);
}
out[..SNAPSHOT_MAGIC.len()].copy_from_slice(&SNAPSHOT_MAGIC);
out[4] = SNAPSHOT_VERSION;
out[5] = region.tag();
out[6..SNAPSHOT_HEADER_LEN].copy_from_slice(&(payload_len as u32).to_le_bytes());
let checksum = crc32(&out[..SNAPSHOT_HEADER_LEN + payload_len]);
out[SNAPSHOT_HEADER_LEN + payload_len..total_len].copy_from_slice(&checksum.to_le_bytes());
Ok(total_len)
}
pub fn open_snapshot(region: SnapshotRegion, bytes: &[u8]) -> Result<&[u8], SnapshotOpenError> {
if bytes.len() < SNAPSHOT_OVERHEAD_LEN {
return Err(SnapshotOpenError::Truncated);
}
if bytes[..SNAPSHOT_MAGIC.len()] != SNAPSHOT_MAGIC {
return Err(SnapshotOpenError::BadMagic);
}
if bytes[4] != SNAPSHOT_VERSION {
return Err(SnapshotOpenError::UnsupportedVersion { found: bytes[4] });
}
if bytes[5] != region.tag() {
return Err(SnapshotOpenError::WrongRegion { found: bytes[5] });
}
let payload_len = u32::from_le_bytes([bytes[6], bytes[7], bytes[8], bytes[9]]) as usize;
let Some(total_len) = SNAPSHOT_OVERHEAD_LEN.checked_add(payload_len) else {
return Err(SnapshotOpenError::Truncated);
};
if bytes.len() < total_len {
return Err(SnapshotOpenError::Truncated);
}
let checksum_at = SNAPSHOT_HEADER_LEN + payload_len;
let found = u32::from_le_bytes([
bytes[checksum_at],
bytes[checksum_at + 1],
bytes[checksum_at + 2],
bytes[checksum_at + 3],
]);
if found != crc32(&bytes[..checksum_at]) {
return Err(SnapshotOpenError::ChecksumMismatch);
}
Ok(&bytes[SNAPSHOT_HEADER_LEN..checksum_at])
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SnapshotFingerprint([u8; SNAPSHOT_CHECKSUM_LEN]);
pub fn snapshot_fingerprint(sealed: &[u8]) -> Option<SnapshotFingerprint> {
if sealed.len() < SNAPSHOT_OVERHEAD_LEN {
return None;
}
let payload_len = u32::from_le_bytes([sealed[6], sealed[7], sealed[8], sealed[9]]) as usize;
let checksum_at = SNAPSHOT_HEADER_LEN.checked_add(payload_len)?;
let checksum = sealed.get(checksum_at..checksum_at + SNAPSHOT_CHECKSUM_LEN)?;
let mut fingerprint = [0u8; SNAPSHOT_CHECKSUM_LEN];
fingerprint.copy_from_slice(checksum);
Some(SnapshotFingerprint(fingerprint))
}
pub(crate) fn crc32(bytes: &[u8]) -> u32 {
#[cfg(feature = "std")]
return crc32fast::hash(bytes);
#[cfg(not(feature = "std"))]
return crc32_portable(bytes);
}
#[cfg(any(test, not(feature = "std")))]
fn crc32_portable(bytes: &[u8]) -> u32 {
let mut crc = 0xFFFF_FFFFu32;
for &byte in bytes {
crc ^= u32::from(byte);
for _ in 0..8 {
let low_bit_set = (crc & 1).wrapping_neg();
crc = (crc >> 1) ^ (0xEDB8_8320 & low_bit_set);
}
}
!crc
}
#[cfg(test)]
mod tests {
use super::*;
const REGION: SnapshotRegion = SnapshotRegion::Timebase;
fn sealed(payload: &[u8]) -> std::vec::Vec<u8> {
let mut out = std::vec![0u8; SNAPSHOT_OVERHEAD_LEN + payload.len()];
let len = seal_snapshot(REGION, payload, &mut out).unwrap();
out.truncate(len);
out
}
#[test]
fn a_sealed_snapshot_opens_to_its_payload() {
let bytes = sealed(b"the payload");
assert_eq!(open_snapshot(REGION, &bytes).unwrap(), b"the payload");
}
#[test]
fn tail_padding_after_the_checksum_is_ignored() {
let mut bytes = sealed(b"the payload");
bytes.extend_from_slice(&[0xFF; 32]);
assert_eq!(open_snapshot(REGION, &bytes).unwrap(), b"the payload");
}
#[test]
fn an_empty_payload_round_trips() {
assert_eq!(open_snapshot(REGION, &sealed(b"")).unwrap(), b"");
}
#[test]
fn a_short_seal_buffer_is_refused() {
let mut out = [0u8; SNAPSHOT_OVERHEAD_LEN];
assert_eq!(
seal_snapshot(REGION, b"x", &mut out),
Err(SnapshotSealError::BufferTooShort),
);
}
#[test]
fn identical_payloads_carry_identical_fingerprints_and_a_changed_payload_does_not() {
let first = snapshot_fingerprint(&sealed(b"the payload")).unwrap();
let second = snapshot_fingerprint(&sealed(b"the payload")).unwrap();
let changed = snapshot_fingerprint(&sealed(b"the payloae")).unwrap();
assert_eq!(first, second);
assert_ne!(first, changed);
}
#[test]
fn crc_matches_the_ieee_check_value() {
assert_eq!(crc32(b"123456789"), 0xCBF4_3926);
}
#[cfg(feature = "std")]
#[test]
fn accelerated_and_portable_crc_are_identical() {
let bytes = (0..=u8::MAX).collect::<std::vec::Vec<_>>();
assert_eq!(crc32(&bytes), crc32_portable(&bytes));
}
#[test]
fn a_truncated_snapshot_has_no_fingerprint() {
let bytes = sealed(b"the payload");
assert_eq!(snapshot_fingerprint(&bytes[..bytes.len() - 1]), None);
assert_eq!(snapshot_fingerprint(&[]), None);
}
#[test]
fn each_corruption_refuses_by_name() {
let bytes = sealed(b"the payload");
assert_eq!(
open_snapshot(REGION, &bytes[..SNAPSHOT_OVERHEAD_LEN - 1]),
Err(SnapshotOpenError::Truncated),
);
let mut wrong_magic = bytes.clone();
wrong_magic[0] ^= 0xFF;
assert_eq!(
open_snapshot(REGION, &wrong_magic),
Err(SnapshotOpenError::BadMagic),
);
let mut future_version = bytes.clone();
future_version[4] = SNAPSHOT_VERSION + 1;
assert_eq!(
open_snapshot(REGION, &future_version),
Err(SnapshotOpenError::UnsupportedVersion {
found: SNAPSHOT_VERSION + 1
}),
);
let mut foreign_region = bytes.clone();
foreign_region[5] = 0xEE;
assert_eq!(
open_snapshot(REGION, &foreign_region),
Err(SnapshotOpenError::WrongRegion { found: 0xEE }),
);
let mut bit_rot = bytes.clone();
bit_rot[SNAPSHOT_HEADER_LEN] ^= 0x01;
assert_eq!(
open_snapshot(REGION, &bit_rot),
Err(SnapshotOpenError::ChecksumMismatch),
);
let mut lying_length = bytes;
lying_length[6..10].copy_from_slice(&u32::MAX.to_le_bytes());
assert_eq!(
open_snapshot(REGION, &lying_length),
Err(SnapshotOpenError::Truncated),
);
}
}