···1212#include <stdint.h>
1313#include <stdbool.h>
14141515+/**
1616+ * Compression mode for entries.
1717+ */
1518enum BindleCompress {
1919+ /**
2020+ * No compression.
2121+ */
1622 BindleCompressNone = 0,
2323+ /**
2424+ * Zstandard compression.
2525+ */
1726 BindleCompressZstd = 1,
2727+ /**
2828+ * Automatically compress if entry is larger than 2KB threshold.
2929+ * Note: This is never stored on disk, only used as a policy hint.
3030+ */
1831 BindleCompressAuto = 2,
1932};
2033typedef uint8_t BindleCompress;
21343535+/**
3636+ * A binary archive for collecting files.
3737+ *
3838+ * Uses memory-mapped I/O for fast reads, supports optional zstd compression, and handles updates via shadowing.
3939+ * Files can be added incrementally without rewriting the entire archive.
4040+ *
4141+ * # Example
4242+ *
4343+ * ```no_run
4444+ * use bindle_file::{Bindle, Compress};
4545+ *
4646+ * let mut archive = Bindle::open("data.bndl")?;
4747+ * archive.add("file.txt", b"data", Compress::None)?;
4848+ * archive.save()?;
4949+ * # Ok::<(), std::io::Error>(())
5050+ * ```
5151+ */
2252typedef struct Bindle Bindle;
23535454+/**
5555+ * A streaming reader for archive entries.
5656+ *
5757+ * Created by the archive's `reader()` method. Automatically decompresses compressed entries and tracks CRC32 for integrity verification.
5858+ *
5959+ * # Example
6060+ *
6161+ * ```no_run
6262+ * # use bindle_file::Bindle;
6363+ * # let archive = Bindle::open("data.bndl")?;
6464+ * let mut reader = archive.reader("file.txt")?;
6565+ * std::io::copy(&mut reader, &mut std::io::stdout())?;
6666+ * reader.verify_crc32()?;
6767+ * # Ok::<(), std::io::Error>(())
6868+ * ```
6969+ */
2470typedef struct BindleReader BindleReader;
25717272+/**
7373+ * A streaming writer for adding entries to an archive.
7474+ *
7575+ * Created by [`Bindle::writer()`]. Automatically compresses data if requested and computes CRC32 for integrity verification.
7676+ *
7777+ * The writer must be closed with [`close()`](Writer::close) or will be automatically closed when dropped. After closing, call [`Bindle::save()`] to commit the index.
7878+ *
7979+ * # Example
8080+ *
8181+ * ```no_run
8282+ * use std::io::Write;
8383+ * use bindle_file::{Bindle, Compress};
8484+ *
8585+ * let mut archive = Bindle::open("data.bndl")?;
8686+ * let mut writer = archive.writer("file.txt", Compress::None)?;
8787+ * writer.write_all(b"data")?;
8888+ * writer.close()?;
8989+ * archive.save()?;
9090+ * # Ok::<(), std::io::Error>(())
9191+ * ```
9292+ */
2693typedef struct BindleWriter BindleWriter;
27942895/**
+68-22
src/bindle.rs
···1313use crate::reader::{Either, Reader};
1414use crate::writer::Writer;
1515use crate::{
1616- pad, write_padding, AUTO_COMPRESS_THRESHOLD, BNDL_ALIGN, BNDL_MAGIC, ENTRY_SIZE, FOOTER_MAGIC,
1717- FOOTER_SIZE, HEADER_SIZE,
1616+ AUTO_COMPRESS_THRESHOLD, BNDL_ALIGN, BNDL_MAGIC, ENTRY_SIZE, FOOTER_MAGIC, FOOTER_SIZE,
1717+ HEADER_SIZE, pad, write_padding,
1818};
19192020+/// A binary archive for collecting files.
2121+///
2222+/// Uses memory-mapped I/O for fast reads, supports optional zstd compression, and handles updates via shadowing.
2323+/// Files can be added incrementally without rewriting the entire archive.
2424+///
2525+/// # Example
2626+///
2727+/// ```no_run
2828+/// use bindle_file::{Bindle, Compress};
2929+///
3030+/// let mut archive = Bindle::open("data.bndl")?;
3131+/// archive.add("file.txt", b"data", Compress::None)?;
3232+/// archive.save()?;
3333+/// # Ok::<(), std::io::Error>(())
3434+/// ```
2035pub struct Bindle {
2136 pub(crate) path: PathBuf,
2237 pub(crate) file: File,
···2641}
27422843impl Bindle {
2929- /// Create a new bindle file, this will overwrite the existing file
4444+ /// Creates a new archive, overwriting any existing file at the path.
3045 pub fn create<P: AsRef<Path>>(path: P) -> io::Result<Self> {
3146 let path_buf = path.as_ref().to_path_buf();
3247 let opts = OpenOptions::new()
···3853 Self::new(path_buf, opts)
3954 }
40554141- /// Open or create a bindle file
5656+ /// Opens an existing archive or creates a new one if it doesn't exist.
4257 pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
4358 let path_buf = path.as_ref().to_path_buf();
4459 let opts = OpenOptions::new()
···4964 Self::new(path_buf, opts)
5065 }
51665252- /// Open a bindle file, this will not create it if it doesn't exist
6767+ /// Opens an existing archive. Returns an error if the file doesn't exist.
5368 pub fn load<P: AsRef<Path>>(path: P) -> io::Result<Self> {
5469 let path_buf = path.as_ref().to_path_buf();
5570 let opts = OpenOptions::new().read(true).write(true).to_owned();
···9310894109 // Calculate footer position. Subtraction is now safe due to the check above.
95110 let footer_pos = m.len() - FOOTER_SIZE;
9696- let footer = Footer::read_from_bytes(&m[footer_pos..]).map_err(|_| {
9797- io::Error::new(io::ErrorKind::InvalidData, "Failed to read footer")
9898- })?;
111111+ let footer = Footer::read_from_bytes(&m[footer_pos..])
112112+ .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Failed to read footer"))?;
99113100114 if footer.magic() != FOOTER_MAGIC {
101115 return Err(io::Error::new(
···147161 compress == Compress::Zstd || (compress == Compress::Auto && len > AUTO_COMPRESS_THRESHOLD)
148162 }
149163164164+ /// Adds data to the archive with the given name.
165165+ ///
166166+ /// If an entry with the same name exists, it will be shadowed. Call [`save()`](Bindle::save) to commit changes.
150167 pub fn add(&mut self, name: &str, data: &[u8], compress: Compress) -> io::Result<()> {
151168 let mut stream = self.writer(name, compress)?;
152169 stream.write_all(data)?;
···154171 Ok(())
155172 }
156173174174+ /// Adds a file from the filesystem to the archive.
175175+ ///
176176+ /// Reads the file at `path` and stores it with the given `name`. Call [`save()`](Bindle::save) to commit changes.
157177 pub fn add_file(
158178 &mut self,
159179 name: &str,
···166186 Ok(())
167187 }
168188189189+ /// Commits all pending changes by writing the index and footer to disk.
190190+ ///
191191+ /// Must be called after add/remove operations to make changes persistent.
169192 pub fn save(&mut self) -> io::Result<()> {
170193 self.file.lock_exclusive()?;
171194 self.file.seek(SeekFrom::Start(self.data_end))?;
···193216 Ok(())
194217 }
195218219219+ /// Reclaims space by removing shadowed data.
220220+ ///
221221+ /// Rebuilds the archive with only live entries, removing old versions of updated files.
196222 pub fn vacuum(&mut self) -> io::Result<()> {
197223 let temp_path = self.path.with_extension("tmp");
198224···256282257283 let footer_pos = mmap.len() - FOOTER_SIZE;
258284 let footer = Footer::read_from_bytes(&mmap[footer_pos..]).map_err(|_| {
259259- io::Error::new(io::ErrorKind::InvalidData, "Failed to read footer after vacuum")
285285+ io::Error::new(
286286+ io::ErrorKind::InvalidData,
287287+ "Failed to read footer after vacuum",
288288+ )
260289 })?;
261290262291 self.file = temp_file;
···266295 Ok(())
267296 }
268297298298+ /// Reads an entry from the archive, decompressing if needed.
299299+ ///
300300+ /// Returns `None` if the entry doesn't exist or if CRC32 verification fails.
269301 pub fn read<'a>(&'a self, name: &str) -> Option<Cow<'a, [u8]>> {
270302 let entry = self.index.get(name)?;
271303 let mmap = self.mmap.as_ref()?;
···296328 Some(data)
297329 }
298330299299- /// Read to an `std::io::Write`
331331+ /// Reads an entry and writes it to the given writer.
332332+ ///
333333+ /// Returns the number of bytes written. Verifies CRC32 after reading.
300334 pub fn read_to<W: std::io::Write>(&self, name: &str, mut w: W) -> std::io::Result<u64> {
301335 let mut reader = self.reader(name)?;
302336 let bytes_copied = std::io::copy(&mut reader, &mut w)?;
···304338 Ok(bytes_copied)
305339 }
306340307307- // Returns a seekable reader for an entry.
308308- /// If compressed, it provides a transparently decompressing stream.
341341+ /// Returns a streaming reader for an entry.
342342+ ///
343343+ /// Automatically decompresses if the entry is compressed. Call [`Reader::verify_crc32()`] after reading to verify integrity.
309344 pub fn reader<'a>(&'a self, name: &str) -> io::Result<Reader<'a>> {
310345 let entry = self
311346 .index
···339374 }
340375 }
341376342342- /// The number of entries
377377+ /// Returns the number of entries in the archive.
343378 pub fn len(&self) -> usize {
344379 self.index.len()
345380 }
346381347347- /// Returns true if there are no entries
382382+ /// Returns true if the archive contains no entries.
348383 pub fn is_empty(&self) -> bool {
349384 self.index.is_empty()
350385 }
351386352352- /// Direct readonly access to the index
387387+ /// Returns a reference to the archive index.
388388+ ///
389389+ /// The index maps entry names to their metadata.
353390 pub fn index(&self) -> &BTreeMap<String, Entry> {
354391 &self.index
355392 }
356393357357- /// Clear all entries
394394+ /// Removes all entries from the index.
395395+ ///
396396+ /// Call [`save()`](Bindle::save) to commit. Data remains in the file until [`vacuum()`](Bindle::vacuum) is called.
358397 pub fn clear(&mut self) {
359398 self.index.clear()
360399 }
361400362362- /// Checks if an entry exists in the archive index.
401401+ /// Returns true if an entry with the given name exists.
363402 pub fn exists(&self, name: &str) -> bool {
364403 self.index.contains_key(name)
365404 }
366405367367- /// Remove an entry from the index.
368368- /// The data remains in the file until vacuum() is called.
369369- /// Returns true if the entry existed and was removed.
406406+ /// Removes an entry from the index.
407407+ ///
408408+ /// Returns true if the entry existed. Data remains in the file until [`vacuum()`](Bindle::vacuum) is called.
370409 pub fn remove(&mut self, name: &str) -> bool {
371410 self.index.remove(name).is_some()
372411 }
373412374374- /// Recursively packs a directory into the archive.
413413+ /// Recursively adds all files from a directory to the archive.
414414+ ///
415415+ /// File paths are stored relative to the source directory. Call [`save()`](Bindle::save) to commit.
375416 pub fn pack<P: AsRef<Path>>(&mut self, src_dir: P, compress: Compress) -> io::Result<()> {
376417 self.pack_recursive(src_dir.as_ref(), src_dir.as_ref(), compress)
377418 }
···398439 Ok(())
399440 }
400441401401- /// Unpacks all archive entries to a destination directory.
442442+ /// Extracts all entries to a destination directory.
443443+ ///
444444+ /// Creates subdirectories as needed to match the stored paths.
402445 pub fn unpack<P: AsRef<Path>>(&self, dest: P) -> io::Result<()> {
403446 let dest_path = dest.as_ref();
404447 if let Some(parent) = dest_path.parent() {
···416459 Ok(())
417460 }
418461462462+ /// Creates a streaming writer for adding an entry.
463463+ ///
464464+ /// The writer must be closed and then [`save()`](Bindle::save) must be called to commit the entry.
419465 pub fn writer<'a>(&'a mut self, name: &str, compress: Compress) -> io::Result<Writer<'a>> {
420466 self.file.lock_exclusive()?;
421467 self.file.seek(SeekFrom::Start(self.data_end))?;
+5
src/compress.rs
···11+/// Compression mode for entries.
12#[repr(u8)]
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
34pub enum Compress {
55+ /// No compression.
46 None = 0,
77+ /// Zstandard compression.
58 Zstd = 1,
99+ /// Automatically compress if entry is larger than 2KB threshold.
1010+ /// Note: This is never stored on disk, only used as a policy hint.
611 #[default]
712 Auto = 2,
813}
+15-5
src/entry.rs
···2233use crate::compress::Compress;
4455+/// Metadata for an entry in the archive.
66+///
77+/// Contains information about stored files including offset, size, compression, and CRC32 checksum.
88+/// Retrieved via the archive's `index()` method.
59#[repr(C, packed)]
610#[derive(FromBytes, Unaligned, IntoBytes, Immutable, Clone, Copy, Debug, Default)]
711pub struct Entry {
···2024// - On big-endian systems: bytes are swapped to/from little-endian
21252226impl Entry {
2727+ /// Returns the byte offset where this entry's data starts in the archive.
2328 pub fn offset(&self) -> u64 {
2429 u64::from_le(self.offset)
2530 }
26312727- pub fn set_offset(&mut self, value: u64) {
3232+ pub(crate) fn set_offset(&mut self, value: u64) {
2833 self.offset = value.to_le();
2934 }
30353636+ /// Returns the compressed size of this entry in bytes.
3137 pub fn compressed_size(&self) -> u64 {
3238 u64::from_le(self.compressed_size)
3339 }
34403535- pub fn set_compressed_size(&mut self, value: u64) {
4141+ pub(crate) fn set_compressed_size(&mut self, value: u64) {
3642 self.compressed_size = value.to_le();
3743 }
38444545+ /// Returns the uncompressed size of this entry in bytes.
3946 pub fn uncompressed_size(&self) -> u64 {
4047 u64::from_le(self.uncompressed_size)
4148 }
42494343- pub fn set_uncompressed_size(&mut self, value: u64) {
5050+ pub(crate) fn set_uncompressed_size(&mut self, value: u64) {
4451 self.uncompressed_size = value.to_le();
4552 }
46535454+ /// Returns the CRC32 checksum of the uncompressed data.
4755 pub fn crc32(&self) -> u32 {
4856 u32::from_le(self.crc32)
4957 }
50585151- pub fn set_crc32(&mut self, value: u32) {
5959+ pub(crate) fn set_crc32(&mut self, value: u32) {
5260 self.crc32 = value.to_le();
5361 }
54626363+ /// Returns the length of the entry name in bytes.
5564 pub fn name_len(&self) -> usize {
5665 u16::from_le(self.name_len) as usize
5766 }
58675959- pub fn set_name_len(&mut self, value: u16) {
6868+ pub(crate) fn set_name_len(&mut self, value: u16) {
6069 self.name_len = value.to_le();
6170 }
62717272+ /// Returns the compression type for this entry.
6373 pub fn compression_type(&self) -> Compress {
6474 Compress::from_u8(self.compression_type)
6575 }
+18
src/lib.rs
···11+//! Bindle is a binary archive format for collecting files.
22+//!
33+//! The format uses memory-mapped I/O for fast reads, optional zstd compression,
44+//! and supports append-only writes with shadowing for updates.
55+//!
66+//! # Example
77+//!
88+//! ```no_run
99+//! use bindle_file::{Bindle, Compress};
1010+//!
1111+//! let mut archive = Bindle::open("data.bndl")?;
1212+//! archive.add("file.txt", b"data", Compress::None)?;
1313+//! archive.save()?;
1414+//!
1515+//! let data = archive.read("file.txt").unwrap();
1616+//! # Ok::<(), std::io::Error>(())
1717+//! ```
1818+119use std::io::{self, Write};
220321// Module declarations
+21-4
src/reader.rs
···11use crc32fast::Hasher;
22use std::io::{self, BufReader, Read, Seek, SeekFrom};
3344-pub enum Either<A, B> {
44+pub(crate) enum Either<A, B> {
55 Left(A),
66 Right(B),
77}
8899+/// A streaming reader for archive entries.
1010+///
1111+/// Created by the archive's `reader()` method. Automatically decompresses compressed entries and tracks CRC32 for integrity verification.
1212+///
1313+/// # Example
1414+///
1515+/// ```no_run
1616+/// # use bindle_file::Bindle;
1717+/// # let archive = Bindle::open("data.bndl")?;
1818+/// let mut reader = archive.reader("file.txt")?;
1919+/// std::io::copy(&mut reader, &mut std::io::stdout())?;
2020+/// reader.verify_crc32()?;
2121+/// # Ok::<(), std::io::Error>(())
2222+/// ```
923pub struct Reader<'a> {
1010- pub(crate) decoder: Either<zstd::Decoder<'static, BufReader<io::Cursor<&'a [u8]>>>, io::Cursor<&'a [u8]>>,
2424+ pub(crate) decoder:
2525+ Either<zstd::Decoder<'static, BufReader<io::Cursor<&'a [u8]>>>, io::Cursor<&'a [u8]>>,
1126 pub(crate) crc32_hasher: Hasher,
1227 pub(crate) expected_crc32: u32,
1328}
···4257}
43584459impl<'a> Reader<'a> {
4545- /// Verify the CRC32 of the data read so far.
4646- /// This should be called after all data has been read to ensure data integrity.
6060+ /// Verifies the CRC32 checksum of the data read so far.
6161+ ///
6262+ /// Should be called after reading all data to ensure integrity.
6363+ /// Returns an error if the computed CRC32 doesn't match the expected value.
4764 pub fn verify_crc32(&self) -> io::Result<()> {
4865 let computed_crc = self.crc32_hasher.clone().finalize();
4966 if computed_crc != self.expected_crc32 {
+22
src/writer.rs
···44use crate::bindle::Bindle;
55use crate::entry::Entry;
6677+/// A streaming writer for adding entries to an archive.
88+///
99+/// Created by [`Bindle::writer()`]. Automatically compresses data if requested and computes CRC32 for integrity verification.
1010+///
1111+/// The writer must be closed with [`close()`](Writer::close) or will be automatically closed when dropped. After closing, call [`Bindle::save()`] to commit the index.
1212+///
1313+/// # Example
1414+///
1515+/// ```no_run
1616+/// use std::io::Write;
1717+/// use bindle_file::{Bindle, Compress};
1818+///
1919+/// let mut archive = Bindle::open("data.bndl")?;
2020+/// let mut writer = archive.writer("file.txt", Compress::None)?;
2121+/// writer.write_all(b"data")?;
2222+/// writer.close()?;
2323+/// archive.save()?;
2424+/// # Ok::<(), std::io::Error>(())
2525+/// ```
726pub struct Writer<'a> {
827 pub(crate) bindle: &'a mut Bindle,
928 pub(crate) encoder: Option<zstd::Encoder<'a, std::fs::File>>,
···92111 Ok(())
93112 }
94113114114+ /// Closes the writer and finalizes the entry.
115115+ ///
116116+ /// Automatically called when the writer is dropped, but calling explicitly allows error handling.
95117 pub fn close(mut self) -> io::Result<()> {
96118 self.close_drop()
97119 }