···114114115115ptrdiff_t bindle_reader_read(struct BindleReader *reader, uint8_t *buffer, size_t buffer_len);
116116117117+/**
118118+ * Verify the CRC32 of data read from the reader.
119119+ * Should be called after reading all data to ensure integrity.
120120+ * Returns true if CRC32 matches, false otherwise.
121121+ */
122122+bool bindle_reader_verify_crc32(const struct BindleReader *reader);
123123+117124void bindle_reader_close(struct BindleReader *reader);
118125119126#endif /* BINDLE_H */
+13
src/ffi.rs
···380380 }
381381}
382382383383+/// Verify the CRC32 of data read from the reader.
384384+/// Should be called after reading all data to ensure integrity.
385385+/// Returns true if CRC32 matches, false otherwise.
386386+#[unsafe(no_mangle)]
387387+pub unsafe extern "C" fn bindle_reader_verify_crc32(reader: *const Reader) -> bool {
388388+ if reader.is_null() {
389389+ return false;
390390+ }
391391+392392+ let r = unsafe { &*reader };
393393+ r.verify_crc32().is_ok()
394394+}
395395+383396#[unsafe(no_mangle)]
384397pub unsafe extern "C" fn bindle_reader_close(reader: *mut Reader) {
385398 if !reader.is_null() {
+137-10
src/lib.rs
···11+use crc32fast::Hasher;
12use fs2::FileExt;
23use memmap2::Mmap;
34use std::borrow::Cow;
···7980 _ => Compress::default(),
8081 }
8182 }
8383+8484+ pub fn crc32(&self) -> u32 {
8585+ u32::from_le_bytes(self.crc32)
8686+ }
8287}
83888489#[repr(C, packed)]
···104109105110pub struct Reader<'a> {
106111 decoder: Either<zstd::Decoder<'static, BufReader<io::Cursor<&'a [u8]>>>, io::Cursor<&'a [u8]>>,
112112+ crc32_hasher: Hasher,
113113+ expected_crc32: u32,
107114}
108115109116impl<'a> Read for Reader<'a> {
110117 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
111111- match &mut self.decoder {
112112- Either::Left(x) => x.read(buf),
113113- Either::Right(x) => x.read(buf),
118118+ let n = match &mut self.decoder {
119119+ Either::Left(x) => x.read(buf)?,
120120+ Either::Right(x) => x.read(buf)?,
121121+ };
122122+123123+ if n > 0 {
124124+ self.crc32_hasher.update(&buf[..n]);
114125 }
126126+127127+ Ok(n)
115128 }
116129}
117130···129142 }
130143}
131144145145+impl<'a> Reader<'a> {
146146+ /// Verify the CRC32 of the data read so far.
147147+ /// This should be called after all data has been read to ensure data integrity.
148148+ pub fn verify_crc32(&self) -> io::Result<()> {
149149+ let computed_crc = self.crc32_hasher.clone().finalize();
150150+ if computed_crc != self.expected_crc32 {
151151+ return Err(io::Error::new(
152152+ io::ErrorKind::InvalidData,
153153+ format!("CRC32 mismatch: expected {:x}, got {:x}", self.expected_crc32, computed_crc),
154154+ ));
155155+ }
156156+ Ok(())
157157+ }
158158+}
159159+132160pub struct Writer<'a> {
133161 pub(crate) bindle: &'a mut Bindle,
134162 pub(crate) encoder: Option<zstd::Encoder<'a, std::fs::File>>,
135163 pub(crate) name: String,
136164 pub(crate) start_offset: u64,
137165 pub(crate) uncompressed_size: u64,
166166+ pub(crate) crc32_hasher: Hasher,
138167}
139168140169impl<'a> Drop for Writer<'a> {
···161190 }
162191163192 self.uncompressed_size += data.len() as u64;
193193+ self.crc32_hasher.update(data);
164194165195 if let Some(encoder) = &mut self.encoder {
166196 encoder.write_all(data)?;
···197227198228 self.bindle.data_end = current_pos + pad_len;
199229230230+ let crc32 = self.crc32_hasher.clone().finalize();
231231+200232 let entry = Entry {
201233 offset: self.start_offset.to_le_bytes(),
202234 compressed_size: compressed_size.to_le_bytes(),
203235 uncompressed_size: self.uncompressed_size.to_le_bytes(),
236236+ crc32: crc32.to_le_bytes(),
204237 compression_type,
205238 name_len: (self.name.len() as u16).to_le_bytes(),
206239 ..Default::default()
···460493 let entry = self.index.get(name)?;
461494 let mmap = self.mmap.as_ref()?;
462495463463- if entry.compression_type == Compress::Zstd as u8 {
464464- let data = mmap.get(
496496+ let data = if entry.compression_type == Compress::Zstd as u8 {
497497+ let compressed_data = mmap.get(
465498 entry.offset() as usize..(entry.offset() + entry.compressed_size()) as usize,
466499 )?;
467500 let mut out = Vec::with_capacity(entry.uncompressed_size() as usize);
468468- zstd::Decoder::new(data).ok()?.read_to_end(&mut out).ok()?;
469469- Some(Cow::Owned(out))
501501+ zstd::Decoder::new(compressed_data).ok()?.read_to_end(&mut out).ok()?;
502502+ Cow::Owned(out)
470503 } else {
471471- let data = mmap.get(
504504+ let uncompressed_data = mmap.get(
472505 entry.offset() as usize..(entry.offset() + entry.uncompressed_size()) as usize,
473506 )?;
474474- Some(Cow::Borrowed(data))
507507+ Cow::Borrowed(uncompressed_data)
508508+ };
509509+510510+ // Verify CRC32
511511+ let computed_crc = crc32fast::hash(&data);
512512+ if computed_crc != entry.crc32() {
513513+ return None;
475514 }
515515+516516+ Some(data)
476517 }
477518478519 /// Read to an `std::io::Write`
479520 pub fn read_to<W: std::io::Write>(&self, name: &str, mut w: W) -> std::io::Result<u64> {
480480- std::io::copy(&mut self.reader(name)?, &mut w)
521521+ let mut reader = self.reader(name)?;
522522+ let bytes_copied = std::io::copy(&mut reader, &mut w)?;
523523+ reader.verify_crc32()?;
524524+ Ok(bytes_copied)
481525 }
482526483527 // Returns a seekable reader for an entry.
···503547 let decoder = zstd::Decoder::new(cursor)?;
504548 Ok(Reader {
505549 decoder: Either::Left(decoder),
550550+ crc32_hasher: Hasher::new(),
551551+ expected_crc32: entry.crc32(),
506552 })
507553 } else {
508554 Ok(Reader {
509555 decoder: Either::Right(cursor),
556556+ crc32_hasher: Hasher::new(),
557557+ expected_crc32: entry.crc32(),
510558 })
511559 }
512560 }
···596644 },
597645 start_offset,
598646 uncompressed_size: 0,
647647+ crc32_hasher: Hasher::new(),
599648 })
600649 }
601650}
···839888 let result = b.read("streamed_file.txt").expect("Entry not found");
840889 assert_eq!(result.as_ref(), expected);
841890 assert_eq!(result.len(), expected.len());
891891+892892+ let _ = std::fs::remove_file(path);
893893+ }
894894+895895+ #[test]
896896+ fn test_crc32_corruption_detection() {
897897+ let path = "test_crc32.bindl";
898898+ let _ = std::fs::remove_file(path);
899899+ let data = b"Test data for CRC32 verification";
900900+901901+ // 1. Create a file with valid data
902902+ {
903903+ let mut b = Bindle::open(path).expect("Failed to open");
904904+ b.add("test.txt", data, Compress::None).unwrap();
905905+ b.save().unwrap();
906906+ }
907907+908908+ // 2. Verify that reading with correct data works
909909+ {
910910+ let b = Bindle::open(path).expect("Failed to reopen");
911911+ let result = b.read("test.txt").expect("Should read successfully");
912912+ assert_eq!(result.as_ref(), data);
913913+ }
914914+915915+ // 3. Corrupt the data by modifying a byte directly in the file
916916+ {
917917+ use std::io::{Seek, SeekFrom, Write};
918918+ let mut file = OpenOptions::new()
919919+ .write(true)
920920+ .read(true)
921921+ .open(path)
922922+ .unwrap();
923923+924924+ // Skip the header and modify the first byte of data
925925+ file.seek(SeekFrom::Start(HEADER_SIZE as u64)).unwrap();
926926+ file.write_all(b"X").unwrap(); // Corrupt first byte
927927+ file.flush().unwrap();
928928+ }
929929+930930+ // 4. Verify that reading corrupted data fails CRC32 check
931931+ {
932932+ let b = Bindle::open(path).expect("Failed to reopen after corruption");
933933+ let result = b.read("test.txt");
934934+ assert!(result.is_none(), "Read should fail due to CRC32 mismatch");
935935+ }
936936+937937+ let _ = std::fs::remove_file(path);
938938+ }
939939+940940+ #[test]
941941+ fn test_crc32_with_compression() {
942942+ let path = "test_crc32_compressed.bindl";
943943+ let _ = std::fs::remove_file(path);
944944+ let data = vec![b'A'; 2000]; // Large enough to trigger compression
945945+946946+ // 1. Create a file with compressed data
947947+ {
948948+ let mut b = Bindle::open(path).expect("Failed to open");
949949+ b.add("compressed.bin", &data, Compress::Zstd).unwrap();
950950+ b.save().unwrap();
951951+ }
952952+953953+ // 2. Verify that reading compressed data works and CRC32 is verified
954954+ {
955955+ let b = Bindle::open(path).expect("Failed to reopen");
956956+ let result = b.read("compressed.bin").expect("Should read successfully");
957957+ assert_eq!(result.as_ref(), data.as_slice());
958958+ }
959959+960960+ // 3. Also test with the streaming reader
961961+ {
962962+ let b = Bindle::open(path).expect("Failed to reopen");
963963+ let mut reader = b.reader("compressed.bin").unwrap();
964964+ let mut output = Vec::new();
965965+ std::io::copy(&mut reader, &mut output).unwrap();
966966+ reader.verify_crc32().expect("CRC32 should match");
967967+ assert_eq!(output, data);
968968+ }
842969843970 let _ = std::fs::remove_file(path);
844971 }