an efficient binary archive format
1use std::alloc::{Layout, dealloc};
2use std::ffi::{CStr, CString};
3use std::io::{Read, Write};
4use std::mem;
5use std::os::raw::c_char;
6use std::slice;
7
8use crate::{Compress, Reader, Writer};
9
10/// FFI wrapper around Bindle that caches null-terminated entry names for C API.
11pub struct Bindle {
12 bindle: crate::Bindle,
13 entry_names_cache: Vec<CString>,
14}
15
16impl Bindle {
17 fn new(bindle: crate::Bindle) -> Self {
18 let mut ffi = Bindle {
19 bindle,
20 entry_names_cache: Vec::new(),
21 };
22 ffi.rebuild_cache();
23 ffi
24 }
25
26 fn rebuild_cache(&mut self) {
27 self.entry_names_cache.clear();
28 for (name, _) in &self.bindle.index {
29 if let Ok(c_str) = CString::new(name.as_str()) {
30 self.entry_names_cache.push(c_str);
31 }
32 }
33 }
34}
35
36/// Creates a new archive, overwriting any existing file.
37///
38/// # Parameters
39/// * `path` - NUL-terminated path to the archive file
40///
41/// # Returns
42/// A pointer to the Bindle handle, or NULL on error. Must be freed with `bindle_close()`.
43#[unsafe(no_mangle)]
44pub unsafe extern "C" fn bindle_create(path: *const c_char) -> *mut Bindle {
45 if path.is_null() {
46 return std::ptr::null_mut();
47 }
48
49 let path_str = unsafe {
50 match CStr::from_ptr(path).to_str() {
51 Ok(s) => s,
52 Err(_) => return std::ptr::null_mut(),
53 }
54 };
55
56 match crate::Bindle::create(path_str) {
57 Ok(b) => Box::into_raw(Box::new(Bindle::new(b))),
58 Err(_) => std::ptr::null_mut(),
59 }
60}
61
62/// Opens an existing archive or creates a new one.
63///
64/// # Parameters
65/// * `path` - NUL-terminated path to the archive file
66///
67/// # Returns
68/// A pointer to the Bindle handle, or NULL on error. Must be freed with `bindle_close()`.
69#[unsafe(no_mangle)]
70pub unsafe extern "C" fn bindle_open(path: *const c_char) -> *mut Bindle {
71 if path.is_null() {
72 return std::ptr::null_mut();
73 }
74
75 let path_str = unsafe {
76 match CStr::from_ptr(path).to_str() {
77 Ok(s) => s,
78 Err(_) => return std::ptr::null_mut(),
79 }
80 };
81
82 match crate::Bindle::open(path_str) {
83 Ok(b) => Box::into_raw(Box::new(Bindle::new(b))),
84 Err(_) => std::ptr::null_mut(),
85 }
86}
87
88/// Opens an existing archive. Returns NULL if the file doesn't exist.
89///
90/// # Parameters
91/// * `path` - NUL-terminated path to the archive file
92///
93/// # Returns
94/// A pointer to the Bindle handle, or NULL on error. Must be freed with `bindle_close()`.
95#[unsafe(no_mangle)]
96pub unsafe extern "C" fn bindle_load(path: *const c_char) -> *mut Bindle {
97 if path.is_null() {
98 return std::ptr::null_mut();
99 }
100
101 let path_str = unsafe {
102 match CStr::from_ptr(path).to_str() {
103 Ok(s) => s,
104 Err(_) => return std::ptr::null_mut(),
105 }
106 };
107
108 match crate::Bindle::load(path_str) {
109 Ok(b) => Box::into_raw(Box::new(Bindle::new(b))),
110 Err(_) => std::ptr::null_mut(),
111 }
112}
113
114/// Adds data to the archive with the given name.
115///
116/// # Parameters
117/// * `ctx` - Bindle handle from `bindle_open()`
118/// * `name` - NUL-terminated entry name
119/// * `data` - Data bytes (may contain NUL bytes)
120/// * `data_len` - Length of data in bytes
121/// * `compress` - Compression mode (BindleCompressNone, BindleCompressZstd, or BindleCompressAuto)
122///
123/// # Returns
124/// True on success. Call `bindle_save()` to commit changes.
125#[unsafe(no_mangle)]
126pub unsafe extern "C" fn bindle_add(
127 ctx: *mut Bindle,
128 name: *const c_char,
129 data: *const u8,
130 data_len: usize,
131 compress: Compress,
132) -> bool {
133 if ctx.is_null() || name.is_null() || (data.is_null() && data_len > 0) {
134 return false;
135 }
136
137 unsafe {
138 let name_str = match CStr::from_ptr(name).to_str() {
139 Ok(s) => s,
140 Err(_) => return false,
141 };
142
143 let data_slice = slice::from_raw_parts(data, data_len);
144 let b = &mut (*ctx);
145
146 let result = b.bindle.add(name_str, data_slice, compress).is_ok();
147 if result {
148 b.rebuild_cache();
149 }
150 result
151 }
152}
153
154/// Adds a file from the filesystem to the archive.
155///
156/// # Parameters
157/// * `ctx` - Bindle handle from `bindle_open()`
158/// * `name` - NUL-terminated entry name
159/// * `path` - NUL-terminated path to file on disk
160/// * `compress` - Compression mode
161///
162/// # Returns
163/// True on success. Call `bindle_save()` to commit changes.
164#[unsafe(no_mangle)]
165pub unsafe extern "C" fn bindle_add_file(
166 ctx: *mut Bindle,
167 name: *const c_char,
168 path: *const c_char,
169 compress: Compress,
170) -> bool {
171 if ctx.is_null() || name.is_null() || path.is_null() {
172 return false;
173 }
174
175 unsafe {
176 let name_str = match CStr::from_ptr(name).to_str() {
177 Ok(s) => s,
178 Err(_) => return false,
179 };
180
181 let path_str = match CStr::from_ptr(path).to_str() {
182 Ok(s) => s,
183 Err(_) => return false,
184 };
185
186 let b = &mut (*ctx);
187
188 let result = b.bindle.add_file(name_str, path_str, compress).is_ok();
189 if result {
190 b.rebuild_cache();
191 }
192 result
193 }
194}
195
196/// Commits all pending changes to disk.
197///
198/// Writes the index and footer. Must be called after add/remove operations.
199#[unsafe(no_mangle)]
200pub unsafe extern "C" fn bindle_save(ctx: *mut Bindle) -> bool {
201 if ctx.is_null() {
202 return false;
203 }
204 unsafe {
205 let b = &mut (*ctx);
206 b.bindle.save().is_ok()
207 }
208}
209
210/// Closes the archive and frees the handle.
211///
212/// After calling this, the ctx pointer is no longer valid.
213#[unsafe(no_mangle)]
214pub unsafe extern "C" fn bindle_close(ctx: *mut Bindle) {
215 if ctx.is_null() {
216 return;
217 }
218 unsafe { drop(Box::from_raw(ctx)) }
219}
220
221/// Reads an entry from the archive, decompressing if needed.
222///
223/// # Parameters
224/// * `ctx_ptr` - Bindle handle
225/// * `name` - NUL-terminated entry name
226/// * `out_len` - Output parameter for data length
227///
228/// # Returns
229/// Pointer to data buffer, or NULL if not found or CRC32 check fails.
230/// Must be freed with `bindle_free_buffer()`.
231#[unsafe(no_mangle)]
232pub unsafe extern "C" fn bindle_read_buffer(
233 ctx_ptr: *mut Bindle,
234 name: *const c_char,
235 out_len: *mut usize,
236) -> *mut u8 {
237 unsafe {
238 if ctx_ptr.is_null() || name.is_null() {
239 return std::ptr::null_mut();
240 }
241
242 // 1. Convert the C string to a Rust &str
243 let c_str = std::ffi::CStr::from_ptr(name);
244 let name_str = match c_str.to_str() {
245 Ok(s) => s,
246 Err(_) => return std::ptr::null_mut(),
247 };
248
249 // 2. Access your Rust Bindle struct
250 let ctx = &mut *ctx_ptr;
251
252 // 3. The actual data retrieval logic
253 match ctx.bindle.read(name_str) {
254 Some(bytes) => wrap_in_ffi_header(bytes.as_ref(), out_len),
255 None => return std::ptr::null_mut(),
256 }
257 }
258}
259
260/// Internal helper to perform the "Hidden Header" allocation
261unsafe fn wrap_in_ffi_header(data: &[u8], out_len: *mut usize) -> *mut u8 {
262 unsafe {
263 let len = data.len();
264 if !out_len.is_null() {
265 *out_len = len;
266 }
267
268 let size_of_header = std::mem::size_of::<usize>();
269 let total_size = size_of_header + len;
270 let layout =
271 std::alloc::Layout::from_size_align(total_size, std::mem::align_of::<usize>()).unwrap();
272
273 let raw_ptr = std::alloc::alloc(layout);
274 if raw_ptr.is_null() {
275 return std::ptr::null_mut();
276 }
277
278 // Store the length at the start
279 *(raw_ptr as *mut usize) = len;
280
281 // Copy data to the payload area
282 let data_ptr = raw_ptr.add(size_of_header);
283 std::ptr::copy_nonoverlapping(data.as_ptr(), data_ptr, len);
284
285 data_ptr
286 }
287}
288
289/// Frees a buffer returned by `bindle_read()`.
290#[unsafe(no_mangle)]
291pub unsafe extern "C" fn bindle_free_buffer(ptr: *mut u8) {
292 unsafe {
293 if ptr.is_null() {
294 return;
295 }
296
297 let size_of_header = mem::size_of::<usize>();
298
299 // 1. Step back to find the start of the header
300 let raw_ptr = ptr.sub(size_of_header);
301
302 // 2. Read the length we stored there
303 let len = *(raw_ptr as *const usize);
304
305 // 3. Reconstruct the layout used during allocation
306 let total_size = size_of_header + len;
307 let layout = Layout::from_size_align(total_size, mem::align_of::<usize>()).unwrap();
308
309 // 4. Deallocate the entire block
310 dealloc(raw_ptr, layout);
311 }
312}
313
314/// Reads an uncompressed entry without allocating.
315///
316/// Returns a pointer directly into the memory-mapped archive. Only works for uncompressed entries.
317///
318/// # Parameters
319/// * `ctx` - Bindle handle
320/// * `name` - NUL-terminated entry name
321/// * `out_len` - Output parameter for data length
322///
323/// # Returns
324/// Pointer into the mmap, or NULL if entry is compressed or doesn't exist.
325/// The pointer is valid as long as the Bindle handle is open. Do NOT free this pointer.
326#[unsafe(no_mangle)]
327pub unsafe extern "C" fn bindle_read_uncompressed_direct(
328 ctx: *mut Bindle,
329 name: *const c_char,
330 out_len: *mut usize,
331) -> *const u8 {
332 if ctx.is_null() || name.is_null() || out_len.is_null() {
333 return std::ptr::null_mut();
334 }
335
336 unsafe {
337 let name_str = match CStr::from_ptr(name).to_str() {
338 Ok(s) => s,
339 Err(_) => return std::ptr::null_mut(),
340 };
341
342 let b = &(*ctx);
343 if let Some(data) = b.bindle.read(name_str) {
344 match data {
345 std::borrow::Cow::Borrowed(bytes) => bytes.as_ptr(),
346 _ => std::ptr::null_mut(),
347 }
348 } else {
349 std::ptr::null_mut()
350 }
351 }
352}
353
354/// Returns the number of entries in the archive.
355#[unsafe(no_mangle)]
356pub unsafe extern "C" fn bindle_length(ctx: *const Bindle) -> usize {
357 if ctx.is_null() {
358 return 0;
359 }
360 unsafe { (*ctx).bindle.len() }
361}
362
363/// Returns the name of the entry at the given index as a null-terminated C string.
364///
365/// Use with `bindle_length()` to iterate over all entries. The pointer is valid as long as the Bindle handle is open.
366/// Do NOT free the returned pointer.
367#[unsafe(no_mangle)]
368pub unsafe extern "C" fn bindle_entry_name(ctx: *const Bindle, index: usize) -> *const c_char {
369 if ctx.is_null() {
370 return std::ptr::null();
371 }
372
373 let b = unsafe { &(*ctx) };
374 match b.entry_names_cache.get(index) {
375 Some(c_str) => c_str.as_ptr(),
376 None => std::ptr::null(),
377 }
378}
379
380/// Reclaims space by removing shadowed data.
381///
382/// Rebuilds the archive with only live entries.
383#[unsafe(no_mangle)]
384pub unsafe extern "C" fn bindle_vacuum(ctx: *mut Bindle) -> bool {
385 if ctx.is_null() {
386 return false;
387 }
388 let b = unsafe { &mut (*ctx) };
389 let result = b.bindle.vacuum().is_ok();
390 if result {
391 b.rebuild_cache();
392 }
393 result
394}
395
396/// Extracts all entries to a destination directory.
397#[unsafe(no_mangle)]
398pub unsafe extern "C" fn bindle_unpack(ctx: *mut Bindle, dest_path: *const c_char) -> bool {
399 if ctx.is_null() || dest_path.is_null() {
400 return false;
401 }
402 let b = unsafe { &*ctx };
403 let path = unsafe { CStr::from_ptr(dest_path).to_string_lossy() };
404 b.bindle.unpack(path.as_ref()).is_ok()
405}
406
407/// Recursively adds all files from a directory to the archive.
408///
409/// Call `bindle_save()` to commit changes.
410#[unsafe(no_mangle)]
411pub unsafe extern "C" fn bindle_pack(
412 ctx: *mut Bindle,
413 src_path: *const c_char,
414 compress: Compress,
415) -> bool {
416 if ctx.is_null() || src_path.is_null() {
417 return false;
418 }
419 let b = unsafe { &mut *ctx };
420 let path = unsafe { CStr::from_ptr(src_path).to_string_lossy() };
421 let result = b.bindle.pack(path.as_ref(), compress).is_ok();
422 if result {
423 b.rebuild_cache();
424 }
425 result
426}
427
428/// Returns true if an entry with the given name exists.
429#[unsafe(no_mangle)]
430pub unsafe extern "C" fn bindle_exists(ctx: *const Bindle, name: *const c_char) -> bool {
431 if ctx.is_null() || name.is_null() {
432 return false;
433 }
434
435 let b = unsafe { &*ctx };
436 let name_str = unsafe {
437 match CStr::from_ptr(name).to_str() {
438 Ok(s) => s,
439 Err(_) => return false,
440 }
441 };
442
443 b.bindle.exists(name_str)
444}
445
446/// Removes an entry from the index.
447///
448/// Returns true if the entry existed. Data remains in the file until `bindle_vacuum()` is called.
449/// Call `bindle_save()` to commit changes.
450#[unsafe(no_mangle)]
451pub unsafe extern "C" fn bindle_remove(ctx: *mut Bindle, name: *const c_char) -> bool {
452 if ctx.is_null() || name.is_null() {
453 return false;
454 }
455
456 let b = unsafe { &mut *ctx };
457 let name_str = unsafe {
458 match CStr::from_ptr(name).to_str() {
459 Ok(s) => s,
460 Err(_) => return false,
461 }
462 };
463
464 let result = b.bindle.remove(name_str);
465 if result {
466 b.rebuild_cache();
467 }
468 result
469}
470
471/// Creates a streaming writer for adding an entry.
472///
473/// The writer must be closed with `bindle_writer_close()`, then call `bindle_save()` to commit.
474/// Do not access the Bindle handle while the writer is active.
475#[unsafe(no_mangle)]
476pub unsafe extern "C" fn bindle_writer_new<'a>(
477 ctx: *mut Bindle,
478 name: *const c_char,
479 compress: Compress,
480) -> *mut Writer<'a> {
481 unsafe {
482 let b = &mut *ctx;
483 let name_str = CStr::from_ptr(name).to_string_lossy();
484
485 match b.bindle.writer(&name_str, compress) {
486 Ok(stream) => Box::into_raw(Box::new(std::mem::transmute(stream))),
487 Err(_) => std::ptr::null_mut(),
488 }
489 }
490}
491
492/// Writes data to the writer.
493#[unsafe(no_mangle)]
494pub unsafe extern "C" fn bindle_writer_write(
495 stream: *mut Writer,
496 data: *const u8,
497 len: usize,
498) -> bool {
499 unsafe {
500 let s = &mut *stream;
501 let chunk = std::slice::from_raw_parts(data, len);
502 s.write_all(chunk).is_ok()
503 }
504}
505
506/// Closes the writer and finalizes the entry.
507#[unsafe(no_mangle)]
508pub unsafe extern "C" fn bindle_writer_close(stream: *mut Writer) -> bool {
509 let s = unsafe { Box::from_raw(stream) };
510 s.close().is_ok()
511}
512
513/// Creates a streaming reader for an entry.
514///
515/// Automatically decompresses if needed. Must be freed with `bindle_reader_close()`.
516/// Call `bindle_reader_verify_crc32()` after reading to verify integrity.
517#[unsafe(no_mangle)]
518pub unsafe extern "C" fn bindle_reader_new<'a>(
519 ctx: *const Bindle,
520 name: *const c_char,
521) -> *mut Reader<'a> {
522 if ctx.is_null() || name.is_null() {
523 return std::ptr::null_mut();
524 }
525
526 let b = unsafe { &*ctx };
527 let name_str = unsafe { CStr::from_ptr(name).to_string_lossy() };
528
529 match b.bindle.reader(&name_str) {
530 Ok(reader) => Box::into_raw(Box::new(reader)),
531 Err(_) => std::ptr::null_mut(),
532 }
533}
534
535/// Reads data from the reader into the provided buffer.
536///
537/// Returns the number of bytes read, or -1 on error. Returns 0 on EOF.
538#[unsafe(no_mangle)]
539pub unsafe extern "C" fn bindle_reader_read(
540 reader: *mut Reader,
541 buffer: *mut u8,
542 buffer_len: usize,
543) -> isize {
544 if reader.is_null() || buffer.is_null() {
545 return -1;
546 }
547
548 let r = unsafe { &mut *reader };
549 let out_slice = unsafe { slice::from_raw_parts_mut(buffer, buffer_len) };
550
551 match r.read(out_slice) {
552 Ok(n) => n as isize,
553 Err(_) => -1,
554 }
555}
556
557/// Verify the CRC32 of data read from the reader.
558/// Should be called after reading all data to ensure integrity.
559/// Returns true if CRC32 matches, false otherwise.
560#[unsafe(no_mangle)]
561pub unsafe extern "C" fn bindle_reader_verify_crc32(reader: *const Reader) -> bool {
562 if reader.is_null() {
563 return false;
564 }
565
566 let r = unsafe { &*reader };
567 r.verify_crc32().is_ok()
568}
569
570/// Closes the reader and frees the handle.
571#[unsafe(no_mangle)]
572pub unsafe extern "C" fn bindle_reader_close(reader: *mut Reader) {
573 if !reader.is_null() {
574 unsafe {
575 drop(Box::from_raw(reader));
576 }
577 }
578}
579
580/// Gets the uncompressed size of an entry by name.
581///
582/// # Parameters
583/// * `ctx` - Bindle handle
584/// * `name` - NUL-terminated entry name
585///
586/// # Returns
587/// The uncompressed size in bytes, or 0 if the entry doesn't exist.
588/// Note: Returns 0 for both non-existent entries and zero-length entries.
589#[unsafe(no_mangle)]
590pub unsafe extern "C" fn bindle_entry_size(ctx: *const Bindle, name: *const c_char) -> usize {
591 if ctx.is_null() || name.is_null() {
592 return 0;
593 }
594
595 unsafe {
596 let name_str = match CStr::from_ptr(name).to_str() {
597 Ok(s) => s,
598 Err(_) => return 0,
599 };
600
601 let b = &*ctx;
602 match b.bindle.index.get(name_str) {
603 Some(entry) => entry.uncompressed_size() as usize,
604 None => 0,
605 }
606 }
607}
608
609/// Gets the compression type of an entry by name.
610///
611/// # Parameters
612/// * `ctx` - Bindle handle
613/// * `name` - NUL-terminated entry name
614///
615/// # Returns
616/// The Compress value (0 = None, 1 = Zstd), or 0 if the entry doesn't exist.
617#[unsafe(no_mangle)]
618pub unsafe extern "C" fn bindle_entry_compress(ctx: *const Bindle, name: *const c_char) -> Compress {
619 if ctx.is_null() || name.is_null() {
620 return Compress::None;
621 }
622
623 unsafe {
624 let name_str = match CStr::from_ptr(name).to_str() {
625 Ok(s) => s,
626 Err(_) => return Compress::None,
627 };
628
629 let b = &*ctx;
630 match b.bindle.index.get(name_str) {
631 Some(entry) => {
632 if entry.compression_type == 1 {
633 Compress::Zstd
634 } else {
635 Compress::None
636 }
637 }
638 None => Compress::None,
639 }
640 }
641}
642
643/// Reads an entry into a pre-existing buffer.
644///
645/// Decompresses if needed and verifies CRC32. Reads up to `buffer_len` bytes.
646///
647/// # Parameters
648/// * `ctx` - Bindle handle
649/// * `name` - NUL-terminated entry name
650/// * `buffer` - Pre-allocated buffer to read into
651/// * `buffer_len` - Maximum number of bytes to read
652///
653/// # Returns
654/// The number of bytes actually read, or 0 if the entry doesn't exist or CRC32 check fails.
655/// If the entry is larger than `buffer_len`, only `buffer_len` bytes are read.
656#[unsafe(no_mangle)]
657pub unsafe extern "C" fn bindle_read(
658 ctx: *const Bindle,
659 name: *const c_char,
660 buffer: *mut u8,
661 buffer_len: usize,
662) -> usize {
663 if ctx.is_null() || name.is_null() || buffer.is_null() {
664 return 0;
665 }
666
667 unsafe {
668 let name_str = match CStr::from_ptr(name).to_str() {
669 Ok(s) => s,
670 Err(_) => return 0,
671 };
672
673 let b = &*ctx;
674 let buffer_slice = slice::from_raw_parts_mut(buffer, buffer_len);
675
676 match b.bindle.read_into(name_str, buffer_slice) {
677 Ok(bytes_read) => bytes_read,
678 Err(_) => 0,
679 }
680 }
681}