qualia_core_db/bundle/
writer.rs1use sha2::{Digest, Sha256};
4
5use crate::container_10d::crc32c::crc32c;
6
7use super::format::{
8 align_up, BundleEntry, BundleError, BUNDLE_ENTRY_ALIGN, BUNDLE_HEADER_SIZE, BUNDLE_MAGIC,
9 BUNDLE_VERSION, OFF_CRC32C, OFF_ENTRY_COUNT, OFF_INDEX_LENGTH, OFF_INDEX_OFFSET, OFF_MAGIC,
10 OFF_TOTAL_LENGTH, OFF_VERSION,
11};
12
13#[derive(Default)]
20pub struct BundleWriter {
21 entries: Vec<(String, String, Vec<u8>, Option<Vec<u8>>)>,
23}
24
25impl BundleWriter {
26 pub fn new() -> Self {
27 Self::default()
28 }
29
30 pub fn len(&self) -> usize {
32 self.entries.len()
33 }
34
35 pub fn is_empty(&self) -> bool {
36 self.entries.is_empty()
37 }
38
39 pub fn add_file(
43 &mut self,
44 key: impl Into<String>,
45 kind: impl Into<String>,
46 bytes: Vec<u8>,
47 meta: Option<Vec<u8>>,
48 ) -> Result<&mut Self, BundleError> {
49 let key = key.into();
50 if key.is_empty() {
51 return Err(BundleError::EmptyKey);
52 }
53 if self.entries.iter().any(|(k, _, _, _)| *k == key) {
54 return Err(BundleError::DuplicateKey(key));
55 }
56 self.entries.push((key, kind.into(), bytes, meta));
57 Ok(self)
58 }
59
60 pub fn build(&self) -> Result<Vec<u8>, BundleError> {
62 let mut out = vec![0u8; BUNDLE_HEADER_SIZE];
63 let mut index: Vec<BundleEntry> = Vec::with_capacity(self.entries.len());
64
65 for (key, kind, bytes, meta) in &self.entries {
66 let padded = align_up(out.len(), BUNDLE_ENTRY_ALIGN);
68 out.resize(padded, 0);
69 let offset = out.len() as u64;
70 out.extend_from_slice(bytes);
71
72 let mut hasher = Sha256::new();
73 hasher.update(bytes);
74 let sha256 = hasher.finalize().to_vec();
75
76 index.push(BundleEntry {
77 key: key.clone(),
78 kind: kind.clone(),
79 offset,
80 length: bytes.len() as u64,
81 sha256,
82 meta: meta.clone(),
83 });
84 }
85
86 let padded = align_up(out.len(), BUNDLE_ENTRY_ALIGN);
88 out.resize(padded, 0);
89 let index_offset = out.len() as u64;
90
91 let mut index_bytes = Vec::new();
92 ciborium::into_writer(&index, &mut index_bytes)
93 .map_err(|e| BundleError::Cbor(e.to_string()))?;
94 out.extend_from_slice(&index_bytes);
95 let index_length = index_bytes.len() as u64;
96 let total_length = out.len() as u64;
97
98 out[OFF_MAGIC..OFF_MAGIC + 4].copy_from_slice(&BUNDLE_MAGIC);
100 out[OFF_VERSION..OFF_VERSION + 2].copy_from_slice(&BUNDLE_VERSION.to_le_bytes());
101 out[OFF_ENTRY_COUNT..OFF_ENTRY_COUNT + 4]
102 .copy_from_slice(&(self.entries.len() as u32).to_le_bytes());
103 out[OFF_INDEX_OFFSET..OFF_INDEX_OFFSET + 8].copy_from_slice(&index_offset.to_le_bytes());
104 out[OFF_INDEX_LENGTH..OFF_INDEX_LENGTH + 8].copy_from_slice(&index_length.to_le_bytes());
105 out[OFF_TOTAL_LENGTH..OFF_TOTAL_LENGTH + 8].copy_from_slice(&total_length.to_le_bytes());
106
107 let crc = crc32c(&out);
109 out[OFF_CRC32C..OFF_CRC32C + 4].copy_from_slice(&crc.to_le_bytes());
110
111 Ok(out)
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 #[test]
120 fn rejects_empty_and_duplicate_keys() {
121 let mut w = BundleWriter::new();
122 assert!(matches!(
123 w.add_file("", "10d", vec![1], None),
124 Err(BundleError::EmptyKey)
125 ));
126 w.add_file("a", "10d", vec![1, 2], None).unwrap();
127 assert!(matches!(
128 w.add_file("a", "q42", vec![3], None),
129 Err(BundleError::DuplicateKey(_))
130 ));
131 }
132
133 #[test]
134 fn entries_are_page_aligned() {
135 let mut w = BundleWriter::new();
136 w.add_file("a", "10d", vec![0xAB; 7], None).unwrap();
137 w.add_file("b", "10d", vec![0xCD; 130], None).unwrap();
138 let bytes = w.build().unwrap();
139 let reader = super::super::reader::BundleReader::parse(&bytes).unwrap();
140 for e in reader.entries() {
141 assert_eq!(
142 e.offset % BUNDLE_ENTRY_ALIGN as u64,
143 0,
144 "entry {} unaligned",
145 e.key
146 );
147 }
148 }
149}