···11+/* Auto-generated by cbindgen - do not edit manually */
22+33+#ifndef BINDLE_H
44+#define BINDLE_H
55+66+#include <stdarg.h>
77+#include <stdbool.h>
88+#include <stddef.h>
99+#include <stdint.h>
1010+#include <stdlib.h>
1111+#include <stddef.h>
1212+#include <stdint.h>
1313+#include <stdbool.h>
1414+1515+/**
1616+ * Opaque handle to a Bindle archive.
1717+ */
1818+typedef struct BindleContext BindleContext;
1919+2020+struct BindleContext *bindle_open(const char *path);
2121+2222+/**
2323+ * Adds a new entry. Returns true on success.
2424+ */
2525+bool bindle_add(struct BindleContext *ctx,
2626+ const char *name,
2727+ const uint8_t *data,
2828+ size_t data_len,
2929+ bool compress);
3030+3131+/**
3232+ * Commits changes to disk.
3333+ */
3434+bool bindle_save(struct BindleContext *ctx);
3535+3636+/**
3737+ * Frees BindleContext
3838+ */
3939+void bindle_free(struct BindleContext *ctx);
4040+4141+uint8_t *bindle_read(struct BindleContext *ctx, const char *name, size_t *out_len);
4242+4343+void bindle_free_buffer(uint8_t *ptr, size_t len);
4444+4545+size_t bindle_length(const struct BindleContext *ctx);
4646+4747+/**
4848+ * Returns the name of the entry at the given index.
4949+ * The string is owned by the Bindle; the caller must NOT free it.
5050+ */
5151+const char *bindle_entry_name(const struct BindleContext *ctx, size_t index, size_t *len);
5252+5353+#endif /* BINDLE_H */
+16
build.rs
···11+use std::env;
22+use std::path::PathBuf;
33+44+fn main() {
55+ let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
66+77+ // Instead of using .generate(), use the Builder for more control
88+ let config = cbindgen::Config::from_file("cbindgen.toml").unwrap_or_default();
99+1010+ cbindgen::Builder::new()
1111+ .with_crate(&crate_dir)
1212+ .with_config(config)
1313+ .generate()
1414+ .expect("Unable to generate bindings")
1515+ .write_to_file(PathBuf::from(crate_dir).join("bindle.h"));
1616+}
+6
cbindgen.toml
···11+language = "C"
22+header = "/* Auto-generated by cbindgen - do not edit manually */"
33+include_guard = "BINDLE_H"
44+sys_includes = ["stddef.h", "stdint.h", "stdbool.h"]
55+usize_is_size_t = true
66+
+149
src/ffi.rs
···11+use std::ffi::CStr;
22+use std::os::raw::c_char;
33+use std::slice;
44+55+use crate::Bindle;
66+77+/// Opaque handle to a Bindle archive.
88+pub struct BindleContext {
99+ pub(crate) inner: Bindle,
1010+}
1111+1212+#[unsafe(no_mangle)]
1313+pub unsafe extern "C" fn bindle_open(path: *const c_char) -> *mut BindleContext {
1414+ if path.is_null() {
1515+ return std::ptr::null_mut();
1616+ }
1717+1818+ // Explicit unsafe block for raw pointer dereference
1919+ let path_str = unsafe {
2020+ match CStr::from_ptr(path).to_str() {
2121+ Ok(s) => s,
2222+ Err(_) => return std::ptr::null_mut(),
2323+ }
2424+ };
2525+2626+ match Bindle::open(path_str) {
2727+ Ok(b) => Box::into_raw(Box::new(BindleContext { inner: b })),
2828+ Err(_) => std::ptr::null_mut(),
2929+ }
3030+}
3131+3232+/// Adds a new entry. Returns true on success.
3333+#[unsafe(no_mangle)]
3434+pub unsafe extern "C" fn bindle_add(
3535+ ctx: *mut BindleContext,
3636+ name: *const c_char,
3737+ data: *const u8,
3838+ data_len: usize,
3939+ compress: bool,
4040+) -> bool {
4141+ if ctx.is_null() || name.is_null() || (data.is_null() && data_len > 0) {
4242+ return false;
4343+ }
4444+4545+ unsafe {
4646+ let name_str = match CStr::from_ptr(name).to_str() {
4747+ Ok(s) => s,
4848+ Err(_) => return false,
4949+ };
5050+5151+ let data_slice = slice::from_raw_parts(data, data_len);
5252+ let b = &mut (*ctx).inner;
5353+5454+ b.add(name_str, data_slice, compress).is_ok()
5555+ }
5656+}
5757+5858+/// Commits changes to disk.
5959+#[unsafe(no_mangle)]
6060+pub unsafe extern "C" fn bindle_save(ctx: *mut BindleContext) -> bool {
6161+ if ctx.is_null() {
6262+ return false;
6363+ }
6464+ unsafe {
6565+ let b = &mut (*ctx).inner;
6666+ b.save().is_ok()
6767+ }
6868+}
6969+7070+/// Frees BindleContext
7171+#[unsafe(no_mangle)]
7272+pub unsafe extern "C" fn bindle_free(ctx: *mut BindleContext) {
7373+ if ctx.is_null() {
7474+ return;
7575+ }
7676+ unsafe { drop(Box::from_raw(ctx)) }
7777+}
7878+7979+#[unsafe(no_mangle)]
8080+pub unsafe extern "C" fn bindle_read(
8181+ ctx: *mut BindleContext,
8282+ name: *const c_char,
8383+ out_len: *mut usize,
8484+) -> *mut u8 {
8585+ if ctx.is_null() || name.is_null() || out_len.is_null() {
8686+ return std::ptr::null_mut();
8787+ }
8888+8989+ unsafe {
9090+ let name_str = match CStr::from_ptr(name).to_str() {
9191+ Ok(s) => s,
9292+ Err(_) => return std::ptr::null_mut(),
9393+ };
9494+9595+ let b = &(*ctx).inner;
9696+9797+ if let Some(data) = b.read(name_str) {
9898+ let mut bytes = data.to_vec();
9999+ bytes.shrink_to_fit();
100100+ let ptr = bytes.as_mut_ptr();
101101+ *out_len = bytes.len();
102102+ std::mem::forget(bytes);
103103+ ptr
104104+ } else {
105105+ std::ptr::null_mut()
106106+ }
107107+ }
108108+}
109109+110110+#[unsafe(no_mangle)]
111111+pub unsafe extern "C" fn bindle_free_buffer(ptr: *mut u8, len: usize) {
112112+ if !ptr.is_null() {
113113+ unsafe {
114114+ let _ = Vec::from_raw_parts(ptr, len, len);
115115+ }
116116+ }
117117+}
118118+119119+#[unsafe(no_mangle)]
120120+pub unsafe extern "C" fn bindle_length(ctx: *const BindleContext) -> usize {
121121+ if ctx.is_null() {
122122+ return 0;
123123+ }
124124+ unsafe { (*ctx).inner.len() }
125125+}
126126+127127+/// Returns the name of the entry at the given index.
128128+/// The string is owned by the Bindle; the caller must NOT free it.
129129+#[unsafe(no_mangle)]
130130+pub unsafe extern "C" fn bindle_entry_name(
131131+ ctx: *const BindleContext,
132132+ index: usize,
133133+ len: *mut usize,
134134+) -> *const c_char {
135135+ if ctx.is_null() {
136136+ return std::ptr::null();
137137+ }
138138+139139+ let b = unsafe { &(*ctx).inner };
140140+ match b.entries.get(index) {
141141+ Some((_, name)) => {
142142+ unsafe {
143143+ *len = name.as_bytes().len();
144144+ }
145145+ name.as_ptr() as *const _
146146+ }
147147+ None => std::ptr::null(),
148148+ }
149149+}
+359
src/lib.rs
···11+use memmap2::Mmap;
22+use std::borrow::Cow;
33+use std::fs::{File, OpenOptions};
44+use std::io::{self, Read, Seek, SeekFrom, Write};
55+use std::path::Path;
66+use zerocopy::{FromBytes, Immutable, IntoBytes, Unaligned};
77+88+mod ffi;
99+1010+const BNDL_MAGIC: &[u8; 8] = b"BINDL001";
1111+const BNDL_ALIGN: usize = 8;
1212+const ENTRY_SIZE: usize = std::mem::size_of::<Entry>();
1313+const FOOTER_SIZE: usize = std::mem::size_of::<Footer>();
1414+const HEADER_SIZE: u64 = 8;
1515+1616+#[repr(C, packed)]
1717+#[derive(FromBytes, Unaligned, IntoBytes, Immutable, Clone, Copy, Debug)]
1818+pub struct Entry {
1919+ pub offset: [u8; 8],
2020+ pub compressed_size: [u8; 8],
2121+ pub uncompressed_size: [u8; 8],
2222+ pub crc32: [u8; 4],
2323+ pub name_len: [u8; 2],
2424+ pub compression_type: u8,
2525+ pub _reserved: u8,
2626+}
2727+2828+#[repr(C, packed)]
2929+#[derive(FromBytes, Unaligned, IntoBytes, Immutable, Debug)]
3030+struct Footer {
3131+ pub index_offset: [u8; 8],
3232+ pub entry_count: [u8; 4],
3333+}
3434+3535+pub struct Bindle {
3636+ file: File,
3737+ mmap: Option<Mmap>,
3838+ entries: Vec<(Entry, String)>,
3939+ data_end: u64,
4040+}
4141+4242+impl Bindle {
4343+ pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
4444+ let mut file = OpenOptions::new()
4545+ .read(true)
4646+ .write(true)
4747+ .create(true)
4848+ .open(path)?;
4949+5050+ let len = file.metadata()?.len();
5151+5252+ if len == 0 {
5353+ // New file: Write the magic header immediately
5454+ file.write_all(BNDL_MAGIC)?;
5555+ return Ok(Self {
5656+ file,
5757+ mmap: None,
5858+ entries: Vec::new(),
5959+ data_end: HEADER_SIZE,
6060+ });
6161+ }
6262+6363+ // Existing file: Check header magic
6464+ let mut header = [0u8; 8];
6565+ file.read_exact(&mut header)?;
6666+ if &header != BNDL_MAGIC {
6767+ return Err(io::Error::new(
6868+ io::ErrorKind::InvalidData,
6969+ "Invalid Bindle header",
7070+ ));
7171+ }
7272+ // Case 2: File exists but is too small to even hold a footer
7373+ if len < FOOTER_SIZE as u64 {
7474+ return Err(io::Error::new(
7575+ io::ErrorKind::InvalidData,
7676+ "File too small to be a Bindle",
7777+ ));
7878+ }
7979+8080+ let m = unsafe { Mmap::map(&file)? };
8181+ let footer_pos = m.len() - FOOTER_SIZE;
8282+8383+ let footer = Footer::read_from_bytes(&m[footer_pos..])
8484+ .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid footer alignment"))?;
8585+8686+ // If magic is valid, proceed to parse the index
8787+ let data_end = u64::from_le_bytes(footer.index_offset);
8888+ let count = u32::from_le_bytes(footer.entry_count);
8989+ let mut entries = Vec::with_capacity(count as usize);
9090+9191+ let mut cursor = data_end as usize;
9292+ for _ in 0..count {
9393+ let entry_bytes = m
9494+ .get(cursor..cursor + ENTRY_SIZE)
9595+ .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Index out of bounds"))?;
9696+ let entry = Entry::read_from_bytes(entry_bytes)
9797+ .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid entry"))?;
9898+9999+ let n_len = u16::from_le_bytes(entry.name_len) as usize;
100100+ let n_start = cursor + ENTRY_SIZE;
101101+ let n_end = n_start + n_len;
102102+103103+ let name_bytes = m
104104+ .get(n_start..n_end)
105105+ .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Name out of bounds"))?;
106106+ let name = String::from_utf8_lossy(name_bytes).into_owned();
107107+108108+ entries.push((entry, name));
109109+110110+ let total = ENTRY_SIZE + n_len;
111111+ cursor += (total + (BNDL_ALIGN - 1)) & !(BNDL_ALIGN - 1);
112112+ }
113113+114114+ Ok(Self {
115115+ file,
116116+ mmap: Some(m),
117117+ entries,
118118+ data_end,
119119+ })
120120+ }
121121+122122+ /// Reads data for an entry using Cow to avoid unnecessary copies.
123123+ pub fn read<'a>(&'a self, name: &str) -> Option<Cow<'a, [u8]>> {
124124+ let (entry, _) = self.entries.iter().find(|(_, n)| n == name)?;
125125+ let mmap = self.mmap.as_ref()?;
126126+127127+ let offset = u64::from_le_bytes(entry.offset) as usize;
128128+ let c_size = u64::from_le_bytes(entry.compressed_size) as usize;
129129+ let u_size = u64::from_le_bytes(entry.uncompressed_size) as usize;
130130+131131+ let data = mmap.get(offset..offset + c_size)?;
132132+133133+ if entry.compression_type == 1 {
134134+ let mut out = Vec::with_capacity(u_size);
135135+ zstd::Decoder::new(data).ok()?.read_to_end(&mut out).ok()?;
136136+ Some(Cow::Owned(out))
137137+ } else {
138138+ Some(Cow::Borrowed(data))
139139+ }
140140+ }
141141+142142+ /// Streams data directly to a writer (e.g., File, TcpStream) to keep memory usage low.
143143+ pub fn read_to_writer<W: Write>(&self, name: &str, mut writer: W) -> io::Result<u64> {
144144+ let (entry, _) = self
145145+ .entries
146146+ .iter()
147147+ .find(|(_, n)| n == name)
148148+ .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Entry not found"))?;
149149+150150+ let mmap = self
151151+ .mmap
152152+ .as_ref()
153153+ .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Archive not mapped"))?;
154154+155155+ let offset = u64::from_le_bytes(entry.offset) as usize;
156156+ let c_size = u64::from_le_bytes(entry.compressed_size) as usize;
157157+ let data = mmap.get(offset..offset + c_size).ok_or_else(|| {
158158+ io::Error::new(io::ErrorKind::InvalidData, "Data range out of bounds")
159159+ })?;
160160+161161+ if entry.compression_type == 1 {
162162+ let mut decoder = zstd::Decoder::new(data)?;
163163+ io::copy(&mut decoder, &mut writer)
164164+ } else {
165165+ writer.write_all(data)?;
166166+ Ok(data.len() as u64)
167167+ }
168168+ }
169169+170170+ pub fn add(&mut self, name: &str, data: &[u8], compress: bool) -> io::Result<()> {
171171+ // 1. Prevent Duplicate Keys
172172+ if self
173173+ .entries
174174+ .iter()
175175+ .any(|(_, existing_name)| existing_name == name)
176176+ {
177177+ return Err(io::Error::new(
178178+ io::ErrorKind::AlreadyExists,
179179+ format!("Entry '{}' already exists in bindle", name),
180180+ ));
181181+ }
182182+183183+ // 2. Position the file pointer at the end of valid data
184184+ // If data_end is 0, we start after the 8-byte Magic Header
185185+ let write_pos = if self.data_end >= HEADER_SIZE {
186186+ self.data_end
187187+ } else {
188188+ HEADER_SIZE
189189+ };
190190+191191+ self.file.seek(SeekFrom::Start(write_pos))?;
192192+193193+ // 3. Prepare and write data
194194+ let write_data = if compress {
195195+ zstd::encode_all(data, 3)?
196196+ } else {
197197+ data.to_vec()
198198+ };
199199+200200+ let start_offset = self.file.stream_position()?;
201201+ self.file.write_all(&write_data)?;
202202+203203+ // 4. Align to 8 bytes for the next entry or index
204204+ let current_pos = self.file.stream_position()?;
205205+ let pad = (BNDL_ALIGN as u64 - (current_pos % BNDL_ALIGN as u64)) % BNDL_ALIGN as u64;
206206+ if pad > 0 {
207207+ self.file.write_all(&vec![0u8; pad as usize])?;
208208+ }
209209+210210+ // 5. Update state
211211+ self.data_end = self.file.stream_position()?;
212212+ let entry = Entry {
213213+ offset: start_offset.to_le_bytes(),
214214+ compressed_size: (write_data.len() as u64).to_le_bytes(),
215215+ uncompressed_size: (data.len() as u64).to_le_bytes(),
216216+ crc32: crc32fast::hash(&write_data).to_le_bytes(),
217217+ name_len: (name.len() as u16).to_le_bytes(),
218218+ compression_type: if compress { 1 } else { 0 },
219219+ _reserved: 0,
220220+ };
221221+222222+ self.entries.push((entry, name.to_string()));
223223+ Ok(())
224224+ }
225225+226226+ pub fn save(&mut self) -> io::Result<()> {
227227+ self.file.seek(SeekFrom::Start(self.data_end))?;
228228+ let index_start = self.data_end;
229229+230230+ for (entry, name) in &self.entries {
231231+ self.file.write_all(entry.as_bytes())?;
232232+ self.file.write_all(name.as_bytes())?;
233233+ let current_disk_size = ENTRY_SIZE + name.len();
234234+ let pad = (BNDL_ALIGN - (current_disk_size % BNDL_ALIGN)) % BNDL_ALIGN;
235235+ if pad > 0 {
236236+ self.file.write_all(&vec![0u8; pad])?;
237237+ }
238238+ }
239239+240240+ let footer = Footer {
241241+ index_offset: index_start.to_le_bytes(),
242242+ entry_count: (self.entries.len() as u32).to_le_bytes(),
243243+ };
244244+245245+ self.file.write_all(footer.as_bytes())?;
246246+ self.file.flush()?;
247247+248248+ self.mmap = Some(unsafe { Mmap::map(&self.file)? });
249249+ Ok(())
250250+ }
251251+252252+ /// Returns a list of all entry names in the archive.
253253+ pub fn list(&self) -> Vec<&str> {
254254+ self.entries.iter().map(|(_, name)| name.as_str()).collect()
255255+ }
256256+257257+ /// Returns the number of entries.
258258+ pub fn len(&self) -> usize {
259259+ self.entries.len()
260260+ }
261261+262262+ pub fn is_empty(&self) -> bool {
263263+ self.entries.is_empty()
264264+ }
265265+}
266266+267267+#[cfg(test)]
268268+mod tests {
269269+ use super::*;
270270+ use std::fs;
271271+272272+ #[test]
273273+ fn test_create_and_read() {
274274+ let path = "test_basic.bindl";
275275+ let data = b"Hello, Bindle World!";
276276+277277+ // 1. Create and Write
278278+ {
279279+ let mut fp = Bindle::open(path).expect("Failed to open");
280280+ fp.add("hello.txt", data, false).expect("Failed to add");
281281+ fp.save().expect("Failed to commit");
282282+ }
283283+284284+ // 2. Open and Read
285285+ {
286286+ let fp = Bindle::open(path).expect("Failed to re-open");
287287+ let result = fp.read("hello.txt").expect("File not found");
288288+ assert_eq!(result.as_ref(), data);
289289+ }
290290+291291+ fs::remove_file(path).ok();
292292+ }
293293+294294+ #[test]
295295+ fn test_zstd_compression() {
296296+ let path = "test_zstd.bindl";
297297+ // Highly compressible data
298298+ let data = vec![b'A'; 1000];
299299+300300+ {
301301+ let mut fp = Bindle::open(path).expect("Failed to open");
302302+ fp.add("large.bin", &data, true).expect("Failed to add");
303303+ fp.save().expect("Failed to commit");
304304+ }
305305+306306+ let fp = Bindle::open(path).expect("Failed to re-open");
307307+308308+ // Ensure data is correct
309309+ let result = fp.read("large.bin").expect("File not found");
310310+ assert_eq!(result, data);
311311+312312+ // Ensure the file on disk is actually smaller than the raw data (including headers)
313313+ let meta = fs::metadata(path).unwrap();
314314+ assert!(meta.len() < 1000);
315315+316316+ fs::remove_file(path).ok();
317317+ }
318318+319319+ #[test]
320320+ fn test_append_functionality() {
321321+ let path = "test_append.bindl";
322322+ let _ = std::fs::remove_file(path);
323323+324324+ // 1. Initial creation
325325+ {
326326+ let mut fp = Bindle::open(path).expect("Fail open 1");
327327+ fp.add("1.txt", b"First", false).unwrap();
328328+ fp.save().expect("Fail commit 1");
329329+ } // File handle closed here
330330+331331+ // 2. Append session
332332+ {
333333+ let mut fp = Bindle::open(path).expect("Fail open 2");
334334+ // At this point, entries contains "1.txt"
335335+336336+ fp.add("2.txt", b"Second", false).unwrap();
337337+ fp.save().expect("Fail commit 2");
338338+339339+ // Now test the read
340340+ let first = fp.read("1.txt").expect("Could not find 1.txt");
341341+ let second = fp.read("2.txt").expect("Could not find 2.txt");
342342+343343+ assert_eq!(first.as_ref(), b"First");
344344+ assert_eq!(second.as_ref(), b"Second");
345345+ }
346346+ let _ = std::fs::remove_file(path);
347347+ }
348348+349349+ #[test]
350350+ fn test_invalid_magic() {
351351+ let path = "invalid.bindl";
352352+ fs::write(path, b"NOT_A_PACK_FILE_AT_ALL").unwrap();
353353+354354+ let res = Bindle::open(path);
355355+ assert!(res.is_err());
356356+357357+ fs::remove_file(path).ok();
358358+ }
359359+}