qualia_core_db/bundle/
reader.rs1use crate::container_10d::crc32c::crc32c_update;
5
6use super::format::{
7 BundleEntry, BundleError, BUNDLE_HEADER_SIZE, BUNDLE_MAGIC, BUNDLE_VERSION, OFF_CRC32C,
8 OFF_FLAGS, OFF_INDEX_LENGTH, OFF_INDEX_OFFSET, OFF_MAGIC, OFF_TOTAL_LENGTH, OFF_VERSION,
9};
10
11#[inline]
12fn read_u32(b: &[u8], off: usize) -> u32 {
13 u32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
14}
15
16#[inline]
17fn read_u64(b: &[u8], off: usize) -> u64 {
18 let mut a = [0u8; 8];
19 a.copy_from_slice(&b[off..off + 8]);
20 u64::from_le_bytes(a)
21}
22
23fn whole_file_crc(bytes: &[u8]) -> u32 {
27 let mut crc = 0xFFFF_FFFFu32;
28 crc = crc32c_update(crc, &bytes[..OFF_CRC32C]);
29 crc = crc32c_update(crc, &[0u8; 4]);
30 crc = crc32c_update(crc, &bytes[OFF_CRC32C + 4..]);
31 !crc
32}
33
34pub struct BundleReader<'a> {
37 bytes: &'a [u8],
38 entries: Vec<BundleEntry>,
39 flags: u16,
40}
41
42impl<'a> BundleReader<'a> {
43 pub fn parse(bytes: &'a [u8]) -> Result<Self, BundleError> {
47 if bytes.len() < BUNDLE_HEADER_SIZE {
48 return Err(BundleError::TooShort);
49 }
50 if bytes[OFF_MAGIC..OFF_MAGIC + 4] != BUNDLE_MAGIC {
51 return Err(BundleError::BadMagic);
52 }
53 let version = u16::from_le_bytes([bytes[OFF_VERSION], bytes[OFF_VERSION + 1]]);
54 if version != BUNDLE_VERSION {
55 return Err(BundleError::UnsupportedVersion(version));
56 }
57 let flags = u16::from_le_bytes([bytes[OFF_FLAGS], bytes[OFF_FLAGS + 1]]);
58
59 let total_length = read_u64(bytes, OFF_TOTAL_LENGTH);
60 if total_length as usize != bytes.len() {
61 return Err(BundleError::LengthMismatch {
62 header: total_length,
63 actual: bytes.len(),
64 });
65 }
66
67 let stored_crc = read_u32(bytes, OFF_CRC32C);
69 let got = whole_file_crc(bytes);
70 if got != stored_crc {
71 return Err(BundleError::CrcMismatch {
72 expected: stored_crc,
73 got,
74 });
75 }
76
77 let index_offset = read_u64(bytes, OFF_INDEX_OFFSET);
78 let index_length = read_u64(bytes, OFF_INDEX_LENGTH);
79 let end = index_offset
80 .checked_add(index_length)
81 .ok_or(BundleError::BadIndexPointer {
82 offset: index_offset,
83 length: index_length,
84 total: bytes.len(),
85 })?;
86 if index_offset < BUNDLE_HEADER_SIZE as u64 || end as usize > bytes.len() {
87 return Err(BundleError::BadIndexPointer {
88 offset: index_offset,
89 length: index_length,
90 total: bytes.len(),
91 });
92 }
93
94 let index_slice = &bytes[index_offset as usize..end as usize];
95 let entries: Vec<BundleEntry> =
96 ciborium::from_reader(index_slice).map_err(|e| BundleError::Cbor(e.to_string()))?;
97
98 for e in &entries {
100 let e_end =
101 e.offset
102 .checked_add(e.length)
103 .ok_or_else(|| BundleError::EntryOutOfBounds {
104 key: e.key.clone(),
105 offset: e.offset,
106 length: e.length,
107 })?;
108 if e.offset < BUNDLE_HEADER_SIZE as u64 || e_end > index_offset {
109 return Err(BundleError::EntryOutOfBounds {
110 key: e.key.clone(),
111 offset: e.offset,
112 length: e.length,
113 });
114 }
115 }
116
117 Ok(Self {
118 bytes,
119 entries,
120 flags,
121 })
122 }
123
124 pub fn entries(&self) -> &[BundleEntry] {
126 &self.entries
127 }
128
129 pub fn flags(&self) -> u16 {
132 self.flags
133 }
134
135 pub fn entry(&self, key: &str) -> Option<&BundleEntry> {
137 self.entries.iter().find(|e| e.key == key)
138 }
139
140 pub fn get(&self, key: &str) -> Option<&'a [u8]> {
144 let e = self.entry(key)?;
145 Some(&self.bytes[e.offset as usize..(e.offset + e.length) as usize])
146 }
147
148 pub fn segment(&self, key: &str, seg_offset: u64, seg_len: u64) -> Option<&'a [u8]> {
153 let e = self.entry(key)?;
154 let seg_end = seg_offset.checked_add(seg_len)?;
155 if seg_end > e.length {
156 return None;
157 }
158 let start = (e.offset + seg_offset) as usize;
159 Some(&self.bytes[start..start + seg_len as usize])
160 }
161
162 pub fn verify_entry(&self, key: &str) -> bool {
164 use sha2::{Digest, Sha256};
165 let Some(e) = self.entry(key) else {
166 return false;
167 };
168 let Some(payload) = self.get(key) else {
169 return false;
170 };
171 let mut hasher = Sha256::new();
172 hasher.update(payload);
173 hasher.finalize().as_slice() == e.sha256.as_slice()
174 }
175}
176
177#[cfg(not(target_arch = "wasm32"))]
183pub struct BundleMmap {
184 map: memmap2::Mmap,
185}
186
187#[cfg(not(target_arch = "wasm32"))]
188impl BundleMmap {
189 pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, BundleError> {
191 let file = std::fs::File::open(path).map_err(|e| BundleError::Io(e.to_string()))?;
192 let map =
195 unsafe { memmap2::Mmap::map(&file) }.map_err(|e| BundleError::Io(e.to_string()))?;
196 Ok(Self { map })
197 }
198
199 pub fn as_bytes(&self) -> &[u8] {
201 &self.map
202 }
203
204 pub fn reader(&self) -> Result<BundleReader<'_>, BundleError> {
206 BundleReader::parse(&self.map)
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::super::writer::BundleWriter;
213 use super::*;
214
215 fn sample_bundle() -> Vec<u8> {
216 let mut w = BundleWriter::new();
217 w.add_file(
218 "liver.10d",
219 "10d",
220 (0..200u32).map(|i| i as u8).collect(),
221 Some(vec![0xAA, 0xBB]),
222 )
223 .unwrap();
224 w.add_file(
225 "graph.q42",
226 "q42",
227 b"hello q42 segment world".to_vec(),
228 None,
229 )
230 .unwrap();
231 w.build().unwrap()
232 }
233
234 #[test]
235 fn roundtrip_is_byte_identical() {
236 let liver: Vec<u8> = (0..200u32).map(|i| i as u8).collect();
237 let bytes = sample_bundle();
238 let r = BundleReader::parse(&bytes).unwrap();
239 assert_eq!(r.entries().len(), 2);
240 assert_eq!(r.get("liver.10d").unwrap(), liver.as_slice());
242 assert_eq!(r.get("graph.q42").unwrap(), b"hello q42 segment world");
243 assert_eq!(r.entry("liver.10d").unwrap().kind, "10d");
244 assert_eq!(
245 r.entry("liver.10d").unwrap().meta.as_deref(),
246 Some(&[0xAA, 0xBB][..])
247 );
248 assert!(r.get("missing").is_none());
249 }
250
251 #[test]
252 fn interior_segment_access_is_not_interfered_with() {
253 let bytes = sample_bundle();
254 let r = BundleReader::parse(&bytes).unwrap();
255 assert_eq!(r.segment("graph.q42", 6, 11).unwrap(), b"q42 segment");
257 assert!(r.segment("graph.q42", 20, 100).is_none());
259 }
260
261 #[test]
262 fn per_entry_sha256_verifies_and_detects_tamper() {
263 let mut bytes = sample_bundle();
264 let r = BundleReader::parse(&bytes).unwrap();
265 assert!(r.verify_entry("liver.10d"));
266 assert!(r.verify_entry("graph.q42"));
267 drop(r);
268 let off = {
270 let r = BundleReader::parse(&bytes).unwrap();
271 r.entry("liver.10d").unwrap().offset as usize
272 };
273 bytes[off] ^= 0xFF;
274 assert!(matches!(
275 BundleReader::parse(&bytes),
276 Err(BundleError::CrcMismatch { .. })
277 ));
278 }
279
280 #[test]
281 fn rejects_bad_magic_and_short_input() {
282 assert!(matches!(
283 BundleReader::parse(&[0u8; 10]),
284 Err(BundleError::TooShort)
285 ));
286 let mut bytes = sample_bundle();
287 bytes[0] = b'X';
288 assert!(matches!(
289 BundleReader::parse(&bytes),
290 Err(BundleError::BadMagic)
291 ));
292 }
293
294 #[cfg(not(target_arch = "wasm32"))]
295 #[test]
296 fn mmap_roundtrip() {
297 let bytes = sample_bundle();
298 let dir = tempfile::tempdir().unwrap();
299 let path = dir.path().join("pack.hmc");
300 std::fs::write(&path, &bytes).unwrap();
301 let m = BundleMmap::open(&path).unwrap();
302 let r = m.reader().unwrap();
303 assert_eq!(r.get("graph.q42").unwrap(), b"hello q42 segment world");
304 assert!(r.verify_entry("liver.10d"));
305 }
306}