···11# bindle-file
2233-`bindle` is a general purpose, binary archive format designed for efficient reads and writes.
33+[bindle](https://en.wikipedia.org/wiki/Bindle) is a general purpose binary archive
44+format for collecting files.
+9-9
SPEC.md
···3030- **Shadowing:** New versions of existing files are simply appended to the end of the data segment. The file remains append-only until a vacuum operation is performed.
31313232### 2.3 Index Entry
3333-The index is a series of entries. Each entry consists of a fixed metadata block followed by a variable-length filename.
3333+The index is a series of entries. Each entry consists of a fixed metadata block followed by a variable-length filename. All multi-byte integers are stored in little-endian byte order.
34343535| Field | Size | Type | Description |
3636| :--- | :--- | :--- | :--- |
3737| `offset` | 8 bytes | u64 | Absolute file offset to the data blob |
3838| `c_size` | 8 bytes | u64 | Compressed size on disk |
3939| `u_size` | 8 bytes | u64 | Original uncompressed size |
4040-| `crc32` | 4 bytes | u32 | Checksum of the stored data |
4040+| `crc32` | 4 bytes | u32 | CRC32 checksum of the uncompressed data |
4141| `name_len` | 2 bytes | u16 | Length of the filename string |
4242-| `comp_type` | 1 byte | u8 | `0` = Raw, `1` = Zstandard |
4242+| `comp_type` | 1 byte | u8 | `0` = None, `1` = Zstd |
4343| `reserved` | 1 byte | u8 | Alignment padding |
4444| `filename` | Variable | UTF-8 | The entry name |
45454646**Padding:** After the filename, the file MUST be padded with null bytes (`\0`) to the next 8-byte boundary before the next entry begins.
47474848### 2.4 Footer
4949-The last 16 bytes of the file are used to locate the index. Both fields are stored in little-endian format.
4949+The last 16 bytes of the file are used to locate the index. All fields are stored in little-endian format.
50505151| Field | Size | Type | Description |
5252| :--- | :--- | :--- | :--- |
5353| `index_offset` | 8 bytes | u64 | Absolute offset to the start of the index |
5454| `entry_count` | 4 bytes | u32 | Total number of unique entries in the index |
5555-| `magic` | 4 bytes | u32 | Magic sentinel value `62 62 62 62` (ASCII: `bbbb`).
5555+| `magic` | 4 bytes | u32 | Magic sentinel value `0x62626262` (ASCII: `bbbb`)
56565757---
5858···6868### 3.2 Vacuuming
6969To reclaim space used by shadowed data:
70701. Create a temporary file and write the `BINDL001` header.
7171-2. Iterate through the **live** index entries only.
7272-3. Copy the referenced data blobs to the new file, updating their offsets in a new in-memory index.
7373-4. Write the new Index and Footer to the temporary file.
7474-5. Atomically replace the old file with the new one.
7171+2. Iterate through the **live** index entries only, copying referenced data from the original.
7272+3. Write the new Index and Footer to the temporary file.
7373+4. Atomically replace the original file with the temporary file.
7474+5. On failure, delete the temporary file.
75757676---
7777
+31-37
src/bindle.rs
···194194 }
195195196196 pub fn vacuum(&mut self) -> io::Result<()> {
197197- let backup_path = self.path.with_extension("backup");
197197+ let temp_path = self.path.with_extension("tmp");
198198199199- // Release locks and close current file
200200- drop(self.mmap.take());
201201- let _ = self.file.unlock();
202202-203203- // Rename original to backup
204204- std::fs::rename(&self.path, &backup_path)?;
205205-206206- // Open backup for reading
207207- let mut backup_file = File::open(&backup_path)?;
208208-209209- // Create new file at original path
199199+ // Create and lock temp file
210200 let result = {
211211- let mut new_file = OpenOptions::new()
201201+ let mut temp_file = OpenOptions::new()
212202 .write(true)
213203 .read(true)
214204 .create(true)
215205 .truncate(true)
216216- .open(&self.path)?;
206206+ .open(&temp_path)?;
217207218218- new_file.write_all(BNDL_MAGIC)?;
208208+ temp_file.lock_exclusive()?;
209209+ temp_file.write_all(BNDL_MAGIC)?;
219210 let mut current_offset = HEADER_SIZE as u64;
220211221221- // Copy only live entries from backup to new file
212212+ // Copy only live entries from original to temp
222213 for entry in self.index.values_mut() {
223214 let mut buf = vec![0u8; entry.compressed_size() as usize];
224224- backup_file.seek(SeekFrom::Start(entry.offset()))?;
225225- backup_file.read_exact(&mut buf)?;
215215+ self.file.seek(SeekFrom::Start(entry.offset()))?;
216216+ self.file.read_exact(&mut buf)?;
226217227227- new_file.seek(SeekFrom::Start(current_offset))?;
228228- new_file.write_all(&buf)?;
218218+ temp_file.seek(SeekFrom::Start(current_offset))?;
219219+ temp_file.write_all(&buf)?;
229220230221 entry.set_offset(current_offset);
231222 let pad = pad::<8, u64>(entry.compressed_size());
232223 if pad > 0 {
233233- write_padding(&mut new_file, pad as usize)?;
224224+ write_padding(&mut temp_file, pad as usize)?;
234225 }
235226 current_offset += entry.compressed_size() + pad;
236227 }
···238229 // Write the index and footer
239230 let index_start = current_offset;
240231 for (name, entry) in &self.index {
241241- new_file.write_all(entry.as_bytes())?;
242242- new_file.write_all(name.as_bytes())?;
232232+ temp_file.write_all(entry.as_bytes())?;
233233+ temp_file.write_all(name.as_bytes())?;
243234 let pad = pad::<BNDL_ALIGN, usize>(ENTRY_SIZE + name.len());
244235 if pad > 0 {
245245- write_padding(&mut new_file, pad)?;
236236+ write_padding(&mut temp_file, pad)?;
246237 }
247238 }
248239249240 let footer = Footer::new(index_start, self.index.len() as u32, FOOTER_MAGIC);
250250- new_file.write_all(footer.as_bytes())?;
251251- new_file.sync_all()?;
241241+ temp_file.write_all(footer.as_bytes())?;
242242+ temp_file.sync_all()?;
252243253244 Ok(())
254245 };
255246256247 // Handle result
257257- match result {
258258- Ok(()) => {
259259- // Success - delete backup
260260- std::fs::remove_file(&backup_path).ok();
261261- }
262262- Err(e) => {
263263- // Failure - restore from backup
264264- std::fs::remove_file(&self.path).ok();
265265- std::fs::rename(&backup_path, &self.path).ok();
266266- return Err(e);
267267- }
248248+ if let Err(e) = result {
249249+ std::fs::remove_file(&temp_path).ok();
250250+ return Err(e);
268251 }
252252+253253+ // Acquire exclusive lock just before rename to prevent concurrent access
254254+ self.file.lock_exclusive()?;
255255+256256+ // Release locks and close current file
257257+ drop(self.mmap.take());
258258+ let _ = self.file.unlock();
259259+260260+ // Atomically replace original with temp
261261+ std::fs::rename(&temp_path, &self.path)?;
269262270263 // Re-open the new file
271264 let file = OpenOptions::new().read(true).write(true).open(&self.path)?;
···435428 }
436429437430 pub fn writer<'a>(&'a mut self, name: &str, compress: Compress) -> io::Result<Writer<'a>> {
431431+ self.file.lock_exclusive()?;
438432 self.file.seek(SeekFrom::Start(self.data_end))?;
439433 let compress = self.should_auto_compress(compress, 0);
440434 let f = self.file.try_clone()?;
+3
src/writer.rs
···86868787 self.bindle.index.insert(self.name.clone(), entry);
8888 self.name.clear(); // Mark as closed
8989+9090+ // Downgrade to shared lock after write completes
9191+ self.bindle.file.lock_shared()?;
8992 Ok(())
9093 }
9194