an efficient binary archive format
at main 681 lines 19 kB view raw
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}