1#![allow(deprecated)]
48
49use crate::error::{Error, Result};
50
51pub const CONTAINER_MAGIC: &[u8; 4] = b"VRTC";
53pub const CONTAINER_VERSION: u8 = 1;
55pub const CONTAINER_HEADER_LEN: usize = 32;
57const ALIGN: usize = 8;
58const INDEX_ENTRY_LEN: usize = 16;
59
60#[inline]
61fn align_up(x: usize, a: usize) -> usize {
62 (x + a - 1) & !(a - 1)
63}
64
65#[derive(Default)]
81#[deprecated(
82 since = "0.2.0",
83 note = "superseded by the `.verit` file: use `FileBuilder` (in memory) or `FileWriter` (on disk). \
84 A `.vertc` container carries no schema section, so it cannot be read without an \
85 out-of-band registry. Removed in 0.3.0 — see ADR-0002."
86)]
87pub struct ContainerWriter {
88 buf: Vec<u8>,
89 index: Vec<(u64, u64)>,
91}
92
93impl ContainerWriter {
94 pub fn new() -> ContainerWriter {
95 ContainerWriter {
96 buf: vec![0; CONTAINER_HEADER_LEN], index: Vec::new(),
98 }
99 }
100
101 pub fn add(&mut self, message: &[u8]) -> &mut Self {
104 let pad = align_up(self.buf.len(), ALIGN) - self.buf.len();
105 self.buf.resize(self.buf.len() + pad, 0);
106 let offset = self.buf.len() as u64;
107 self.buf.extend_from_slice(message);
108 self.index.push((offset, message.len() as u64));
109 self
110 }
111
112 pub fn len(&self) -> usize {
114 self.index.len()
115 }
116
117 pub fn is_empty(&self) -> bool {
118 self.index.is_empty()
119 }
120
121 pub fn finish(mut self) -> Vec<u8> {
124 let pad = align_up(self.buf.len(), ALIGN) - self.buf.len();
125 self.buf.resize(self.buf.len() + pad, 0);
126 let index_offset = self.buf.len() as u64;
127 for (off, len) in &self.index {
128 self.buf.extend_from_slice(&off.to_le_bytes());
129 self.buf.extend_from_slice(&len.to_le_bytes());
130 }
131 let file_len = self.buf.len() as u64;
132
133 self.buf[0..4].copy_from_slice(CONTAINER_MAGIC);
134 self.buf[4] = CONTAINER_VERSION;
135 self.buf[8..12].copy_from_slice(&(self.index.len() as u32).to_le_bytes());
137 self.buf[16..24].copy_from_slice(&index_offset.to_le_bytes());
139 self.buf[24..32].copy_from_slice(&file_len.to_le_bytes());
140 self.buf
141 }
142}
143
144#[derive(Clone, Debug)]
148#[deprecated(
149 since = "0.2.0",
150 note = "superseded by the `.verit` file: use `FileView`. A `.vertc` container carries no \
151 schema section, so it cannot be read without an out-of-band registry. \
152 Removed in 0.3.0 — see ADR-0002."
153)]
154pub struct Container<'a> {
155 buf: &'a [u8],
156 index_offset: usize,
157 count: usize,
158}
159
160impl<'a> Container<'a> {
161 pub fn parse(buf: &'a [u8]) -> Result<Container<'a>> {
165 if buf.len() < CONTAINER_HEADER_LEN {
166 return Err(Error::Truncated);
167 }
168 if &buf[0..4] != CONTAINER_MAGIC {
169 return Err(Error::BadContainer("bad magic"));
170 }
171 if buf[4] != CONTAINER_VERSION {
172 return Err(Error::BadContainer("unsupported container version"));
173 }
174 if buf[5] != 0 || u16::from_le_bytes(buf[6..8].try_into().unwrap()) != 0 {
175 return Err(Error::BadContainer("nonzero reserved header field"));
176 }
177 let count = u32::from_le_bytes(buf[8..12].try_into().unwrap()) as usize;
178 let index_offset = u64::from_le_bytes(buf[16..24].try_into().unwrap());
179 let file_len = u64::from_le_bytes(buf[24..32].try_into().unwrap());
180 if file_len as usize != buf.len() {
181 return Err(Error::BadContainer("file length mismatch"));
182 }
183 let index_offset = usize::try_from(index_offset)
184 .map_err(|_| Error::BadContainer("index offset overflow"))?;
185 if index_offset % ALIGN != 0 || index_offset < CONTAINER_HEADER_LEN {
186 return Err(Error::BadContainer("misaligned index offset"));
187 }
188 let index_bytes = count
190 .checked_mul(INDEX_ENTRY_LEN)
191 .ok_or(Error::BadContainer("index size overflow"))?;
192 let index_end = index_offset
193 .checked_add(index_bytes)
194 .ok_or(Error::BadContainer("index end overflow"))?;
195 if index_end > buf.len() {
196 return Err(Error::BadContainer("index out of bounds"));
197 }
198 let container = Container {
199 buf,
200 index_offset,
201 count,
202 };
203 for i in 0..count {
205 let (off, len) = container.raw_entry(i);
206 let off =
207 usize::try_from(off).map_err(|_| Error::BadContainer("record offset overflow"))?;
208 let len =
209 usize::try_from(len).map_err(|_| Error::BadContainer("record length overflow"))?;
210 if off % ALIGN != 0 {
211 return Err(Error::BadContainer("misaligned record"));
212 }
213 let end = off
214 .checked_add(len)
215 .ok_or(Error::BadContainer("record extent overflow"))?;
216 if off < CONTAINER_HEADER_LEN || end > index_offset {
218 return Err(Error::BadContainer("record outside record region"));
219 }
220 }
221 Ok(container)
222 }
223
224 #[inline]
225 fn raw_entry(&self, i: usize) -> (u64, u64) {
226 let base = self.index_offset + i * INDEX_ENTRY_LEN;
227 let off = u64::from_le_bytes(self.buf[base..base + 8].try_into().unwrap());
228 let len = u64::from_le_bytes(self.buf[base + 8..base + 16].try_into().unwrap());
229 (off, len)
230 }
231
232 pub fn len(&self) -> usize {
234 self.count
235 }
236
237 pub fn is_empty(&self) -> bool {
238 self.count == 0
239 }
240
241 pub fn get(&self, i: usize) -> Result<&'a [u8]> {
244 if i >= self.count {
245 return Err(Error::IndexOutOfBounds);
246 }
247 let (off, len) = self.raw_entry(i);
248 Ok(&self.buf[off as usize..(off + len) as usize])
250 }
251
252 pub fn iter(&self) -> impl Iterator<Item = &'a [u8]> + '_ {
254 (0..self.count).map(move |i| self.get(i).expect("index validated in parse"))
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn round_trips_messages() {
264 let msgs: Vec<Vec<u8>> = vec![
265 b"a".to_vec(),
266 b"".to_vec(),
267 (0..100u8).collect(),
268 b"the last one".to_vec(),
269 ];
270 let mut w = ContainerWriter::new();
271 for m in &msgs {
272 w.add(m);
273 }
274 assert_eq!(w.len(), 4);
275 let file = w.finish();
276
277 let c = Container::parse(&file).unwrap();
278 assert_eq!(c.len(), 4);
279 for (i, m) in msgs.iter().enumerate() {
280 assert_eq!(c.get(i).unwrap(), &m[..]);
281 }
282 let collected: Vec<&[u8]> = c.iter().collect();
283 assert_eq!(collected.len(), 4);
284 assert!(c.get(4).is_err());
285 }
286
287 #[test]
288 fn records_are_eight_byte_aligned() {
289 let mut w = ContainerWriter::new();
290 w.add(b"odd-length-7").add(b"x"); let file = w.finish();
292 let c = Container::parse(&file).unwrap();
293 for i in 0..c.len() {
294 let (off, _) = c.raw_entry(i);
295 assert_eq!(off % 8, 0, "record {i} not 8-aligned");
296 }
297 }
298
299 #[test]
300 fn empty_container_is_valid() {
301 let file = ContainerWriter::new().finish();
302 let c = Container::parse(&file).unwrap();
303 assert_eq!(c.len(), 0);
304 assert!(c.is_empty());
305 }
306
307 #[test]
308 fn rejects_corruption() {
309 let mut file = {
310 let mut w = ContainerWriter::new();
311 w.add(b"hello");
312 w.finish()
313 };
314 assert!(Container::parse(&file[..10]).is_err(), "truncated");
315
316 let mut bad_magic = file.clone();
317 bad_magic[0] = b'X';
318 assert!(matches!(
319 Container::parse(&bad_magic),
320 Err(Error::BadContainer(_))
321 ));
322
323 let mut bad_ver = file.clone();
324 bad_ver[4] = 2;
325 assert!(matches!(
326 Container::parse(&bad_ver),
327 Err(Error::BadContainer(_))
328 ));
329
330 let idx_off = u64::from_le_bytes(file[16..24].try_into().unwrap()) as usize;
332 file[idx_off..idx_off + 8].copy_from_slice(&u64::MAX.to_le_bytes());
333 assert!(matches!(
334 Container::parse(&file),
335 Err(Error::BadContainer(_))
336 ));
337 }
338}