···4444bindle_add(bindle, "file.txt", data, len, BindleCompressNone);
4545bindle_save(bindle);
46464747-size_t size;
4848-uint8_t* data = bindle_read(bindle, "file.txt", &size);
4949-4747+size_t len = bindle_entry_size(bindle, "file.txt");
4848+uint8_t *data = malloc(len);
4949+assert(bindle_read(bindle, "file.txt", data) == len);
5050// Or for uncompressed entries, read directly without decompression
5151uint8_t* raw = bindle_read_uncompressed_direct(bindle, "file.txt", &size);
5252
+45-4
include/bindle.h
···181181 * Pointer to data buffer, or NULL if not found or CRC32 check fails.
182182 * Must be freed with `bindle_free_buffer()`.
183183 */
184184-uint8_t *bindle_read(struct Bindle *ctx_ptr, const char *name, size_t *out_len);
184184+uint8_t *bindle_read_buffer(struct Bindle *ctx_ptr, const char *name, size_t *out_len);
185185186186/**
187187 * Frees a buffer returned by `bindle_read()`.
···212212size_t bindle_length(const struct Bindle *ctx);
213213214214/**
215215- * Returns the name of the entry at the given index.
215215+ * Returns the name of the entry at the given index as a null-terminated C string.
216216 *
217217 * Use with `bindle_length()` to iterate over all entries. The pointer is valid as long as the Bindle handle is open.
218218 * Do NOT free the returned pointer.
219219 */
220220const char *bindle_entry_name(const struct Bindle *ctx,
221221- size_t index,
222222- size_t *len);
221221+ size_t index);
223222224223/**
225224 * Reclaims space by removing shadowed data.
···299298 * Closes the reader and frees the handle.
300299 */
301300void bindle_reader_close(struct BindleReader *reader);
301301+302302+/**
303303+ * Gets the uncompressed size of an entry by name.
304304+ *
305305+ * # Parameters
306306+ * * `ctx` - Bindle handle
307307+ * * `name` - NUL-terminated entry name
308308+ *
309309+ * # Returns
310310+ * The uncompressed size in bytes, or 0 if the entry doesn't exist.
311311+ * Note: Returns 0 for both non-existent entries and zero-length entries.
312312+ */
313313+size_t bindle_entry_size(const struct Bindle *ctx, const char *name);
314314+315315+/**
316316+ * Gets the compression type of an entry by name.
317317+ *
318318+ * # Parameters
319319+ * * `ctx` - Bindle handle
320320+ * * `name` - NUL-terminated entry name
321321+ *
322322+ * # Returns
323323+ * The Compress value (0 = None, 1 = Zstd), or 0 if the entry doesn't exist.
324324+ */
325325+enum BindleCompress bindle_entry_compress(const struct Bindle *ctx, const char *name);
326326+327327+/**
328328+ * Reads an entry into a pre-existing buffer.
329329+ *
330330+ * Decompresses if needed and verifies CRC32. Reads up to `buffer_len` bytes.
331331+ *
332332+ * # Parameters
333333+ * * `ctx` - Bindle handle
334334+ * * `name` - NUL-terminated entry name
335335+ * * `buffer` - Pre-allocated buffer to read into
336336+ * * `buffer_len` - Maximum number of bytes to read
337337+ *
338338+ * # Returns
339339+ * The number of bytes actually read, or 0 if the entry doesn't exist or CRC32 check fails.
340340+ * If the entry is larger than `buffer_len`, only `buffer_len` bytes are read.
341341+ */
342342+size_t bindle_read(const struct Bindle *ctx, const char *name, uint8_t *buffer, size_t buffer_len);
302343303344#endif /* BINDLE_H */
+22
src/bindle.rs
···333333 Some(data)
334334 }
335335336336+ /// Reads an entry into a provided buffer, avoiding allocation.
337337+ ///
338338+ /// Decompresses if needed and verifies CRC32. Returns the number of bytes read.
339339+ /// If the buffer is too small, only reads up to buffer.len() bytes.
340340+ ///
341341+ /// # Example
342342+ ///
343343+ /// ```no_run
344344+ /// use bindle_file::Bindle;
345345+ ///
346346+ /// let archive = Bindle::open("data.bndl")?;
347347+ /// let mut buffer = vec![0u8; 1024];
348348+ /// let bytes_read = archive.read_into("file.txt", &mut buffer)?;
349349+ /// # Ok::<(), std::io::Error>(())
350350+ /// ```
351351+ pub fn read_into(&self, name: &str, buffer: &mut [u8]) -> io::Result<usize> {
352352+ let mut reader = self.reader(name)?;
353353+ let bytes_read = reader.read(buffer)?;
354354+ reader.verify_crc32()?;
355355+ Ok(bytes_read)
356356+ }
357357+336358 /// Reads an entry and writes it to the given writer.
337359 ///
338360 /// Returns the number of bytes written. Verifies CRC32 after reading.
+175-36
src/ffi.rs
···11use std::alloc::{Layout, dealloc};
22-use std::ffi::CStr;
22+use std::ffi::{CStr, CString};
33use std::io::{Read, Write};
44use std::mem;
55use std::os::raw::c_char;
66use std::slice;
7788-use crate::{Bindle, Compress, Reader, Writer};
88+use crate::{Compress, Reader, Writer};
99+1010+/// FFI wrapper around Bindle that caches null-terminated entry names for C API.
1111+pub struct Bindle {
1212+ bindle: crate::Bindle,
1313+ entry_names_cache: Vec<CString>,
1414+}
1515+1616+impl Bindle {
1717+ fn new(bindle: crate::Bindle) -> Self {
1818+ let mut ffi = Bindle {
1919+ bindle,
2020+ entry_names_cache: Vec::new(),
2121+ };
2222+ ffi.rebuild_cache();
2323+ ffi
2424+ }
2525+2626+ fn rebuild_cache(&mut self) {
2727+ self.entry_names_cache.clear();
2828+ for (name, _) in &self.bindle.index {
2929+ if let Ok(c_str) = CString::new(name.as_str()) {
3030+ self.entry_names_cache.push(c_str);
3131+ }
3232+ }
3333+ }
3434+}
9351036/// Creates a new archive, overwriting any existing file.
1137///
···2753 }
2854 };
29553030- match Bindle::create(path_str) {
3131- Ok(b) => Box::into_raw(Box::new(b)),
5656+ match crate::Bindle::create(path_str) {
5757+ Ok(b) => Box::into_raw(Box::new(Bindle::new(b))),
3258 Err(_) => std::ptr::null_mut(),
3359 }
3460}
···5379 }
5480 };
55815656- match Bindle::open(path_str) {
5757- Ok(b) => Box::into_raw(Box::new(b)),
8282+ match crate::Bindle::open(path_str) {
8383+ Ok(b) => Box::into_raw(Box::new(Bindle::new(b))),
5884 Err(_) => std::ptr::null_mut(),
5985 }
6086}
···79105 }
80106 };
811078282- match Bindle::load(path_str) {
8383- Ok(b) => Box::into_raw(Box::new(b)),
108108+ match crate::Bindle::load(path_str) {
109109+ Ok(b) => Box::into_raw(Box::new(Bindle::new(b))),
84110 Err(_) => std::ptr::null_mut(),
85111 }
86112}
···117143 let data_slice = slice::from_raw_parts(data, data_len);
118144 let b = &mut (*ctx);
119145120120- b.add(name_str, data_slice, compress).is_ok()
146146+ let result = b.bindle.add(name_str, data_slice, compress).is_ok();
147147+ if result {
148148+ b.rebuild_cache();
149149+ }
150150+ result
121151 }
122152}
123153···155185156186 let b = &mut (*ctx);
157187158158- b.add_file(name_str, path_str, compress).is_ok()
188188+ let result = b.bindle.add_file(name_str, path_str, compress).is_ok();
189189+ if result {
190190+ b.rebuild_cache();
191191+ }
192192+ result
159193 }
160194}
161195···169203 }
170204 unsafe {
171205 let b = &mut (*ctx);
172172- b.save().is_ok()
206206+ b.bindle.save().is_ok()
173207 }
174208}
175209···195229/// Pointer to data buffer, or NULL if not found or CRC32 check fails.
196230/// Must be freed with `bindle_free_buffer()`.
197231#[unsafe(no_mangle)]
198198-pub unsafe extern "C" fn bindle_read(
232232+pub unsafe extern "C" fn bindle_read_buffer(
199233 ctx_ptr: *mut Bindle,
200234 name: *const c_char,
201235 out_len: *mut usize,
···216250 let ctx = &mut *ctx_ptr;
217251218252 // 3. The actual data retrieval logic
219219- // (Assuming your Rust Bindle has a method like .get(name))
220220- match ctx.read(name_str) {
253253+ match ctx.bindle.read(name_str) {
221254 Some(bytes) => wrap_in_ffi_header(bytes.as_ref(), out_len),
222255 None => return std::ptr::null_mut(),
223256 }
···307340 };
308341309342 let b = &(*ctx);
310310- if let Some(data) = b.read(name_str) {
343343+ if let Some(data) = b.bindle.read(name_str) {
311344 match data {
312345 std::borrow::Cow::Borrowed(bytes) => bytes.as_ptr(),
313346 _ => std::ptr::null_mut(),
···324357 if ctx.is_null() {
325358 return 0;
326359 }
327327- unsafe { (*ctx).len() }
360360+ unsafe { (*ctx).bindle.len() }
328361}
329362330330-/// Returns the name of the entry at the given index.
363363+/// Returns the name of the entry at the given index as a null-terminated C string.
331364///
332365/// Use with `bindle_length()` to iterate over all entries. The pointer is valid as long as the Bindle handle is open.
333366/// Do NOT free the returned pointer.
334367#[unsafe(no_mangle)]
335335-pub unsafe extern "C" fn bindle_entry_name(
336336- ctx: *const Bindle,
337337- index: usize,
338338- len: *mut usize,
339339-) -> *const c_char {
368368+pub unsafe extern "C" fn bindle_entry_name(ctx: *const Bindle, index: usize) -> *const c_char {
340369 if ctx.is_null() {
341370 return std::ptr::null();
342371 }
343372344373 let b = unsafe { &(*ctx) };
345345- match b.index.iter().nth(index) {
346346- Some((name, _)) => {
347347- unsafe {
348348- *len = name.as_bytes().len();
349349- }
350350- name.as_ptr() as *const _
351351- }
374374+ match b.entry_names_cache.get(index) {
375375+ Some(c_str) => c_str.as_ptr(),
352376 None => std::ptr::null(),
353377 }
354378}
···362386 return false;
363387 }
364388 let b = unsafe { &mut (*ctx) };
365365- b.vacuum().is_ok()
389389+ let result = b.bindle.vacuum().is_ok();
390390+ if result {
391391+ b.rebuild_cache();
392392+ }
393393+ result
366394}
367395368396/// Extracts all entries to a destination directory.
···373401 }
374402 let b = unsafe { &*ctx };
375403 let path = unsafe { CStr::from_ptr(dest_path).to_string_lossy() };
376376- b.unpack(path.as_ref()).is_ok()
404404+ b.bindle.unpack(path.as_ref()).is_ok()
377405}
378406379407/// Recursively adds all files from a directory to the archive.
···390418 }
391419 let b = unsafe { &mut *ctx };
392420 let path = unsafe { CStr::from_ptr(src_path).to_string_lossy() };
393393- b.pack(path.as_ref(), compress).is_ok()
421421+ let result = b.bindle.pack(path.as_ref(), compress).is_ok();
422422+ if result {
423423+ b.rebuild_cache();
424424+ }
425425+ result
394426}
395427396428/// Returns true if an entry with the given name exists.
···408440 }
409441 };
410442411411- b.exists(name_str)
443443+ b.bindle.exists(name_str)
412444}
413445414446/// Removes an entry from the index.
···429461 }
430462 };
431463432432- b.remove(name_str)
464464+ let result = b.bindle.remove(name_str);
465465+ if result {
466466+ b.rebuild_cache();
467467+ }
468468+ result
433469}
434470435471/// Creates a streaming writer for adding an entry.
···446482 let b = &mut *ctx;
447483 let name_str = CStr::from_ptr(name).to_string_lossy();
448484449449- match b.writer(&name_str, compress) {
485485+ match b.bindle.writer(&name_str, compress) {
450486 Ok(stream) => Box::into_raw(Box::new(std::mem::transmute(stream))),
451487 Err(_) => std::ptr::null_mut(),
452488 }
···490526 let b = unsafe { &*ctx };
491527 let name_str = unsafe { CStr::from_ptr(name).to_string_lossy() };
492528493493- match b.reader(&name_str) {
529529+ match b.bindle.reader(&name_str) {
494530 Ok(reader) => Box::into_raw(Box::new(reader)),
495531 Err(_) => std::ptr::null_mut(),
496532 }
···540576 }
541577 }
542578}
579579+580580+/// Gets the uncompressed size of an entry by name.
581581+///
582582+/// # Parameters
583583+/// * `ctx` - Bindle handle
584584+/// * `name` - NUL-terminated entry name
585585+///
586586+/// # Returns
587587+/// The uncompressed size in bytes, or 0 if the entry doesn't exist.
588588+/// Note: Returns 0 for both non-existent entries and zero-length entries.
589589+#[unsafe(no_mangle)]
590590+pub unsafe extern "C" fn bindle_entry_size(ctx: *const Bindle, name: *const c_char) -> usize {
591591+ if ctx.is_null() || name.is_null() {
592592+ return 0;
593593+ }
594594+595595+ unsafe {
596596+ let name_str = match CStr::from_ptr(name).to_str() {
597597+ Ok(s) => s,
598598+ Err(_) => return 0,
599599+ };
600600+601601+ let b = &*ctx;
602602+ match b.bindle.index.get(name_str) {
603603+ Some(entry) => entry.uncompressed_size() as usize,
604604+ None => 0,
605605+ }
606606+ }
607607+}
608608+609609+/// Gets the compression type of an entry by name.
610610+///
611611+/// # Parameters
612612+/// * `ctx` - Bindle handle
613613+/// * `name` - NUL-terminated entry name
614614+///
615615+/// # Returns
616616+/// The Compress value (0 = None, 1 = Zstd), or 0 if the entry doesn't exist.
617617+#[unsafe(no_mangle)]
618618+pub unsafe extern "C" fn bindle_entry_compress(ctx: *const Bindle, name: *const c_char) -> Compress {
619619+ if ctx.is_null() || name.is_null() {
620620+ return Compress::None;
621621+ }
622622+623623+ unsafe {
624624+ let name_str = match CStr::from_ptr(name).to_str() {
625625+ Ok(s) => s,
626626+ Err(_) => return Compress::None,
627627+ };
628628+629629+ let b = &*ctx;
630630+ match b.bindle.index.get(name_str) {
631631+ Some(entry) => {
632632+ if entry.compression_type == 1 {
633633+ Compress::Zstd
634634+ } else {
635635+ Compress::None
636636+ }
637637+ }
638638+ None => Compress::None,
639639+ }
640640+ }
641641+}
642642+643643+/// Reads an entry into a pre-existing buffer.
644644+///
645645+/// Decompresses if needed and verifies CRC32. Reads up to `buffer_len` bytes.
646646+///
647647+/// # Parameters
648648+/// * `ctx` - Bindle handle
649649+/// * `name` - NUL-terminated entry name
650650+/// * `buffer` - Pre-allocated buffer to read into
651651+/// * `buffer_len` - Maximum number of bytes to read
652652+///
653653+/// # Returns
654654+/// The number of bytes actually read, or 0 if the entry doesn't exist or CRC32 check fails.
655655+/// If the entry is larger than `buffer_len`, only `buffer_len` bytes are read.
656656+#[unsafe(no_mangle)]
657657+pub unsafe extern "C" fn bindle_read(
658658+ ctx: *const Bindle,
659659+ name: *const c_char,
660660+ buffer: *mut u8,
661661+ buffer_len: usize,
662662+) -> usize {
663663+ if ctx.is_null() || name.is_null() || buffer.is_null() {
664664+ return 0;
665665+ }
666666+667667+ unsafe {
668668+ let name_str = match CStr::from_ptr(name).to_str() {
669669+ Ok(s) => s,
670670+ Err(_) => return 0,
671671+ };
672672+673673+ let b = &*ctx;
674674+ let buffer_slice = slice::from_raw_parts_mut(buffer, buffer_len);
675675+676676+ match b.bindle.read_into(name_str, buffer_slice) {
677677+ Ok(bytes_read) => bytes_read,
678678+ Err(_) => 0,
679679+ }
680680+ }
681681+}