1use crate::{Error, Result};
5use alloc::vec::Vec;
6
7const RECORD_MAGIC: [u8; 4] = *b"CUSR";
8pub const RECORD_HEADER_LEN: usize = 45;
9const RECORD_DIGEST_OFFSET: usize = 13;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13#[repr(u8)]
14pub enum RecordKind {
15 Manifest = 0,
16 CopperList = 1,
17 KeyFrame = 2,
18 StructuredLog = 3,
19 Lifecycle = 4,
20 Gap = 5,
21 RecoveryPoint = 6,
22}
23
24impl TryFrom<u8> for RecordKind {
25 type Error = Error;
26
27 fn try_from(value: u8) -> Result<Self> {
28 match value {
29 0 => Ok(Self::Manifest),
30 1 => Ok(Self::CopperList),
31 2 => Ok(Self::KeyFrame),
32 3 => Ok(Self::StructuredLog),
33 4 => Ok(Self::Lifecycle),
34 5 => Ok(Self::Gap),
35 6 => Ok(Self::RecoveryPoint),
36 _ => Err(Error::UnknownRecordKind(value)),
37 }
38 }
39}
40
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub struct DecodedRecord<'a> {
44 pub kind: RecordKind,
45 pub object_id: u64,
46 pub digest: [u8; 32],
48 pub payload: &'a [u8],
49}
50
51pub fn encode_record(kind: RecordKind, object_id: u64, payload: &[u8]) -> Result<Vec<u8>> {
53 let payload_len = u64::try_from(payload.len())
54 .map_err(|_| Error::InvalidConfig("record payload length exceeds u64"))?;
55 let capacity = RECORD_HEADER_LEN
56 .checked_add(payload.len())
57 .ok_or(Error::InvalidConfig("record length overflow"))?;
58 let mut record = Vec::with_capacity(capacity);
59 record.extend_from_slice(&RECORD_MAGIC);
60 record.push(kind as u8);
61 record.extend_from_slice(&object_id.to_be_bytes());
62 record.extend_from_slice(&[0_u8; 32]);
63 record.extend_from_slice(payload);
64
65 let digest = record_digest(kind, object_id, payload_len, payload);
66 record[RECORD_DIGEST_OFFSET..RECORD_HEADER_LEN].copy_from_slice(digest.as_bytes());
67 Ok(record)
68}
69
70pub(crate) fn encode_record_header(
73 kind: RecordKind,
74 object_id: u64,
75 payload: &[u8],
76 header: &mut [u8],
77) -> Result<()> {
78 if header.len() < RECORD_HEADER_LEN {
79 return Err(Error::BufferTooSmall {
80 needed: RECORD_HEADER_LEN,
81 available: header.len(),
82 });
83 }
84 let payload_len = u64::try_from(payload.len())
85 .map_err(|_| Error::InvalidConfig("record payload length exceeds u64"))?;
86 let header = &mut header[..RECORD_HEADER_LEN];
87 header.fill(0);
88 header[..4].copy_from_slice(&RECORD_MAGIC);
89 header[4] = kind as u8;
90 header[5..13].copy_from_slice(&object_id.to_be_bytes());
91 let digest = record_digest(kind, object_id, payload_len, payload);
92 header[RECORD_DIGEST_OFFSET..RECORD_HEADER_LEN].copy_from_slice(digest.as_bytes());
93 Ok(())
94}
95
96pub fn decode_record(record: &[u8]) -> Result<DecodedRecord<'_>> {
101 if record.len() < RECORD_HEADER_LEN {
102 return Err(Error::TruncatedRecord);
103 }
104 if record[..4] != RECORD_MAGIC {
105 return Err(Error::InvalidMagic);
106 }
107 let kind = RecordKind::try_from(record[4])?;
108 let object_id = u64::from_be_bytes(record[5..13].try_into().unwrap());
109 let payload = &record[RECORD_HEADER_LEN..];
110 let payload_len = u64::try_from(payload.len())
111 .map_err(|_| Error::InvalidConfig("record payload length exceeds u64"))?;
112 let expected_digest = record_digest(kind, object_id, payload_len, payload);
113 if record[RECORD_DIGEST_OFFSET..RECORD_HEADER_LEN] != expected_digest.as_bytes()[..] {
114 return Err(Error::RecordDigestMismatch);
115 }
116 Ok(DecodedRecord {
117 kind,
118 object_id,
119 digest: *expected_digest.as_bytes(),
120 payload,
121 })
122}
123
124fn record_digest(
125 kind: RecordKind,
126 object_id: u64,
127 payload_len: u64,
128 payload: &[u8],
129) -> blake3::Hash {
130 let mut hasher = blake3::Hasher::new();
131 hasher.update(&[kind as u8]);
132 hasher.update(&object_id.to_be_bytes());
133 hasher.update(&payload_len.to_be_bytes());
134 hasher.update(payload);
135 hasher.finalize()
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn packed_record_header_contains_only_framing_fields() {
144 let record = encode_record(RecordKind::CopperList, 42, &[42]).unwrap();
145 assert_eq!(record.len(), 46);
146 assert_eq!(&record[..5], b"CUSR\x01");
147 assert_eq!(&record[5..13], &42_u64.to_be_bytes());
148 assert_eq!(record[45], 42);
149 assert_eq!(decode_record(&record).unwrap().payload, &[42]);
150 let mut header = [0xaa; RECORD_HEADER_LEN];
151 encode_record_header(RecordKind::CopperList, 42, &[42], &mut header).unwrap();
152 assert_eq!(header, record[..RECORD_HEADER_LEN]);
153 }
154
155 #[test]
156 fn record_extent_defines_payload_length_without_changing_digest_binding() {
157 for kind in [
158 RecordKind::Manifest,
159 RecordKind::CopperList,
160 RecordKind::KeyFrame,
161 RecordKind::StructuredLog,
162 RecordKind::Lifecycle,
163 RecordKind::Gap,
164 RecordKind::RecoveryPoint,
165 ] {
166 for object_id in [0, 42, u64::MAX] {
167 for payload_len in [0, 1, 95, 96, 4096] {
168 let payload = alloc::vec![0xa5; payload_len];
169 let encoded = encode_record(kind, object_id, &payload).unwrap();
170 assert_eq!(encoded.len(), 45 + payload_len);
171 let decoded = decode_record(&encoded).unwrap();
172 assert_eq!(decoded.kind, kind);
173 assert_eq!(decoded.object_id, object_id);
174 assert_eq!(decoded.payload, payload);
175 assert_eq!(decoded.payload.as_ptr(), encoded[45..].as_ptr());
176
177 let mut preimage = alloc::vec![kind as u8];
179 preimage.extend_from_slice(&object_id.to_be_bytes());
180 preimage.extend_from_slice(&(payload_len as u64).to_be_bytes());
181 preimage.extend_from_slice(&payload);
182 let digest = blake3::hash(&preimage);
183 assert_eq!(decoded.digest, *digest.as_bytes());
184 assert_eq!(&encoded[13..45], digest.as_bytes());
185
186 let mut header = [0xaa; RECORD_HEADER_LEN + 1];
187 encode_record_header(kind, object_id, &payload, &mut header).unwrap();
188 assert_eq!(&header[..RECORD_HEADER_LEN], &encoded[..45]);
189 assert_eq!(header[RECORD_HEADER_LEN], 0xaa);
190 }
191 }
192 }
193 }
194
195 #[test]
196 fn record_rejects_truncation_appended_bytes_and_corruption() {
197 for payload in [b"".as_slice(), b"state"] {
198 let encoded = encode_record(RecordKind::KeyFrame, 42, payload).unwrap();
199 for end in 0..encoded.len() {
200 let expected = if end < RECORD_HEADER_LEN {
201 Error::TruncatedRecord
202 } else {
203 Error::RecordDigestMismatch
204 };
205 assert_eq!(decode_record(&encoded[..end]), Err(expected));
206 }
207 for extra in [b"\0".as_slice(), b"state", encoded.as_slice()] {
208 let mut extended = encoded.clone();
209 extended.extend_from_slice(extra);
210 assert_eq!(decode_record(&extended), Err(Error::RecordDigestMismatch));
211 }
212 for offset in 0..encoded.len() {
213 let mut corrupted = encoded.clone();
214 corrupted[offset] ^= 1; let expected = if offset < 4 {
216 Error::InvalidMagic
217 } else {
218 Error::RecordDigestMismatch
219 };
220 assert_eq!(decode_record(&corrupted), Err(expected));
221 }
222 let mut invalid_kind = encoded;
223 invalid_kind[4] = u8::MAX;
224 assert_eq!(
225 decode_record(&invalid_kind),
226 Err(Error::UnknownRecordKind(u8::MAX))
227 );
228 }
229 }
230
231 #[test]
232 fn record_header_rejects_short_buffers_without_writing() {
233 for len in 0..RECORD_HEADER_LEN {
234 let mut header = [0xaa; RECORD_HEADER_LEN];
235 assert_eq!(
236 encode_record_header(RecordKind::CopperList, 42, b"state", &mut header[..len]),
237 Err(Error::BufferTooSmall {
238 needed: RECORD_HEADER_LEN,
239 available: len,
240 })
241 );
242 assert_eq!(header, [0xaa; RECORD_HEADER_LEN]);
243 }
244 }
245}