use crate::error::{Result, WalError};
pub const PREAMBLE_SIZE: usize = 16;
pub const PREAMBLE_VERSION: u16 = 1;
pub const CIPHER_AES_256_GCM: u8 = 0;
pub const WAL_PREAMBLE_MAGIC: [u8; 4] = *b"WALP";
pub const SEG_PREAMBLE_MAGIC: [u8; 4] = *b"SEGP";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SegmentPreamble {
pub magic: [u8; 4],
pub version: u16,
pub cipher_alg: u8,
pub kid: u8,
pub epoch: [u8; 4],
}
impl SegmentPreamble {
pub fn new_wal(epoch: [u8; 4]) -> Self {
Self {
magic: WAL_PREAMBLE_MAGIC,
version: PREAMBLE_VERSION,
cipher_alg: CIPHER_AES_256_GCM,
kid: 0,
epoch,
}
}
pub fn new_seg(epoch: [u8; 4]) -> Self {
Self {
magic: SEG_PREAMBLE_MAGIC,
version: PREAMBLE_VERSION,
cipher_alg: CIPHER_AES_256_GCM,
kid: 0,
epoch,
}
}
pub fn to_bytes(&self) -> [u8; PREAMBLE_SIZE] {
let mut buf = [0u8; PREAMBLE_SIZE];
buf[0..4].copy_from_slice(&self.magic);
buf[4..6].copy_from_slice(&self.version.to_le_bytes());
buf[6] = self.cipher_alg;
buf[7] = self.kid;
buf[8..12].copy_from_slice(&self.epoch);
buf
}
pub fn from_bytes(buf: &[u8; PREAMBLE_SIZE], expected_magic: &[u8; 4]) -> Result<Self> {
let magic: [u8; 4] = [buf[0], buf[1], buf[2], buf[3]];
if &magic != expected_magic {
return Err(WalError::EncryptionError {
detail: format!(
"preamble magic mismatch: expected {:?}, got {:?}",
expected_magic, magic
),
});
}
let version = u16::from_le_bytes([buf[4], buf[5]]);
if version != PREAMBLE_VERSION {
return Err(WalError::UnsupportedVersion {
version,
supported: PREAMBLE_VERSION,
});
}
let cipher_alg = buf[6];
let kid = buf[7];
let epoch: [u8; 4] = [buf[8], buf[9], buf[10], buf[11]];
Ok(Self {
magic,
version,
cipher_alg,
kid,
epoch,
})
}
pub fn epoch(&self) -> &[u8; 4] {
&self.epoch
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wal_preamble_roundtrip() {
let epoch = [0xAA, 0xBB, 0xCC, 0xDD];
let p = SegmentPreamble::new_wal(epoch);
let bytes = p.to_bytes();
let parsed = SegmentPreamble::from_bytes(&bytes, &WAL_PREAMBLE_MAGIC).unwrap();
assert_eq!(p, parsed);
assert_eq!(parsed.epoch, epoch);
assert_eq!(parsed.cipher_alg, CIPHER_AES_256_GCM);
assert_eq!(parsed.kid, 0);
assert_eq!(parsed.version, PREAMBLE_VERSION);
}
#[test]
fn seg_preamble_roundtrip() {
let epoch = [0x11, 0x22, 0x33, 0x44];
let p = SegmentPreamble::new_seg(epoch);
let bytes = p.to_bytes();
let parsed = SegmentPreamble::from_bytes(&bytes, &SEG_PREAMBLE_MAGIC).unwrap();
assert_eq!(p, parsed);
}
#[test]
fn wrong_magic_rejected() {
let p = SegmentPreamble::new_wal([0u8; 4]);
let bytes = p.to_bytes();
assert!(SegmentPreamble::from_bytes(&bytes, &SEG_PREAMBLE_MAGIC).is_err());
}
#[test]
fn unsupported_version_rejected() {
let p = SegmentPreamble::new_wal([0u8; 4]);
let mut bytes = p.to_bytes();
bytes[4] = 2;
bytes[5] = 0;
assert!(matches!(
SegmentPreamble::from_bytes(&bytes, &WAL_PREAMBLE_MAGIC),
Err(WalError::UnsupportedVersion { version: 2, .. })
));
}
#[test]
fn kid_and_cipher_alg_roundtrip() {
let p = SegmentPreamble {
magic: WAL_PREAMBLE_MAGIC,
version: PREAMBLE_VERSION,
cipher_alg: CIPHER_AES_256_GCM,
kid: 3,
epoch: [1, 2, 3, 4],
};
let bytes = p.to_bytes();
let parsed = SegmentPreamble::from_bytes(&bytes, &WAL_PREAMBLE_MAGIC).unwrap();
assert_eq!(parsed.kid, 3);
assert_eq!(parsed.epoch, [1, 2, 3, 4]);
}
#[test]
fn reserved_bytes_are_zero() {
let p = SegmentPreamble::new_wal([0xFF; 4]);
let bytes = p.to_bytes();
assert_eq!(&bytes[12..16], &[0u8, 0, 0, 0]);
}
}