1use crate::error::{Result, WalError};
33
34pub const PREAMBLE_SIZE: usize = 16;
36
37pub const PREAMBLE_VERSION: u16 = 1;
39
40pub const CIPHER_AES_256_GCM: u8 = 0;
42
43pub const WAL_PREAMBLE_MAGIC: [u8; 4] = *b"WALP";
45
46pub const SEG_PREAMBLE_MAGIC: [u8; 4] = *b"SEGP";
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct SegmentPreamble {
52 pub magic: [u8; 4],
54 pub version: u16,
56 pub cipher_alg: u8,
58 pub kid: u8,
60 pub epoch: [u8; 4],
63 }
65
66impl SegmentPreamble {
67 pub fn new_wal(epoch: [u8; 4]) -> Self {
69 Self {
70 magic: WAL_PREAMBLE_MAGIC,
71 version: PREAMBLE_VERSION,
72 cipher_alg: CIPHER_AES_256_GCM,
73 kid: 0,
74 epoch,
75 }
76 }
77
78 pub fn new_seg(epoch: [u8; 4]) -> Self {
80 Self {
81 magic: SEG_PREAMBLE_MAGIC,
82 version: PREAMBLE_VERSION,
83 cipher_alg: CIPHER_AES_256_GCM,
84 kid: 0,
85 epoch,
86 }
87 }
88
89 pub fn to_bytes(&self) -> [u8; PREAMBLE_SIZE] {
91 let mut buf = [0u8; PREAMBLE_SIZE];
92 buf[0..4].copy_from_slice(&self.magic);
93 buf[4..6].copy_from_slice(&self.version.to_le_bytes());
94 buf[6] = self.cipher_alg;
95 buf[7] = self.kid;
96 buf[8..12].copy_from_slice(&self.epoch);
97 buf
99 }
100
101 pub fn from_bytes(buf: &[u8; PREAMBLE_SIZE], expected_magic: &[u8; 4]) -> Result<Self> {
106 let magic: [u8; 4] = [buf[0], buf[1], buf[2], buf[3]];
107
108 if &magic != expected_magic {
109 return Err(WalError::EncryptionError {
110 detail: format!(
111 "preamble magic mismatch: expected {:?}, got {:?}",
112 expected_magic, magic
113 ),
114 });
115 }
116
117 let version = u16::from_le_bytes([buf[4], buf[5]]);
118 if version != PREAMBLE_VERSION {
119 return Err(WalError::UnsupportedVersion {
120 version,
121 supported: PREAMBLE_VERSION,
122 });
123 }
124
125 let cipher_alg = buf[6];
126 let kid = buf[7];
127 let epoch: [u8; 4] = [buf[8], buf[9], buf[10], buf[11]];
128
129 Ok(Self {
130 magic,
131 version,
132 cipher_alg,
133 kid,
134 epoch,
135 })
136 }
137
138 pub fn epoch(&self) -> &[u8; 4] {
140 &self.epoch
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn wal_preamble_roundtrip() {
150 let epoch = [0xAA, 0xBB, 0xCC, 0xDD];
151 let p = SegmentPreamble::new_wal(epoch);
152 let bytes = p.to_bytes();
153 let parsed = SegmentPreamble::from_bytes(&bytes, &WAL_PREAMBLE_MAGIC).unwrap();
154 assert_eq!(p, parsed);
155 assert_eq!(parsed.epoch, epoch);
156 assert_eq!(parsed.cipher_alg, CIPHER_AES_256_GCM);
157 assert_eq!(parsed.kid, 0);
158 assert_eq!(parsed.version, PREAMBLE_VERSION);
159 }
160
161 #[test]
162 fn seg_preamble_roundtrip() {
163 let epoch = [0x11, 0x22, 0x33, 0x44];
164 let p = SegmentPreamble::new_seg(epoch);
165 let bytes = p.to_bytes();
166 let parsed = SegmentPreamble::from_bytes(&bytes, &SEG_PREAMBLE_MAGIC).unwrap();
167 assert_eq!(p, parsed);
168 }
169
170 #[test]
171 fn wrong_magic_rejected() {
172 let p = SegmentPreamble::new_wal([0u8; 4]);
173 let bytes = p.to_bytes();
174 assert!(SegmentPreamble::from_bytes(&bytes, &SEG_PREAMBLE_MAGIC).is_err());
176 }
177
178 #[test]
179 fn unsupported_version_rejected() {
180 let p = SegmentPreamble::new_wal([0u8; 4]);
181 let mut bytes = p.to_bytes();
182 bytes[4] = 2;
184 bytes[5] = 0;
185 assert!(matches!(
186 SegmentPreamble::from_bytes(&bytes, &WAL_PREAMBLE_MAGIC),
187 Err(WalError::UnsupportedVersion { version: 2, .. })
188 ));
189 }
190
191 #[test]
192 fn kid_and_cipher_alg_roundtrip() {
193 let p = SegmentPreamble {
195 magic: WAL_PREAMBLE_MAGIC,
196 version: PREAMBLE_VERSION,
197 cipher_alg: CIPHER_AES_256_GCM,
198 kid: 3,
199 epoch: [1, 2, 3, 4],
200 };
201 let bytes = p.to_bytes();
202 let parsed = SegmentPreamble::from_bytes(&bytes, &WAL_PREAMBLE_MAGIC).unwrap();
203 assert_eq!(parsed.kid, 3);
204 assert_eq!(parsed.epoch, [1, 2, 3, 4]);
205 }
206
207 #[test]
208 fn reserved_bytes_are_zero() {
209 let p = SegmentPreamble::new_wal([0xFF; 4]);
210 let bytes = p.to_bytes();
211 assert_eq!(&bytes[12..16], &[0u8, 0, 0, 0]);
212 }
213}