···11-# Bindle File Format (.bdnl)
11+# Bindle File Format (.bndl)
2233Bindle is a simple append-only binary archive format. It features a trailing index to support efficient writes and memory-mapped reads.
44
+13-14
bindle.h
···1212#include <stdint.h>
1313#include <stdbool.h>
14141515-/**
1616- * Opaque handle to a Bindle archive.
1717- */
1818-typedef struct BindleContext BindleContext;
1515+typedef struct Bindle Bindle;
19162020-struct BindleContext *bindle_open(const char *path);
1717+struct Bindle *bindle_open(const char *path);
21182219/**
2320 * Adds a new entry. Returns true on success.
2421 */
2525-bool bindle_add(struct BindleContext *ctx,
2222+bool bindle_add(struct Bindle *ctx,
2623 const char *name,
2724 const uint8_t *data,
2825 size_t data_len,
···3128/**
3229 * Commits changes to disk.
3330 */
3434-bool bindle_save(struct BindleContext *ctx);
3131+bool bindle_save(struct Bindle *ctx);
35323633/**
3734 * Frees BindleContext
3835 */
3939-void bindle_free(struct BindleContext *ctx);
3636+void bindle_close(struct Bindle *ctx);
3737+3838+uint8_t *bindle_read(struct Bindle *ctx_ptr, const char *name, size_t *out_len);
40394141-uint8_t *bindle_read(struct BindleContext *ctx, const char *name, size_t *out_len);
4040+void bindle_free_buffer(uint8_t *ptr);
42414343-const uint8_t *bindle_read_uncompressed_direct(struct BindleContext *ctx,
4242+const uint8_t *bindle_read_uncompressed_direct(struct Bindle *ctx,
4443 const char *name,
4544 size_t *out_len);
46454747-void bindle_free_buffer(uint8_t *ptr, size_t len);
4848-4949-size_t bindle_length(const struct BindleContext *ctx);
4646+size_t bindle_length(const struct Bindle *ctx);
50475148/**
5249 * Returns the name of the entry at the given index.
5350 * The string is owned by the Bindle; the caller must NOT free it.
5451 */
5555-const char *bindle_entry_name(const struct BindleContext *ctx, size_t index, size_t *len);
5252+const char *bindle_entry_name(const struct Bindle *ctx, size_t index, size_t *len);
5353+5454+bool bindle_vacuum(struct Bindle *ctx);
56555756#endif /* BINDLE_H */
···11+import subprocess
22+import os
33+import hashlib
44+55+def get_hash(data):
66+ return hashlib.sha256(data).hexdigest()
77+88+def run_test():
99+ test_file = "compat_test.bndl"
1010+ secret_content = b"Consistency is the playground of the gods."
1111+ with open("input.txt", "wb") as f:
1212+ f.write(secret_content)
1313+1414+ print("--- Phase 1: Rust Create -> C Read ---")
1515+ # 1. Create with Rust
1616+ subprocess.run(["cargo", "run", "--", "add",test_file, "msg", "input.txt", "--compress"], check=True)
1717+1818+ # 2. Read with C (Assuming you compiled the C example to ./bindle_c)
1919+ result_c = subprocess.run(["./bindle_c", "cat", test_file, "msg"], capture_output=True)
2020+2121+ if get_hash(result_c.stdout) == get_hash(secret_content):
2222+ print("✅ SUCCESS: C successfully read Rust-compressed data.")
2323+ else:
2424+ print("❌ FAIL: C output does not match original content.")
2525+2626+ print("\n--- Phase 2: C Create -> Rust Read ---")
2727+ # 3. Use C to add a different file (Assuming your C binary has an 'add' command)
2828+ subprocess.run(["./bindle_c", "add", test_file, "c_msg", "input.txt", "1"], check=True) # 1 for compress
2929+3030+ # 4. Use Rust to list and verify
3131+ result_rust = subprocess.run(["cargo", "run", "--", "cat", test_file, "c_msg"], capture_output=True)
3232+3333+ if get_hash(result_rust.stdout) == get_hash(secret_content):
3434+ print("✅ SUCCESS: Rust successfully read C-compressed data.")
3535+ else:
3636+ print("❌ FAIL: Rust output does not match.")
3737+3838+if __name__ == "__main__":
3939+ run_test()
+1-1
src/bin/bindle.rs
···8181 );
8282 println!("{}", "-".repeat(60));
83838484- for (entry, name) in b.entries() {
8484+ for (name, entry) in b.index().iter() {
8585 let size = u64::from_le_bytes(entry.uncompressed_size);
8686 let packed = u64::from_le_bytes(entry.compressed_size);
8787
+94-45
src/ffi.rs
···11+use std::alloc::{Layout, dealloc};
12use std::ffi::CStr;
33+use std::mem;
24use std::os::raw::c_char;
35use std::slice;
4657use crate::Bindle;
6877-/// Opaque handle to a Bindle archive.
88-pub struct BindleContext {
99- pub(crate) inner: Bindle,
1010-}
1111-129#[unsafe(no_mangle)]
1313-pub unsafe extern "C" fn bindle_open(path: *const c_char) -> *mut BindleContext {
1010+pub unsafe extern "C" fn bindle_open(path: *const c_char) -> *mut Bindle {
1411 if path.is_null() {
1512 return std::ptr::null_mut();
1613 }
···2421 };
25222623 match Bindle::open(path_str) {
2727- Ok(b) => Box::into_raw(Box::new(BindleContext { inner: b })),
2424+ Ok(b) => Box::into_raw(Box::new(b)),
2825 Err(_) => std::ptr::null_mut(),
2926 }
3027}
···3229/// Adds a new entry. Returns true on success.
3330#[unsafe(no_mangle)]
3431pub unsafe extern "C" fn bindle_add(
3535- ctx: *mut BindleContext,
3232+ ctx: *mut Bindle,
3633 name: *const c_char,
3734 data: *const u8,
3835 data_len: usize,
···4946 };
50475148 let data_slice = slice::from_raw_parts(data, data_len);
5252- let b = &mut (*ctx).inner;
4949+ let b = &mut (*ctx);
53505451 b.add(name_str, data_slice, compress).is_ok()
5552 }
···57545855/// Commits changes to disk.
5956#[unsafe(no_mangle)]
6060-pub unsafe extern "C" fn bindle_save(ctx: *mut BindleContext) -> bool {
5757+pub unsafe extern "C" fn bindle_save(ctx: *mut Bindle) -> bool {
6158 if ctx.is_null() {
6259 return false;
6360 }
6461 unsafe {
6565- let b = &mut (*ctx).inner;
6262+ let b = &mut (*ctx);
6663 b.save().is_ok()
6764 }
6865}
69667067/// Frees BindleContext
7168#[unsafe(no_mangle)]
7272-pub unsafe extern "C" fn bindle_free(ctx: *mut BindleContext) {
6969+pub unsafe extern "C" fn bindle_close(ctx: *mut Bindle) {
7370 if ctx.is_null() {
7471 return;
7572 }
···78757976#[unsafe(no_mangle)]
8077pub unsafe extern "C" fn bindle_read(
8181- ctx: *mut BindleContext,
7878+ ctx_ptr: *mut Bindle,
8279 name: *const c_char,
8380 out_len: *mut usize,
8481) -> *mut u8 {
8585- if ctx.is_null() || name.is_null() || out_len.is_null() {
8686- return std::ptr::null_mut();
8787- }
8282+ unsafe {
8383+ if ctx_ptr.is_null() || name.is_null() {
8484+ return std::ptr::null_mut();
8585+ }
88868989- unsafe {
9090- let name_str = match CStr::from_ptr(name).to_str() {
8787+ // 1. Convert the C string to a Rust &str
8888+ let c_str = std::ffi::CStr::from_ptr(name);
8989+ let name_str = match c_str.to_str() {
9190 Ok(s) => s,
9291 Err(_) => return std::ptr::null_mut(),
9392 };
94939595- let b = &(*ctx).inner;
9494+ // 2. Access your Rust Bindle struct
9595+ let ctx = &mut *ctx_ptr;
9696+9797+ // 3. The actual data retrieval logic
9898+ // (Assuming your Rust Bindle has a method like .get(name))
9999+ match ctx.read(name_str) {
100100+ Some(bytes) => wrap_in_ffi_header(bytes.as_ref(), out_len),
101101+ None => return std::ptr::null_mut(),
102102+ }
103103+ }
104104+}
105105+106106+/// Internal helper to perform the "Hidden Header" allocation
107107+unsafe fn wrap_in_ffi_header(data: &[u8], out_len: *mut usize) -> *mut u8 {
108108+ unsafe {
109109+ let len = data.len();
110110+ if !out_len.is_null() {
111111+ *out_len = len;
112112+ }
113113+114114+ let size_of_header = std::mem::size_of::<usize>();
115115+ let total_size = size_of_header + len;
116116+ let layout =
117117+ std::alloc::Layout::from_size_align(total_size, std::mem::align_of::<usize>()).unwrap();
118118+119119+ let raw_ptr = std::alloc::alloc(layout);
120120+ if raw_ptr.is_null() {
121121+ return std::ptr::null_mut();
122122+ }
123123+124124+ // Store the length at the start
125125+ *(raw_ptr as *mut usize) = len;
126126+127127+ // Copy data to the payload area
128128+ let data_ptr = raw_ptr.add(size_of_header);
129129+ std::ptr::copy_nonoverlapping(data.as_ptr(), data_ptr, len);
961309797- 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()
131131+ data_ptr
132132+ }
133133+}
134134+135135+#[unsafe(no_mangle)]
136136+pub unsafe extern "C" fn bindle_free_buffer(ptr: *mut u8) {
137137+ unsafe {
138138+ if ptr.is_null() {
139139+ return;
106140 }
141141+142142+ let size_of_header = mem::size_of::<usize>();
143143+144144+ // 1. Step back to find the start of the header
145145+ let raw_ptr = ptr.sub(size_of_header);
146146+147147+ // 2. Read the length we stored there
148148+ let len = *(raw_ptr as *const usize);
149149+150150+ // 3. Reconstruct the layout used during allocation
151151+ let total_size = size_of_header + len;
152152+ let layout = Layout::from_size_align(total_size, mem::align_of::<usize>()).unwrap();
153153+154154+ // 4. Deallocate the entire block
155155+ dealloc(raw_ptr, layout);
107156 }
108157}
109158110159#[unsafe(no_mangle)]
111160pub unsafe extern "C" fn bindle_read_uncompressed_direct(
112112- ctx: *mut BindleContext,
161161+ ctx: *mut Bindle,
113162 name: *const c_char,
114163 out_len: *mut usize,
115164) -> *const u8 {
···123172 Err(_) => return std::ptr::null_mut(),
124173 };
125174126126- let b = &(*ctx).inner;
175175+ let b = &(*ctx);
127176 if let Some(data) = b.read(name_str) {
128177 match data {
129178 std::borrow::Cow::Borrowed(bytes) => bytes.as_ptr(),
···136185}
137186138187#[unsafe(no_mangle)]
139139-pub unsafe extern "C" fn bindle_free_buffer(ptr: *mut u8, len: usize) {
140140- if !ptr.is_null() {
141141- unsafe {
142142- let _ = Vec::from_raw_parts(ptr, len, len);
143143- }
144144- }
145145-}
146146-147147-#[unsafe(no_mangle)]
148148-pub unsafe extern "C" fn bindle_length(ctx: *const BindleContext) -> usize {
188188+pub unsafe extern "C" fn bindle_length(ctx: *const Bindle) -> usize {
149189 if ctx.is_null() {
150190 return 0;
151191 }
152152- unsafe { (*ctx).inner.len() }
192192+ unsafe { (*ctx).len() }
153193}
154194155195/// Returns the name of the entry at the given index.
156196/// The string is owned by the Bindle; the caller must NOT free it.
157197#[unsafe(no_mangle)]
158198pub unsafe extern "C" fn bindle_entry_name(
159159- ctx: *const BindleContext,
199199+ ctx: *const Bindle,
160200 index: usize,
161201 len: *mut usize,
162202) -> *const c_char {
···164204 return std::ptr::null();
165205 }
166206167167- let b = unsafe { &(*ctx).inner };
168168- match b.entries.get(index) {
169169- Some((_, name)) => {
207207+ let b = unsafe { &(*ctx) };
208208+ match b.index.iter().nth(index) {
209209+ Some((name, _)) => {
170210 unsafe {
171211 *len = name.as_bytes().len();
172212 }
···175215 None => std::ptr::null(),
176216 }
177217}
218218+219219+#[unsafe(no_mangle)]
220220+pub unsafe extern "C" fn bindle_vacuum(ctx: *mut Bindle) -> bool {
221221+ if ctx.is_null() {
222222+ return false;
223223+ }
224224+ let b = unsafe { &mut (*ctx) };
225225+ b.vacuum().is_ok()
226226+}
+118-157
src/lib.rs
···11use fs2::FileExt;
22use memmap2::Mmap;
33use std::borrow::Cow;
44+use std::collections::BTreeMap;
45use std::fs::{File, OpenOptions};
56use std::io::{self, Read, Seek, SeekFrom, Write};
66-use std::path::Path;
77+use std::path::{Path, PathBuf};
78use zerocopy::{FromBytes, Immutable, IntoBytes, Unaligned};
89910mod ffi;
10111112const BNDL_MAGIC: &[u8; 8] = b"BINDL001";
1213const BNDL_ALIGN: usize = 8;
1313-const ENTRY_SIZE: usize = std::mem::size_of::<Entry>();
1414-const FOOTER_SIZE: usize = std::mem::size_of::<Footer>();
1414+const ENTRY_SIZE: usize = std::mem::size_of::<BindleEntry>();
1515+const FOOTER_SIZE: usize = std::mem::size_of::<BindleFooter>();
1516const HEADER_SIZE: u64 = 8;
16171718#[repr(C, packed)]
1818-#[derive(FromBytes, Unaligned, IntoBytes, Immutable, Clone, Copy, Debug)]
1919-pub struct Entry {
2020- pub offset: [u8; 8],
1919+#[derive(FromBytes, Unaligned, IntoBytes, Immutable, Clone, Copy, Debug, Default)]
2020+pub struct BindleEntry {
2121+ pub offset: [u8; 8], // Use [u8; 8] for disk stability
2122 pub compressed_size: [u8; 8],
2223 pub uncompressed_size: [u8; 8],
2324 pub crc32: [u8; 4],
···2627 pub _reserved: u8,
2728}
28293030+// Add helpers to convert back to numbers for Rust logic
3131+impl BindleEntry {
3232+ pub fn offset(&self) -> u64 {
3333+ u64::from_le_bytes(self.offset)
3434+ }
3535+3636+ pub fn compressed_size(&self) -> u64 {
3737+ u64::from_le_bytes(self.compressed_size)
3838+ }
3939+4040+ pub fn uncompressed_size(&self) -> u64 {
4141+ u64::from_le_bytes(self.uncompressed_size)
4242+ }
4343+4444+ pub fn name_len(&self) -> usize {
4545+ u16::from_le_bytes(self.name_len) as usize
4646+ }
4747+}
4848+2949#[repr(C, packed)]
3050#[derive(FromBytes, Unaligned, IntoBytes, Immutable, Debug)]
3131-struct Footer {
3232- pub index_offset: [u8; 8],
3333- pub entry_count: [u8; 8],
5151+struct BindleFooter {
5252+ pub index_offset: u64,
5353+ pub entry_count: u64,
3454}
35553656pub struct Bindle {
5757+ path: PathBuf,
3758 file: File,
3859 mmap: Option<Mmap>,
3939- entries: Vec<(Entry, String)>,
6060+ index: BTreeMap<String, BindleEntry>,
4061 data_end: u64,
4162}
42634364impl Bindle {
4465 pub fn open<P: AsRef<Path>>(path: P) -> io::Result<Self> {
6666+ let path_buf = path.as_ref().to_path_buf();
4567 let mut file = OpenOptions::new()
4668 .read(true)
4769 .write(true)
4870 .create(true)
4949- .open(path)?;
5050-7171+ .open(&path_buf)?;
5172 file.lock_shared()?;
52735374 let len = file.metadata()?.len();
5454-5575 if len == 0 {
5656- // New file: Write the magic header immediately
5776 file.write_all(BNDL_MAGIC)?;
5877 return Ok(Self {
7878+ path: path_buf,
5979 file,
6080 mmap: None,
6161- entries: Vec::new(),
8181+ index: BTreeMap::new(),
6282 data_end: HEADER_SIZE,
6383 });
6484 }
65856666- // Existing file: Check header magic
6786 let mut header = [0u8; 8];
6887 file.read_exact(&mut header)?;
6988 if &header != BNDL_MAGIC {
7070- return Err(io::Error::new(
7171- io::ErrorKind::InvalidData,
7272- "Invalid Bindle header",
7373- ));
7474- }
7575- // Case 2: File exists but is too small to even hold a footer
7676- if len < FOOTER_SIZE as u64 {
7777- return Err(io::Error::new(
7878- io::ErrorKind::InvalidData,
7979- "File too small to be a Bindle",
8080- ));
8989+ return Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid header"));
8190 }
82918392 let m = unsafe { Mmap::map(&file)? };
8493 let footer_pos = m.len() - FOOTER_SIZE;
8585-8686- let footer = Footer::read_from_bytes(&m[footer_pos..])
8787- .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid footer alignment"))?;
9494+ let footer = BindleFooter::read_from_bytes(&m[footer_pos..]).unwrap();
88958989- // If magic is valid, proceed to parse the index
9090- let data_end = u64::from_le_bytes(footer.index_offset);
9191- let count = u64::from_le_bytes(footer.entry_count);
9292- let mut entries = Vec::with_capacity(count as usize);
9696+ let data_end = footer.index_offset;
9797+ let count = footer.entry_count;
9898+ let mut index = BTreeMap::new();
939994100 let mut cursor = data_end as usize;
95101 for _ in 0..count {
9696- let entry_bytes = m
9797- .get(cursor..cursor + ENTRY_SIZE)
9898- .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Index out of bounds"))?;
9999- let entry = Entry::read_from_bytes(entry_bytes)
100100- .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid entry"))?;
101101-102102- let n_len = u16::from_le_bytes(entry.name_len) as usize;
102102+ let entry = BindleEntry::read_from_bytes(&m[cursor..cursor + ENTRY_SIZE]).unwrap();
103103 let n_start = cursor + ENTRY_SIZE;
104104- let n_end = n_start + n_len;
105105-106106- let name_bytes = m
107107- .get(n_start..n_end)
108108- .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Name out of bounds"))?;
109109- let name = String::from_utf8_lossy(name_bytes).into_owned();
110110-111111- entries.push((entry, name));
112112-113113- let total = ENTRY_SIZE + n_len;
104104+ let name =
105105+ String::from_utf8_lossy(&m[n_start..n_start + entry.name_len()]).into_owned();
106106+ index.insert(name, entry);
107107+ let total = ENTRY_SIZE + entry.name_len();
114108 cursor += (total + (BNDL_ALIGN - 1)) & !(BNDL_ALIGN - 1);
115109 }
116110117111 Ok(Self {
112112+ path: path_buf,
118113 file,
119114 mmap: Some(m),
120120- entries,
115115+ index,
121116 data_end,
122117 })
123118 }
124119125125- /// Reads data for an entry using Cow to avoid unnecessary copies.
126126- pub fn read<'a>(&'a self, name: &str) -> Option<Cow<'a, [u8]>> {
127127- let (entry, _) = self.entries.iter().find(|(_, n)| n == name)?;
128128- let mmap = self.mmap.as_ref()?;
129129-130130- let offset = u64::from_le_bytes(entry.offset) as usize;
131131- let c_size = u64::from_le_bytes(entry.compressed_size) as usize;
132132- let u_size = u64::from_le_bytes(entry.uncompressed_size) as usize;
133133-134134- let data = mmap.get(offset..offset + c_size)?;
135135-136136- if entry.compression_type == 1 {
137137- let mut out = Vec::with_capacity(u_size);
138138- zstd::Decoder::new(data).ok()?.read_to_end(&mut out).ok()?;
139139- Some(Cow::Owned(out))
140140- } else {
141141- Some(Cow::Borrowed(data))
142142- }
143143- }
144144-145145- /// Streams data directly to a writer (e.g., File, TcpStream) to keep memory usage low.
146146- pub fn read_to_writer<W: Write>(&self, name: &str, mut writer: W) -> io::Result<u64> {
147147- let (entry, _) = self
148148- .entries
149149- .iter()
150150- .find(|(_, n)| n == name)
151151- .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Entry not found"))?;
152152-153153- let mmap = self
154154- .mmap
155155- .as_ref()
156156- .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Archive not mapped"))?;
157157-158158- let offset = u64::from_le_bytes(entry.offset) as usize;
159159- let c_size = u64::from_le_bytes(entry.compressed_size) as usize;
160160- let data = mmap.get(offset..offset + c_size).ok_or_else(|| {
161161- io::Error::new(io::ErrorKind::InvalidData, "Data range out of bounds")
162162- })?;
163163-164164- if entry.compression_type == 1 {
165165- let mut decoder = zstd::Decoder::new(data)?;
166166- io::copy(&mut decoder, &mut writer)
167167- } else {
168168- writer.write_all(data)?;
169169- Ok(data.len() as u64)
170170- }
171171- }
172172-173120 pub fn add(&mut self, name: &str, data: &[u8], compress: bool) -> io::Result<()> {
174174- // Prevent Duplicate Keys
175175- if self
176176- .entries
177177- .iter()
178178- .any(|(_, existing_name)| existing_name == name)
179179- {
180180- return Err(io::Error::new(
181181- io::ErrorKind::AlreadyExists,
182182- format!("Entry '{}' already exists in bindle", name),
183183- ));
184184- }
185185-186186- // Position the file pointer at the end of valid data
187187- // If data_end is 0, we start after the 8-byte Magic Header
188188- let write_pos = if self.data_end >= HEADER_SIZE {
189189- self.data_end
121121+ let (processed, c_type) = if compress {
122122+ (zstd::encode_all(data, 3)?, 1)
190123 } else {
191191- HEADER_SIZE
124124+ (data.to_vec(), 0)
192125 };
193126194194- self.file.seek(SeekFrom::Start(write_pos))?;
127127+ self.file.seek(SeekFrom::Start(self.data_end))?;
128128+ self.file.write_all(&processed)?;
195129196196- // Prepare and write data
197197- let write_data = if compress {
198198- zstd::encode_all(data, 3)?
199199- } else {
200200- data.to_vec()
201201- };
202202-203203- let start_offset = self.file.stream_position()?;
204204- self.file.write_all(&write_data)?;
205205-206206- // Align to 8 bytes for the next entry or index
207207- let current_pos = self.file.stream_position()?;
208208- let pad = (BNDL_ALIGN as u64 - (current_pos % BNDL_ALIGN as u64)) % BNDL_ALIGN as u64;
130130+ let offset = self.data_end;
131131+ let c_size = processed.len() as u64;
132132+ let pad = (8 - (c_size % 8)) % 8;
209133 if pad > 0 {
210134 self.file.write_all(&vec![0u8; pad as usize])?;
211135 }
212136213213- // 5. Update state
214214- self.data_end = self.file.stream_position()?;
215215- let entry = Entry {
216216- offset: start_offset.to_le_bytes(),
217217- compressed_size: (write_data.len() as u64).to_le_bytes(),
137137+ self.data_end = offset + c_size + pad;
138138+139139+ let entry = BindleEntry {
140140+ offset: offset.to_le_bytes(),
141141+ compressed_size: c_size.to_le_bytes(),
218142 uncompressed_size: (data.len() as u64).to_le_bytes(),
219219- crc32: crc32fast::hash(&write_data).to_le_bytes(),
143143+ compression_type: c_type,
220144 name_len: (name.len() as u16).to_le_bytes(),
221221- compression_type: if compress { 1 } else { 0 },
222222- _reserved: 0,
145145+ ..Default::default()
223146 };
224147225225- self.entries.push((entry, name.to_string()));
148148+ self.index.insert(name.to_string(), entry);
226149 Ok(())
227150 }
228151229152 pub fn save(&mut self) -> io::Result<()> {
230153 self.file.lock_exclusive()?;
231231-232154 self.file.seek(SeekFrom::Start(self.data_end))?;
233155 let index_start = self.data_end;
234156235235- for (entry, name) in &self.entries {
157157+ for (name, entry) in &self.index {
236158 self.file.write_all(entry.as_bytes())?;
237159 self.file.write_all(name.as_bytes())?;
238238- let current_disk_size = ENTRY_SIZE + name.len();
239239- let pad = (BNDL_ALIGN - (current_disk_size % BNDL_ALIGN)) % BNDL_ALIGN;
160160+ let pad = (BNDL_ALIGN - ((ENTRY_SIZE + name.len()) % BNDL_ALIGN)) % BNDL_ALIGN;
240161 if pad > 0 {
241162 self.file.write_all(&vec![0u8; pad])?;
242163 }
243164 }
244165245245- let footer = Footer {
246246- index_offset: index_start.to_le_bytes(),
247247- entry_count: (self.entries.len() as u64).to_le_bytes(),
166166+ let footer = BindleFooter {
167167+ index_offset: index_start,
168168+ entry_count: self.index.len() as u64,
248169 };
249249-250170 self.file.write_all(footer.as_bytes())?;
251171 self.file.flush()?;
252172 self.mmap = Some(unsafe { Mmap::map(&self.file)? });
253173 self.file.lock_shared()?;
254254-255174 Ok(())
256175 }
257176258258- /// Returns a list of all entry names in the archive.
259259- pub fn list(&self) -> Vec<&str> {
260260- self.entries.iter().map(|(_, name)| name.as_str()).collect()
177177+ pub fn vacuum(&mut self) -> io::Result<()> {
178178+ let tmp_path = self.path.with_extension("tmp");
179179+ let mut new_file = OpenOptions::new()
180180+ .write(true)
181181+ .create(true)
182182+ .truncate(true)
183183+ .open(&tmp_path)?;
184184+ new_file.write_all(BNDL_MAGIC)?;
185185+ let mut current_offset = HEADER_SIZE;
186186+187187+ for entry in self.index.values_mut() {
188188+ let mut buf = vec![0u8; entry.compressed_size() as usize];
189189+ self.file.seek(SeekFrom::Start(entry.offset()))?;
190190+ self.file.read_exact(&mut buf)?;
191191+192192+ new_file.seek(SeekFrom::Start(current_offset))?;
193193+ new_file.write_all(&buf)?;
194194+195195+ entry.offset = current_offset.to_le_bytes();
196196+ let pad = (8 - (entry.compressed_size() % 8)) % 8;
197197+ if pad > 0 {
198198+ new_file.write_all(&vec![0u8; pad as usize])?;
199199+ }
200200+ current_offset += entry.compressed_size() + pad;
201201+ }
202202+203203+ self.data_end = current_offset;
204204+ self.file = new_file;
205205+ self.save()?;
206206+ std::fs::rename(tmp_path, &self.path)?;
207207+ Ok(())
261208 }
262209263263- pub fn entries(&self) -> &[(Entry, String)] {
264264- &self.entries
210210+ pub fn read<'a>(&'a self, name: &str) -> Option<Cow<'a, [u8]>> {
211211+ let entry = self.index.get(name)?;
212212+ let mmap = self.mmap.as_ref()?;
213213+ let data =
214214+ mmap.get(entry.offset() as usize..(entry.offset() + entry.compressed_size()) as usize)?;
215215+216216+ if entry.compression_type == 1 {
217217+ let mut out = Vec::with_capacity(entry.uncompressed_size() as usize);
218218+ zstd::Decoder::new(data).ok()?.read_to_end(&mut out).ok()?;
219219+ Some(Cow::Owned(out))
220220+ } else {
221221+ Some(Cow::Borrowed(data))
222222+ }
265223 }
266224267267- /// Returns the number of entries.
268225 pub fn len(&self) -> usize {
269269- self.entries.len()
226226+ self.index.len()
270227 }
271228272229 pub fn is_empty(&self) -> bool {
273273- self.entries.is_empty()
230230+ self.index.is_empty()
231231+ }
232232+233233+ pub fn index(&self) -> &BTreeMap<String, BindleEntry> {
234234+ &self.index
274235 }
275236}
276237