1use crate::error::{Error, Result};
36
37pub const CONTAINER_MAGIC: &[u8; 4] = b"VRTC";
39pub const CONTAINER_VERSION: u8 = 1;
41pub const CONTAINER_HEADER_LEN: usize = 32;
43const ALIGN: usize = 8;
44const INDEX_ENTRY_LEN: usize = 16;
45
46#[inline]
47fn align_up(x: usize, a: usize) -> usize {
48 (x + a - 1) & !(a - 1)
49}
50
51#[derive(Default)]
66pub struct ContainerWriter {
67 buf: Vec<u8>,
68 index: Vec<(u64, u64)>,
70}
71
72impl ContainerWriter {
73 pub fn new() -> ContainerWriter {
74 ContainerWriter {
75 buf: vec![0; CONTAINER_HEADER_LEN], index: Vec::new(),
77 }
78 }
79
80 pub fn add(&mut self, message: &[u8]) -> &mut Self {
83 let pad = align_up(self.buf.len(), ALIGN) - self.buf.len();
84 self.buf.resize(self.buf.len() + pad, 0);
85 let offset = self.buf.len() as u64;
86 self.buf.extend_from_slice(message);
87 self.index.push((offset, message.len() as u64));
88 self
89 }
90
91 pub fn len(&self) -> usize {
93 self.index.len()
94 }
95
96 pub fn is_empty(&self) -> bool {
97 self.index.is_empty()
98 }
99
100 pub fn finish(mut self) -> Vec<u8> {
103 let pad = align_up(self.buf.len(), ALIGN) - self.buf.len();
104 self.buf.resize(self.buf.len() + pad, 0);
105 let index_offset = self.buf.len() as u64;
106 for (off, len) in &self.index {
107 self.buf.extend_from_slice(&off.to_le_bytes());
108 self.buf.extend_from_slice(&len.to_le_bytes());
109 }
110 let file_len = self.buf.len() as u64;
111
112 self.buf[0..4].copy_from_slice(CONTAINER_MAGIC);
113 self.buf[4] = CONTAINER_VERSION;
114 self.buf[8..12].copy_from_slice(&(self.index.len() as u32).to_le_bytes());
116 self.buf[16..24].copy_from_slice(&index_offset.to_le_bytes());
118 self.buf[24..32].copy_from_slice(&file_len.to_le_bytes());
119 self.buf
120 }
121}
122
123#[derive(Clone, Debug)]
127pub struct Container<'a> {
128 buf: &'a [u8],
129 index_offset: usize,
130 count: usize,
131}
132
133impl<'a> Container<'a> {
134 pub fn parse(buf: &'a [u8]) -> Result<Container<'a>> {
138 if buf.len() < CONTAINER_HEADER_LEN {
139 return Err(Error::Truncated);
140 }
141 if &buf[0..4] != CONTAINER_MAGIC {
142 return Err(Error::BadContainer("bad magic"));
143 }
144 if buf[4] != CONTAINER_VERSION {
145 return Err(Error::BadContainer("unsupported container version"));
146 }
147 if buf[5] != 0 || u16::from_le_bytes(buf[6..8].try_into().unwrap()) != 0 {
148 return Err(Error::BadContainer("nonzero reserved header field"));
149 }
150 let count = u32::from_le_bytes(buf[8..12].try_into().unwrap()) as usize;
151 let index_offset = u64::from_le_bytes(buf[16..24].try_into().unwrap());
152 let file_len = u64::from_le_bytes(buf[24..32].try_into().unwrap());
153 if file_len as usize != buf.len() {
154 return Err(Error::BadContainer("file length mismatch"));
155 }
156 let index_offset = usize::try_from(index_offset)
157 .map_err(|_| Error::BadContainer("index offset overflow"))?;
158 if index_offset % ALIGN != 0 || index_offset < CONTAINER_HEADER_LEN {
159 return Err(Error::BadContainer("misaligned index offset"));
160 }
161 let index_bytes = count
163 .checked_mul(INDEX_ENTRY_LEN)
164 .ok_or(Error::BadContainer("index size overflow"))?;
165 let index_end = index_offset
166 .checked_add(index_bytes)
167 .ok_or(Error::BadContainer("index end overflow"))?;
168 if index_end > buf.len() {
169 return Err(Error::BadContainer("index out of bounds"));
170 }
171 let container = Container {
172 buf,
173 index_offset,
174 count,
175 };
176 for i in 0..count {
178 let (off, len) = container.raw_entry(i);
179 let off =
180 usize::try_from(off).map_err(|_| Error::BadContainer("record offset overflow"))?;
181 let len =
182 usize::try_from(len).map_err(|_| Error::BadContainer("record length overflow"))?;
183 if off % ALIGN != 0 {
184 return Err(Error::BadContainer("misaligned record"));
185 }
186 let end = off
187 .checked_add(len)
188 .ok_or(Error::BadContainer("record extent overflow"))?;
189 if off < CONTAINER_HEADER_LEN || end > index_offset {
191 return Err(Error::BadContainer("record outside record region"));
192 }
193 }
194 Ok(container)
195 }
196
197 #[inline]
198 fn raw_entry(&self, i: usize) -> (u64, u64) {
199 let base = self.index_offset + i * INDEX_ENTRY_LEN;
200 let off = u64::from_le_bytes(self.buf[base..base + 8].try_into().unwrap());
201 let len = u64::from_le_bytes(self.buf[base + 8..base + 16].try_into().unwrap());
202 (off, len)
203 }
204
205 pub fn len(&self) -> usize {
207 self.count
208 }
209
210 pub fn is_empty(&self) -> bool {
211 self.count == 0
212 }
213
214 pub fn get(&self, i: usize) -> Result<&'a [u8]> {
217 if i >= self.count {
218 return Err(Error::IndexOutOfBounds);
219 }
220 let (off, len) = self.raw_entry(i);
221 Ok(&self.buf[off as usize..(off + len) as usize])
223 }
224
225 pub fn iter(&self) -> impl Iterator<Item = &'a [u8]> + '_ {
227 (0..self.count).map(move |i| self.get(i).expect("index validated in parse"))
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn round_trips_messages() {
237 let msgs: Vec<Vec<u8>> = vec![
238 b"a".to_vec(),
239 b"".to_vec(),
240 (0..100u8).collect(),
241 b"the last one".to_vec(),
242 ];
243 let mut w = ContainerWriter::new();
244 for m in &msgs {
245 w.add(m);
246 }
247 assert_eq!(w.len(), 4);
248 let file = w.finish();
249
250 let c = Container::parse(&file).unwrap();
251 assert_eq!(c.len(), 4);
252 for (i, m) in msgs.iter().enumerate() {
253 assert_eq!(c.get(i).unwrap(), &m[..]);
254 }
255 let collected: Vec<&[u8]> = c.iter().collect();
256 assert_eq!(collected.len(), 4);
257 assert!(c.get(4).is_err());
258 }
259
260 #[test]
261 fn records_are_eight_byte_aligned() {
262 let mut w = ContainerWriter::new();
263 w.add(b"odd-length-7").add(b"x"); let file = w.finish();
265 let c = Container::parse(&file).unwrap();
266 for i in 0..c.len() {
267 let (off, _) = c.raw_entry(i);
268 assert_eq!(off % 8, 0, "record {i} not 8-aligned");
269 }
270 }
271
272 #[test]
273 fn empty_container_is_valid() {
274 let file = ContainerWriter::new().finish();
275 let c = Container::parse(&file).unwrap();
276 assert_eq!(c.len(), 0);
277 assert!(c.is_empty());
278 }
279
280 #[test]
281 fn rejects_corruption() {
282 let mut file = {
283 let mut w = ContainerWriter::new();
284 w.add(b"hello");
285 w.finish()
286 };
287 assert!(Container::parse(&file[..10]).is_err(), "truncated");
288
289 let mut bad_magic = file.clone();
290 bad_magic[0] = b'X';
291 assert!(matches!(
292 Container::parse(&bad_magic),
293 Err(Error::BadContainer(_))
294 ));
295
296 let mut bad_ver = file.clone();
297 bad_ver[4] = 2;
298 assert!(matches!(
299 Container::parse(&bad_ver),
300 Err(Error::BadContainer(_))
301 ));
302
303 let idx_off = u64::from_le_bytes(file[16..24].try_into().unwrap()) as usize;
305 file[idx_off..idx_off + 8].copy_from_slice(&u64::MAX.to_le_bytes());
306 assert!(matches!(
307 Container::parse(&file),
308 Err(Error::BadContainer(_))
309 ));
310 }
311}