1use sha2::{Digest, Sha256};
2use std::io::Cursor;
3use std::path::Path;
4
5use crate::error::{AhuError, Result};
6use crate::header::{FileHeader, HEADER_SIZE};
7use crate::index::{INDEX_RECORD_SIZE, IndexRecord};
8use crate::manifest::Manifest;
9
10#[derive(Debug)]
12pub struct Bundle {
13 pub header: FileHeader,
14 pub manifest: Manifest,
15 pub manifest_bytes: Vec<u8>,
16 pub seal_bytes: Vec<u8>,
17 pub index: Vec<IndexRecord>,
18 pub data: Vec<u8>,
19}
20
21impl Bundle {
22 pub fn from_file(path: &Path) -> Result<Self> {
24 let data = std::fs::read(path)?;
25 Self::from_bytes(&data)
26 }
27
28 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
30 let file_size = bytes.len() as u64;
31 let mut cursor = Cursor::new(bytes);
32
33 let header = FileHeader::read_from(&mut cursor)?;
34 header.validate_bounds(file_size)?;
35
36 let manifest_bytes =
37 Self::read_section(bytes, header.manifest_offset, header.manifest_length as u64)?;
38 let manifest = Manifest::from_cbor(&manifest_bytes)?;
39
40 let seal_bytes = Self::read_section(bytes, header.seal_offset, header.seal_length as u64)?;
41
42 let index_bytes = Self::read_section(bytes, header.index_offset, header.index_length)?;
43 let index = Self::parse_index(&index_bytes)?;
44
45 let data = Self::read_section(bytes, header.data_offset, header.data_length)?;
46
47 Ok(Bundle {
48 header,
49 manifest,
50 manifest_bytes,
51 seal_bytes,
52 index,
53 data,
54 })
55 }
56
57 pub fn to_bytes(&self) -> Result<Vec<u8>> {
59 let mut index = Vec::new();
60 for record in &self.index {
61 record.write_to(&mut index)?;
62 }
63 let mut header = self.header.clone();
64 header.manifest_offset = HEADER_SIZE as u64;
65 header.manifest_length = u32::try_from(self.manifest_bytes.len())
66 .map_err(|_| AhuError::InvalidOperation("manifest too large".into()))?;
67 header.seal_offset = header.manifest_offset + u64::from(header.manifest_length);
68 header.seal_length = u32::try_from(self.seal_bytes.len())
69 .map_err(|_| AhuError::InvalidOperation("seal too large".into()))?;
70 header.index_offset = header.seal_offset + u64::from(header.seal_length);
71 header.index_length = index.len() as u64;
72 header.data_offset = header
73 .index_offset
74 .checked_add(header.index_length)
75 .ok_or_else(|| AhuError::InvalidOperation("bundle too large".into()))?;
76 header.data_length = self.data.len() as u64;
77 let mut out = Vec::new();
78 header.write_to(&mut out)?;
79 out.extend_from_slice(&self.manifest_bytes);
80 out.extend_from_slice(&self.seal_bytes);
81 out.extend_from_slice(&index);
82 out.extend_from_slice(&self.data);
83 Ok(out)
84 }
85
86 pub fn lookup(&self, entry_key: &[u8; 32]) -> Option<&[u8]> {
89 let idx = crate::index::binary_search(&self.index, entry_key)?;
90 self.entry_at(idx)
91 }
92
93 pub fn lookup_preferred(&self, entry_key: &[u8; 32], preferences: &[u16]) -> Option<&[u8]> {
95 let idx = crate::index::binary_search_preferred(&self.index, entry_key, preferences)?;
96 self.entry_at(idx)
97 }
98
99 pub fn entry_at(&self, idx: usize) -> Option<&[u8]> {
100 let record = self.index.get(idx)?;
101 if record.is_tombstone() {
102 return None;
103 }
104 let start = usize::try_from(record.data_offset).ok()?;
105 let length = usize::try_from(record.data_length).ok()?;
106 let end = start.checked_add(length)?;
107 self.data.get(start..end)
108 }
109
110 pub fn entry_bytes(&self, record: &IndexRecord) -> Option<&[u8]> {
112 if record.is_tombstone() {
113 return None;
114 }
115 let start = usize::try_from(record.data_offset).ok()?;
116 let length = usize::try_from(record.data_length).ok()?;
117 let end = start.checked_add(length)?;
118 self.data.get(start..end)
119 }
120
121 fn read_section(bytes: &[u8], offset: u64, length: u64) -> Result<Vec<u8>> {
122 let range = usize::try_from(offset).ok().and_then(|start| {
123 usize::try_from(length)
124 .ok()
125 .and_then(|len| start.checked_add(len).map(|end| start..end))
126 });
127 range
128 .and_then(|r| bytes.get(r))
129 .map(<[u8]>::to_vec)
130 .ok_or(AhuError::HeaderOutOfBounds {
131 field: "section",
132 offset,
133 length,
134 file_size: bytes.len() as u64,
135 })
136 }
137
138 fn parse_index(index_bytes: &[u8]) -> Result<Vec<IndexRecord>> {
139 if index_bytes.len() % INDEX_RECORD_SIZE != 0 {
140 return Err(AhuError::IndexSizeMismatch {
141 size: index_bytes.len() as u64,
142 record_size: INDEX_RECORD_SIZE,
143 });
144 }
145
146 let count = index_bytes.len() / INDEX_RECORD_SIZE;
147 let mut records = Vec::with_capacity(count);
148 let mut cursor = Cursor::new(index_bytes);
149
150 for _ in 0..count {
151 records.push(IndexRecord::read_from(&mut cursor)?);
152 }
153 Ok(records)
154 }
155}
156
157pub struct BundleBuilder {
159 pub manifest: Manifest,
160 entries: Vec<(IndexRecord, Vec<u8>)>,
161}
162
163impl BundleBuilder {
164 pub fn new(manifest: Manifest) -> Self {
165 BundleBuilder {
166 manifest,
167 entries: Vec::new(),
168 }
169 }
170
171 pub fn add_entry(&mut self, entry_key: [u8; 32], response_der: Vec<u8>) {
174 self.add_entry_with_discriminator(entry_key, 0, response_der);
175 }
176
177 pub fn add_entry_with_discriminator(
178 &mut self,
179 entry_key: [u8; 32],
180 discriminator: u16,
181 response_der: Vec<u8>,
182 ) {
183 self.entries.push((
184 IndexRecord {
185 entry_key,
186 data_offset: 0,
187 data_length: response_der.len() as u32,
188 flags: crate::index::IndexFlags::empty(),
189 discriminator,
190 },
191 response_der,
192 ));
193 }
194
195 pub fn add_dual_entry(
201 &mut self,
202 entry_key_1: [u8; 32],
203 entry_key_2: [u8; 32],
204 response_der: Vec<u8>,
205 ) {
206 self.add_dual_entry_with_discriminator(entry_key_1, entry_key_2, response_der, 0);
207 }
208
209 pub fn add_dual_entry_with_discriminator(
210 &mut self,
211 entry_key_1: [u8; 32],
212 entry_key_2: [u8; 32],
213 response_der: Vec<u8>,
214 discriminator: u16,
215 ) {
216 let len = response_der.len() as u32;
217 let flags = crate::index::IndexFlags::ALIAS | crate::index::IndexFlags::MULTI;
218 self.entries.push((
219 IndexRecord {
220 entry_key: entry_key_1,
221 data_offset: 0,
222 data_length: len,
223 flags,
224 discriminator,
225 },
226 response_der.clone(),
227 ));
228 self.entries.push((
229 IndexRecord {
230 entry_key: entry_key_2,
231 data_offset: 0,
232 data_length: len,
233 flags,
234 discriminator,
235 },
236 response_der,
237 ));
238 }
239
240 pub fn add_tombstone(&mut self, entry_key: [u8; 32], discriminator: u16) {
243 self.entries.push((
244 IndexRecord {
245 entry_key,
246 data_offset: 0,
247 data_length: 0,
248 flags: crate::index::IndexFlags::TOMBSTONE,
249 discriminator,
250 },
251 Vec::new(),
252 ));
253 }
254
255 pub fn build<F>(mut self, seal_fn: F) -> Result<Vec<u8>>
258 where
259 F: FnOnce(&[u8]) -> Result<Vec<u8>>,
260 {
261 self.entries
263 .sort_by_key(|a| (a.0.entry_key, a.0.discriminator));
264
265 let mut data_section = Vec::new();
269 let mut index_records = Vec::with_capacity(self.entries.len());
270 let mut payload_offsets: std::collections::HashMap<[u8; 32], (u64, u32)> =
271 std::collections::HashMap::new();
272
273 for (mut record, payload) in self.entries {
274 if record.is_tombstone() {
275 record.data_offset = 0;
276 record.data_length = 0;
277 index_records.push(record);
278 continue;
279 }
280
281 if record.is_alias() {
282 let digest: [u8; 32] = Sha256::digest(&payload).into();
283 if let Some(&(offset, len)) = payload_offsets.get(&digest) {
284 record.data_offset = offset;
285 record.data_length = len;
286 index_records.push(record);
287 continue;
288 }
289 let offset = data_section.len() as u64;
290 let len = payload.len() as u32;
291 data_section.extend_from_slice(&payload);
292 payload_offsets.insert(digest, (offset, len));
293 record.data_offset = offset;
294 record.data_length = len;
295 } else {
296 let offset = data_section.len() as u64;
297 record.data_offset = offset;
298 record.data_length = payload.len() as u32;
299 data_section.extend_from_slice(&payload);
300 }
301
302 index_records.push(record);
303 }
304
305 let mut index_section = Vec::with_capacity(index_records.len() * INDEX_RECORD_SIZE);
307 for record in &index_records {
308 record.write_to(&mut index_section)?;
309 }
310
311 let index_digest: [u8; 32] = Sha256::digest(&index_section).into();
313 let data_digest: [u8; 32] = Sha256::digest(&data_section).into();
314
315 self.manifest.integrity.index_digest = index_digest;
317 self.manifest.integrity.data_digest = data_digest;
318 self.manifest.entry_count = index_records.len() as u64;
319
320 let manifest_bytes = self.manifest.to_cbor();
322
323 let seal_bytes = seal_fn(&manifest_bytes)?;
325
326 let manifest_offset = HEADER_SIZE as u64;
328 let manifest_length = manifest_bytes.len() as u32;
329 let seal_offset = manifest_offset + manifest_length as u64;
330 let seal_length = seal_bytes.len() as u32;
331 let index_offset = seal_offset + seal_length as u64;
332 let index_length = index_section.len() as u64;
333 let data_offset = index_offset + index_length;
334 let data_length = data_section.len() as u64;
335
336 let header = FileHeader {
337 format_major: crate::header::FORMAT_MAJOR,
338 format_minor: crate::header::FORMAT_MINOR,
339 manifest_offset,
340 manifest_length,
341 seal_offset,
342 seal_length,
343 index_offset,
344 index_length,
345 data_offset,
346 data_length,
347 };
348
349 let total_size = data_offset as usize + data_section.len();
351 let mut output = Vec::with_capacity(total_size);
352 header.write_to(&mut output)?;
353 output.extend_from_slice(&manifest_bytes);
354 output.extend_from_slice(&seal_bytes);
355 output.extend_from_slice(&index_section);
356 output.extend_from_slice(&data_section);
357
358 Ok(output)
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365 use crate::manifest::*;
366 use uuid::Uuid;
367
368 fn test_manifest() -> Manifest {
369 Manifest {
370 format_version: 1,
371 bundle_id: Uuid::nil(),
372 producer_id: "test".into(),
373 created_at: 1700000000,
374 bundle_type: BundleType::Full,
375 ca_scopes: vec![CaScope {
376 hash_algorithm: vec![0x01],
377 issuer_name_hash: vec![0xAA; 32],
378 issuer_key_hash: vec![0xBB; 32],
379 epoch: 1,
380 responder_id: ResponderId {
381 id_type: ResponderIdType::ByKey,
382 value: vec![0xCC; 20],
383 },
384 responder_chain: None,
385 signature_algorithm: vec![0x02],
386 completeness: Completeness::AuthoritativeComplete,
387 }],
388 window: Window {
389 produced_at: 1700000000,
390 this_update_min: 1700000000,
391 next_update_min: 1700086400,
392 next_update_max: 1700093600,
393 },
394 integrity: Integrity {
395 index_digest: [0; 32],
396 data_digest: [0; 32],
397 },
398 entry_count: 0,
399 continuity: Continuity {
400 prev_manifest_digest: None,
401 base_manifest_digest: None,
402 chain_length: 0,
403 },
404 shard: None,
405 compression: None,
406 extensions: None,
407 }
408 }
409
410 #[test]
411 fn build_and_read_empty_bundle() {
412 let manifest = test_manifest();
413 let builder = BundleBuilder::new(manifest);
414
415 let bytes = builder
416 .build(|manifest_bytes| {
417 Ok(Sha256::digest(manifest_bytes).to_vec())
419 })
420 .unwrap();
421
422 let bundle = Bundle::from_bytes(&bytes).unwrap();
423 assert_eq!(bundle.index.len(), 0);
424 assert_eq!(bundle.manifest.entry_count, 0);
425 assert_eq!(bundle.manifest.producer_id, "test");
426 }
427
428 #[test]
429 fn build_and_lookup() {
430 let manifest = test_manifest();
431 let mut builder = BundleBuilder::new(manifest);
432
433 let fake_certid = b"fake-certid-for-testing-1234567";
434 let entry_key = crate::index::compute_entry_key(fake_certid);
435 let response = b"fake-ocsp-response-bytes".to_vec();
436
437 builder.add_entry(entry_key, response.clone());
438
439 let bytes = builder.build(|m| Ok(Sha256::digest(m).to_vec())).unwrap();
440
441 let bundle = Bundle::from_bytes(&bytes).unwrap();
442 assert_eq!(bundle.index.len(), 1);
443 assert_eq!(bundle.manifest.entry_count, 1);
444
445 let found = bundle.lookup(&entry_key).expect("entry should be found");
446 assert_eq!(found, &response[..]);
447
448 let missing = [0xFF; 32];
449 assert!(bundle.lookup(&missing).is_none());
450 }
451
452 #[test]
453 fn entries_are_sorted() {
454 let manifest = test_manifest();
455 let mut builder = BundleBuilder::new(manifest);
456
457 let key_b = [0xBB; 32];
459 let key_a = [0xAA; 32];
460
461 builder.add_entry(key_b, b"response-b".to_vec());
462 builder.add_entry(key_a, b"response-a".to_vec());
463
464 let bytes = builder.build(|m| Ok(Sha256::digest(m).to_vec())).unwrap();
465
466 let bundle = Bundle::from_bytes(&bytes).unwrap();
467 assert_eq!(bundle.index[0].entry_key, key_a);
468 assert_eq!(bundle.index[1].entry_key, key_b);
469 }
470}