alopex_core/storage/format/
reader.rs1use std::pin::Pin;
7
8use crate::storage::checksum;
9use crate::storage::compression;
10use crate::storage::format::{
11 FileFooter, FileHeader, FileVersion, FormatError, SectionEntry, SectionIndex, FOOTER_SIZE,
12 HEADER_SIZE,
13};
14
15#[cfg(not(target_arch = "wasm32"))]
16use memmap2::Mmap;
17#[cfg(not(target_arch = "wasm32"))]
18use std::fs::File;
19#[cfg(not(target_arch = "wasm32"))]
20use std::path::{Path, PathBuf};
21
22#[cfg(target_arch = "wasm32")]
23use std::vec::Vec;
24
25pub enum FileSource {
27 #[cfg(not(target_arch = "wasm32"))]
29 Path(PathBuf),
30 #[cfg(target_arch = "wasm32")]
32 Buffer(Vec<u8>),
33 #[cfg(all(target_arch = "wasm32", feature = "wasm-indexeddb"))]
35 IndexedDb {
36 db_name: String,
37 key: String,
38 length: u64,
40 loader: Box<dyn RangeLoader>,
42 },
43}
44
45#[cfg(not(target_arch = "wasm32"))]
47pub type PrefetchFuture<'a> =
48 Pin<Box<dyn std::future::Future<Output = Result<(), FormatError>> + Send + 'a>>;
49#[cfg(target_arch = "wasm32")]
51pub type PrefetchFuture<'a> =
52 Pin<Box<dyn std::future::Future<Output = Result<(), FormatError>> + 'a>>;
53
54#[cfg(target_arch = "wasm32")]
56pub trait RangeLoader: Send + Sync {
57 fn load_range(&self, offset: u64, length: u64) -> Result<Vec<u8>, FormatError>;
59}
60
61#[cfg(target_arch = "wasm32")]
63pub struct BufferRangeLoader {
64 data: Vec<u8>,
65}
66
67#[cfg(target_arch = "wasm32")]
68impl BufferRangeLoader {
69 pub fn new(data: Vec<u8>) -> Self {
71 Self { data }
72 }
73}
74
75#[cfg(target_arch = "wasm32")]
76impl RangeLoader for BufferRangeLoader {
77 fn load_range(&self, offset: u64, length: u64) -> Result<Vec<u8>, FormatError> {
78 let offset = offset as usize;
79 let length = length as usize;
80 let end = offset
81 .checked_add(length)
82 .ok_or(FormatError::IncompleteWrite)?;
83 if end > self.data.len() {
84 return Err(FormatError::IncompleteWrite);
85 }
86 Ok(self.data[offset..end].to_vec())
87 }
88}
89
90pub trait FileReader {
92 fn open(source: FileSource) -> Result<Self, FormatError>
94 where
95 Self: Sized;
96
97 fn header(&self) -> &FileHeader;
99
100 fn footer(&self) -> &FileFooter;
102
103 fn section_index(&self) -> &SectionIndex;
105
106 fn read_section(&self, section_id: u32) -> Result<Vec<u8>, FormatError>;
108
109 fn read_section_raw(&self, section_id: u32) -> Result<Vec<u8>, FormatError>;
111
112 fn validate_section(&self, section_id: u32) -> Result<(), FormatError>;
114
115 fn validate_all(&self) -> Result<(), FormatError>;
117
118 fn prefetch_sections<'a>(&'a self, section_ids: &'a [u32]) -> PrefetchFuture<'a>;
120}
121
122#[cfg(not(target_arch = "wasm32"))]
124pub struct AlopexFileReader {
125 mmap: Mmap,
126 header: FileHeader,
127 footer: FileFooter,
128 section_index: SectionIndex,
129}
130
131#[cfg(not(target_arch = "wasm32"))]
132impl AlopexFileReader {
133 fn map_file(path: &Path) -> Result<Mmap, FormatError> {
134 let file = File::open(path).map_err(|_| FormatError::IncompleteWrite)?;
135 unsafe { Mmap::map(&file).map_err(|_| FormatError::IncompleteWrite) }
136 }
137
138 fn read_footer(mmap: &Mmap) -> Result<FileFooter, FormatError> {
139 if mmap.len() < FOOTER_SIZE {
140 return Err(FormatError::IncompleteWrite);
141 }
142 let start = mmap.len() - FOOTER_SIZE;
143 let mut buf = [0u8; FOOTER_SIZE];
144 buf.copy_from_slice(&mmap[start..]);
145 FileFooter::from_bytes(&buf)
146 }
147
148 fn read_header(mmap: &Mmap) -> Result<FileHeader, FormatError> {
149 if mmap.len() < HEADER_SIZE {
150 return Err(FormatError::IncompleteWrite);
151 }
152 let mut buf = [0u8; HEADER_SIZE];
153 buf.copy_from_slice(&mmap[..HEADER_SIZE]);
154 let header = FileHeader::from_bytes(&buf)?;
155 header.check_compatibility(&FileVersion::CURRENT)?;
156 Ok(header)
157 }
158
159 fn read_section_index(mmap: &Mmap, footer: &FileFooter) -> Result<SectionIndex, FormatError> {
160 let offset = footer.section_index_offset as usize;
161 if offset >= mmap.len() {
162 return Err(FormatError::IncompleteWrite);
163 }
164 if mmap.len() < offset + 4 {
166 return Err(FormatError::IncompleteWrite);
167 }
168 let count = u32::from_le_bytes(
169 mmap[offset..offset + 4]
170 .try_into()
171 .expect("slice length checked"),
172 );
173 let expected = 4usize + count as usize * SectionEntry::SIZE;
174 if mmap.len() < offset + expected {
175 return Err(FormatError::IncompleteWrite);
176 }
177 SectionIndex::from_bytes(&mmap[offset..offset + expected])
178 }
179
180 fn entry(&self, section_id: u32) -> Result<&SectionEntry, FormatError> {
181 self.section_index
182 .find_by_id(section_id)
183 .ok_or(FormatError::IncompleteWrite)
184 }
185}
186
187#[cfg(not(target_arch = "wasm32"))]
188impl FileReader for AlopexFileReader {
189 fn open(source: FileSource) -> Result<Self, FormatError> {
190 let FileSource::Path(path) = source;
191 let mmap = Self::map_file(&path)?;
192 let footer = Self::read_footer(&mmap)?;
193 let section_index = Self::read_section_index(&mmap, &footer)?;
194 let header = Self::read_header(&mmap)?;
195 Ok(Self {
196 mmap,
197 header,
198 footer,
199 section_index,
200 })
201 }
202
203 fn header(&self) -> &FileHeader {
204 &self.header
205 }
206
207 fn footer(&self) -> &FileFooter {
208 &self.footer
209 }
210
211 fn section_index(&self) -> &SectionIndex {
212 &self.section_index
213 }
214
215 fn read_section(&self, section_id: u32) -> Result<Vec<u8>, FormatError> {
216 let entry = self.entry(section_id)?;
217 let raw = self.read_section_raw(section_id)?;
218 if raw.len() as u64 != entry.compressed_length {
219 return Err(FormatError::IncompleteWrite);
220 }
221 checksum::verify(&raw, self.header.checksum_algorithm, entry.checksum as u64)?;
222 let decompressed = compression::decompress(&raw, entry.compression)?;
223 if decompressed.len() as u64 != entry.uncompressed_length {
224 return Err(FormatError::IncompleteWrite);
225 }
226 Ok(decompressed)
227 }
228
229 fn read_section_raw(&self, section_id: u32) -> Result<Vec<u8>, FormatError> {
230 let entry = self.entry(section_id)?;
231 let offset = entry.offset as usize;
232 let end = offset
233 .checked_add(entry.compressed_length as usize)
234 .ok_or(FormatError::IncompleteWrite)?;
235 if end > self.mmap.len() || end - offset != entry.compressed_length as usize {
236 return Err(FormatError::IncompleteWrite);
237 }
238 Ok(self.mmap[offset..end].to_vec())
239 }
240
241 fn validate_section(&self, section_id: u32) -> Result<(), FormatError> {
242 let entry = self.entry(section_id)?;
243 let raw = self.read_section_raw(section_id)?;
244 checksum::verify(&raw, self.header.checksum_algorithm, entry.checksum as u64)
245 }
246
247 fn validate_all(&self) -> Result<(), FormatError> {
248 for entry in &self.section_index.entries {
249 self.validate_section(entry.section_id)?;
250 }
251 Ok(())
252 }
253
254 fn prefetch_sections<'a>(&'a self, _section_ids: &'a [u32]) -> PrefetchFuture<'a> {
255 let section_ids = _section_ids.to_vec();
257 Box::pin(async move {
258 for id in section_ids {
259 let _ = self.read_section_raw(id)?;
260 }
261 Ok(())
262 })
263 }
264}
265
266#[cfg(target_arch = "wasm32")]
268pub struct WasmReaderConfig {
269 pub full_load_threshold_bytes: usize,
271 pub range_loader: Option<Box<dyn RangeLoader>>,
273}
274
275#[cfg(target_arch = "wasm32")]
276impl Default for WasmReaderConfig {
277 fn default() -> Self {
278 Self {
279 full_load_threshold_bytes: 100 * 1024 * 1024, range_loader: None,
281 }
282 }
283}
284
285#[cfg(target_arch = "wasm32")]
287pub struct AlopexFileReader {
288 buffer: Option<Vec<u8>>,
289 loader: Option<Box<dyn RangeLoader>>,
290 #[allow(dead_code)]
291 length: u64,
292 header: FileHeader,
293 footer: FileFooter,
294 section_index: SectionIndex,
295 #[allow(dead_code)]
296 config: WasmReaderConfig,
297}
298
299#[cfg(target_arch = "wasm32")]
300impl AlopexFileReader {
301 pub fn open_with_config(
303 source: FileSource,
304 config: WasmReaderConfig,
305 ) -> Result<Self, FormatError> {
306 match source {
307 FileSource::Buffer(buf) => Self::from_buffer(buf, config),
308 #[cfg(feature = "wasm-indexeddb")]
309 FileSource::IndexedDb {
310 length,
311 db_name: _,
312 key: _,
313 loader,
314 } => {
315 let mut cfg = config;
316 cfg.range_loader = Some(loader);
317 Self::from_indexed_db(length, cfg)
318 }
319 }
320 }
321
322 fn from_buffer(buffer: Vec<u8>, config: WasmReaderConfig) -> Result<Self, FormatError> {
323 if buffer.len() > config.full_load_threshold_bytes {
325 let mut cfg = config;
326 if let Some(loader) = cfg.range_loader.take() {
327 return Self::from_indexed_db(
328 buffer.len() as u64,
329 WasmReaderConfig {
330 range_loader: Some(loader),
331 ..cfg
332 },
333 );
334 } else {
335 return Err(FormatError::IncompleteWrite);
336 }
337 }
338
339 if buffer.len() < HEADER_SIZE + FOOTER_SIZE {
340 return Err(FormatError::IncompleteWrite);
341 }
342
343 let footer = Self::read_footer(&buffer)?;
344 let section_index = Self::read_section_index(&buffer, &footer)?;
345 let header = Self::read_header(&buffer)?;
346
347 Ok(Self {
348 buffer: Some(buffer),
349 loader: None,
350 length: (HEADER_SIZE + FOOTER_SIZE + section_index.serialized_size()) as u64,
351 header,
352 footer,
353 section_index,
354 config,
355 })
356 }
357
358 fn from_indexed_db(length: u64, mut config: WasmReaderConfig) -> Result<Self, FormatError> {
359 let loader = config
360 .range_loader
361 .take()
362 .ok_or(FormatError::IncompleteWrite)?;
363
364 if (length as usize) <= config.full_load_threshold_bytes {
366 let full = loader.load_range(0, length)?;
367 return Self::from_buffer(
368 full,
369 WasmReaderConfig {
370 range_loader: Some(loader),
371 ..config
372 },
373 );
374 }
375
376 let footer_start = length
378 .checked_sub(FOOTER_SIZE as u64)
379 .ok_or(FormatError::IncompleteWrite)?;
380 let footer_bytes = loader.load_range(footer_start, FOOTER_SIZE as u64)?;
381 let footer_array: [u8; FOOTER_SIZE] = footer_bytes
382 .try_into()
383 .map_err(|_| FormatError::IncompleteWrite)?;
384 let footer = FileFooter::from_bytes(&footer_array)?;
385
386 let index_offset = footer.section_index_offset;
388 let count_bytes = loader.load_range(index_offset, 4)?;
390 let count_arr: [u8; 4] = count_bytes
391 .try_into()
392 .map_err(|_| FormatError::IncompleteWrite)?;
393 let count = u32::from_le_bytes(count_arr);
394 let total_size = 4 + count as usize * SectionEntry::SIZE;
395 let index_bytes = loader.load_range(index_offset, total_size as u64)?;
396 let section_index = SectionIndex::from_bytes(&index_bytes)?;
397
398 let header_bytes = loader.load_range(0, HEADER_SIZE as u64)?;
400 let header_array: [u8; HEADER_SIZE] = header_bytes
401 .try_into()
402 .map_err(|_| FormatError::IncompleteWrite)?;
403 let header = FileHeader::from_bytes(&header_array)?;
404 header.check_compatibility(&FileVersion::CURRENT)?;
405
406 Ok(Self {
407 buffer: None,
408 loader: Some(loader),
409 length,
410 header,
411 footer,
412 section_index,
413 config,
414 })
415 }
416
417 fn read_footer(buffer: &[u8]) -> Result<FileFooter, FormatError> {
418 if buffer.len() < FOOTER_SIZE {
419 return Err(FormatError::IncompleteWrite);
420 }
421 let start = buffer.len() - FOOTER_SIZE;
422 let mut buf = [0u8; FOOTER_SIZE];
423 buf.copy_from_slice(&buffer[start..]);
424 FileFooter::from_bytes(&buf)
425 }
426
427 fn read_header(buffer: &[u8]) -> Result<FileHeader, FormatError> {
428 if buffer.len() < HEADER_SIZE {
429 return Err(FormatError::IncompleteWrite);
430 }
431 let mut buf = [0u8; HEADER_SIZE];
432 buf.copy_from_slice(&buffer[..HEADER_SIZE]);
433 let header = FileHeader::from_bytes(&buf)?;
434 header.check_compatibility(&FileVersion::CURRENT)?;
435 Ok(header)
436 }
437
438 fn read_section_index(buffer: &[u8], footer: &FileFooter) -> Result<SectionIndex, FormatError> {
439 let offset = footer.section_index_offset as usize;
440 if buffer.len() < offset + 4 {
441 return Err(FormatError::IncompleteWrite);
442 }
443 let count = u32::from_le_bytes(
444 buffer[offset..offset + 4]
445 .try_into()
446 .expect("slice length checked"),
447 );
448 let expected = 4usize + count as usize * SectionEntry::SIZE;
449 if buffer.len() < offset + expected {
450 return Err(FormatError::IncompleteWrite);
451 }
452 SectionIndex::from_bytes(&buffer[offset..offset + expected])
453 }
454
455 fn entry(&self, section_id: u32) -> Result<&SectionEntry, FormatError> {
456 self.section_index
457 .find_by_id(section_id)
458 .ok_or(FormatError::IncompleteWrite)
459 }
460}
461
462#[cfg(target_arch = "wasm32")]
463impl FileReader for AlopexFileReader {
464 fn open(source: FileSource) -> Result<Self, FormatError>
465 where
466 Self: Sized,
467 {
468 Self::open_with_config(source, WasmReaderConfig::default())
469 }
470
471 fn header(&self) -> &FileHeader {
472 &self.header
473 }
474
475 fn footer(&self) -> &FileFooter {
476 &self.footer
477 }
478
479 fn section_index(&self) -> &SectionIndex {
480 &self.section_index
481 }
482
483 fn read_section(&self, section_id: u32) -> Result<Vec<u8>, FormatError> {
484 let entry = self.entry(section_id)?;
485 let raw = self.read_section_raw(section_id)?;
486 if raw.len() as u64 != entry.compressed_length {
487 return Err(FormatError::IncompleteWrite);
488 }
489 checksum::verify(&raw, self.header.checksum_algorithm, entry.checksum as u64)?;
490 let decompressed = compression::decompress(&raw, entry.compression)?;
491 if decompressed.len() as u64 != entry.uncompressed_length {
492 return Err(FormatError::IncompleteWrite);
493 }
494 Ok(decompressed)
495 }
496
497 fn read_section_raw(&self, section_id: u32) -> Result<Vec<u8>, FormatError> {
498 let entry = self.entry(section_id)?;
499 match &self.buffer {
500 Some(buf) => {
501 let offset = entry.offset as usize;
502 let end = offset
503 .checked_add(entry.compressed_length as usize)
504 .ok_or(FormatError::IncompleteWrite)?;
505 if end > buf.len() || end - offset != entry.compressed_length as usize {
506 return Err(FormatError::IncompleteWrite);
507 }
508 Ok(buf[offset..end].to_vec())
509 }
510 None => {
511 let loader = self.loader.as_ref().ok_or(FormatError::IncompleteWrite)?;
512 loader.load_range(entry.offset, entry.compressed_length)
513 }
514 }
515 }
516
517 fn validate_section(&self, section_id: u32) -> Result<(), FormatError> {
518 let entry = self.entry(section_id)?;
519 let raw = self.read_section_raw(section_id)?;
520 checksum::verify(&raw, self.header.checksum_algorithm, entry.checksum as u64)
521 }
522
523 fn validate_all(&self) -> Result<(), FormatError> {
524 for entry in &self.section_index.entries {
525 self.validate_section(entry.section_id)?;
526 }
527 Ok(())
528 }
529
530 fn prefetch_sections<'a>(&'a self, _section_ids: &'a [u32]) -> PrefetchFuture<'a> {
531 let section_ids = _section_ids.to_vec();
532 Box::pin(async move {
533 for id in section_ids {
534 let _ = self.read_section_raw(id)?;
535 }
536 Ok(())
537 })
538 }
539}