1use crate::error::{Error, Result};
12use crate::integrity::crc32c;
13use crate::limits::Limits;
14
15pub const RECORD_HEADER_LEN: usize = 8;
17pub const RECORD_TRAILER_LEN: usize = 4;
19pub const RECORD_OVERHEAD: usize = RECORD_HEADER_LEN + RECORD_TRAILER_LEN;
21
22pub const FLAG_OPTIONAL: u8 = 0x01;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27#[repr(u8)]
28pub enum RecordTag {
29 Universe = 0x01,
31 Format = 0x02,
33 Object = 0x10,
35 Graph = 0x20,
37 Model = 0x30,
39 EntropyChannel = 0x40,
41 Residual = 0x50,
43 Checkpoint = 0x60,
45 Index = 0x70,
47 ExternalRef = 0x80,
49 Integrity = 0xF0,
51 Trailer = 0xFF,
53}
54
55impl RecordTag {
56 pub const fn from_u8(b: u8) -> Option<RecordTag> {
58 match b {
59 0x01 => Some(RecordTag::Universe),
60 0x02 => Some(RecordTag::Format),
61 0x10 => Some(RecordTag::Object),
62 0x20 => Some(RecordTag::Graph),
63 0x30 => Some(RecordTag::Model),
64 0x40 => Some(RecordTag::EntropyChannel),
65 0x50 => Some(RecordTag::Residual),
66 0x60 => Some(RecordTag::Checkpoint),
67 0x70 => Some(RecordTag::Index),
68 0x80 => Some(RecordTag::ExternalRef),
69 0xF0 => Some(RecordTag::Integrity),
70 0xFF => Some(RecordTag::Trailer),
71 _ => None,
72 }
73 }
74
75 pub const fn name(self) -> &'static str {
77 match self {
78 RecordTag::Universe => "UNIVERSE",
79 RecordTag::Format => "FORMAT",
80 RecordTag::Object => "OBJECT",
81 RecordTag::Graph => "GRAPH",
82 RecordTag::Model => "MODEL",
83 RecordTag::EntropyChannel => "ENTROPY_CHANNEL",
84 RecordTag::Residual => "RESIDUAL",
85 RecordTag::Checkpoint => "CHECKPOINT",
86 RecordTag::Index => "INDEX",
87 RecordTag::ExternalRef => "EXTERNAL_REF",
88 RecordTag::Integrity => "INTEGRITY",
89 RecordTag::Trailer => "TRAILER",
90 }
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct Record {
97 pub tag: u8,
99 pub flags: u8,
101 pub payload: Vec<u8>,
103}
104
105impl Record {
106 pub fn new(tag: u8, payload: Vec<u8>) -> Self {
108 Record {
109 tag,
110 flags: 0,
111 payload,
112 }
113 }
114
115 pub fn known(tag: RecordTag, payload: Vec<u8>) -> Self {
117 Record {
118 tag: tag as u8,
119 flags: 0,
120 payload,
121 }
122 }
123
124 pub fn is_optional(&self) -> bool {
126 self.flags & FLAG_OPTIONAL != 0
127 }
128}
129
130pub fn write_record(out: &mut Vec<u8>, tag: u8, flags: u8, payload: &[u8]) -> Result<()> {
132 let len = u32::try_from(payload.len())
133 .map_err(|_| Error::resource_limit("record payload exceeds 4 GiB"))?;
134 let start = out.len();
135 out.push(tag);
136 out.push(flags);
137 out.extend_from_slice(&0u16.to_le_bytes());
138 out.extend_from_slice(&len.to_le_bytes());
139 out.extend_from_slice(payload);
140 let crc = crc32c(&out[start..]);
141 out.extend_from_slice(&crc.to_le_bytes());
142 Ok(())
143}
144
145pub struct RecordReader<'a> {
147 data: &'a [u8],
148 pos: usize,
149 limits: Limits,
150 count: u32,
151}
152
153impl<'a> RecordReader<'a> {
154 pub fn new(data: &'a [u8], offset: usize, limits: Limits) -> Self {
156 RecordReader {
157 data,
158 pos: offset,
159 limits,
160 count: 0,
161 }
162 }
163
164 pub fn position(&self) -> usize {
166 self.pos
167 }
168
169 pub fn records_read(&self) -> u32 {
171 self.count
172 }
173
174 pub fn next_record(&mut self) -> Result<Option<Record>> {
176 if self.pos == self.data.len() {
177 return Ok(None);
178 }
179 if self.count >= self.limits.max_record_count {
180 return Err(Error::resource_limit("record count limit exceeded"));
181 }
182 let remaining = self.data.len() - self.pos;
183 if remaining < RECORD_OVERHEAD {
184 return Err(Error::invalid_container(format!(
185 "truncated record header: {remaining} bytes remain"
186 )));
187 }
188 let hdr = &self.data[self.pos..self.pos + RECORD_HEADER_LEN];
189 let tag = hdr[0];
190 let flags = hdr[1];
191 let reserved = u16::from_le_bytes([hdr[2], hdr[3]]);
192 if reserved != 0 {
193 return Err(Error::invalid_container(
194 "record reserved field must be zero",
195 ));
196 }
197 let len = u32::from_le_bytes([hdr[4], hdr[5], hdr[6], hdr[7]]);
198 if len > self.limits.max_record_len {
199 return Err(Error::resource_limit(format!(
200 "record payload length {len} exceeds limit {}",
201 self.limits.max_record_len
202 )));
203 }
204 let total = RECORD_HEADER_LEN
205 .checked_add(len as usize)
206 .and_then(|n| n.checked_add(RECORD_TRAILER_LEN))
207 .ok_or_else(|| Error::invalid_container("record length overflow"))?;
208 if total > remaining {
209 return Err(Error::invalid_container(format!(
210 "truncated record payload: need {total}, have {remaining}"
211 )));
212 }
213 let body = &self.data[self.pos..self.pos + RECORD_HEADER_LEN + len as usize];
214 let want_crc = u32::from_le_bytes([
215 self.data[self.pos + RECORD_HEADER_LEN + len as usize],
216 self.data[self.pos + RECORD_HEADER_LEN + len as usize + 1],
217 self.data[self.pos + RECORD_HEADER_LEN + len as usize + 2],
218 self.data[self.pos + RECORD_HEADER_LEN + len as usize + 3],
219 ]);
220 let got_crc = crc32c(body);
221 if want_crc != got_crc {
222 return Err(Error::invalid_container(format!(
223 "record CRC32C mismatch at offset {}: declared {want_crc:#010x}, computed {got_crc:#010x}",
224 self.pos
225 )));
226 }
227 let payload = self.data
228 [self.pos + RECORD_HEADER_LEN..self.pos + RECORD_HEADER_LEN + len as usize]
229 .to_vec();
230 self.pos += total;
231 self.count += 1;
232 Ok(Some(Record {
233 tag,
234 flags,
235 payload,
236 }))
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn write_read_roundtrip() {
246 let mut buf = Vec::new();
247 write_record(&mut buf, RecordTag::Universe as u8, 0, b"hello").unwrap();
248 write_record(&mut buf, RecordTag::Object as u8, 0, &[1, 2, 3]).unwrap();
249 let mut r = RecordReader::new(&buf, 0, Limits::DEFAULT);
250 let a = r.next_record().unwrap().unwrap();
251 assert_eq!(a.tag, RecordTag::Universe as u8);
252 assert_eq!(a.payload, b"hello");
253 let b = r.next_record().unwrap().unwrap();
254 assert_eq!(b.tag, RecordTag::Object as u8);
255 assert_eq!(b.payload, vec![1, 2, 3]);
256 assert!(r.next_record().unwrap().is_none());
257 }
258
259 #[test]
260 fn detects_payload_corruption() {
261 let mut buf = Vec::new();
262 write_record(&mut buf, 0x10, 0, b"abcdef").unwrap();
263 buf[9] ^= 0xFF; let mut r = RecordReader::new(&buf, 0, Limits::DEFAULT);
265 let e = r.next_record().unwrap_err();
266 assert_eq!(e.class(), crate::ErrorClass::InvalidContainer);
267 }
268
269 #[test]
270 fn detects_truncation() {
271 let mut buf = Vec::new();
272 write_record(&mut buf, 0x10, 0, b"abcdef").unwrap();
273 buf.truncate(buf.len() - 2);
274 let mut r = RecordReader::new(&buf, 0, Limits::DEFAULT);
275 let e = r.next_record().unwrap_err();
276 assert_eq!(e.class(), crate::ErrorClass::InvalidContainer);
277 }
278
279 #[test]
280 fn rejects_nonzero_reserved() {
281 let mut buf = Vec::new();
282 write_record(&mut buf, 0x10, 0, b"x").unwrap();
283 buf[2] = 1; let mut r = RecordReader::new(&buf, 0, Limits::DEFAULT);
285 let e = r.next_record().unwrap_err();
286 assert_eq!(e.class(), crate::ErrorClass::InvalidContainer);
287 }
288
289 #[test]
290 fn enforces_record_length_limit() {
291 let mut buf = Vec::new();
292 write_record(&mut buf, 0x10, 0, &[0u8; 100]).unwrap();
293 let limits = Limits {
294 max_record_len: 10,
295 ..Limits::DEFAULT
296 };
297 let mut r = RecordReader::new(&buf, 0, limits);
298 let e = r.next_record().unwrap_err();
299 assert_eq!(e.class(), crate::ErrorClass::ResourceLimit);
300 }
301}