an efficient binary archive format
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add pack/unpack

zach f501d42e 015a25e1

+246 -71
+1
.gitignore
··· 4 4 c/bindle_c 5 5 c/test 6 6 c/input.txt 7 + c/test_dir*
+44 -36
c/bindle-cli.c
··· 4 4 #include <string.h> 5 5 6 6 void print_usage(const char *prog) { 7 - printf("Usage: %s <bindle_file> <command> [args]\n", prog); 7 + printf("Usage: %s <command> <bindle_file> [args]\n", prog); 8 8 printf("Commands:\n"); 9 - printf(" list List all entries\n"); 10 - printf(" cat <name> Output entry content to stdout\n"); 11 - printf(" add <name> <file> Add a file to the archive\n"); 9 + printf(" list List all entries\n"); 10 + printf(" cat <name> Output entry content to stdout\n"); 11 + printf(" add <name> <file> Add a single file to the archive\n"); 12 + printf(" pack <src_dir> Pack a directory into the archive\n"); 13 + printf(" unpack <dest_dir> Unpack archive to a directory\n"); 14 + printf(" vacuum Reclaim space from shadowed entries\n"); 12 15 } 13 16 14 17 int main(int argc, char **argv) { ··· 17 20 return 1; 18 21 } 19 22 23 + // Switched back: command first, then bindle_file 24 + const char *cmd = argv[1]; 20 25 const char *db_path = argv[2]; 21 - const char *cmd = argv[1]; 22 26 23 27 Bindle *b = bindle_open(db_path); 24 28 if (!b) { 25 - fprintf(stderr, "Error: Could not open or create bindle '%s'\n", db_path); 29 + fprintf(stderr, "Error: Could not open bindle '%s'\n", db_path); 26 30 return 1; 27 31 } 28 32 29 33 if (strcmp(cmd, "list") == 0) { 30 34 uint64_t count = bindle_length(b); 31 - printf("%-20s\n", "NAME"); 32 - printf("----------------------------------------------------------\n"); 33 - size_t namelen = 0; 35 + printf("%-30s\n", "NAME"); 36 + printf("------------------------------\n"); 34 37 for (uint64_t i = 0; i < count; i++) { 38 + size_t namelen = 0; 35 39 const char *name = bindle_entry_name(b, i, &namelen); 36 - printf("%*s\n", (int)namelen, name); 40 + printf("%-30s\n", name); 37 41 } 38 42 } else if (strcmp(cmd, "cat") == 0) { 39 43 if (argc < 4) { 40 - fprintf(stderr, "Error: 'cat' requires an entry name\n"); 44 + fprintf(stderr, "Usage: %s cat <file> <name>\n", argv[0]); 45 + bindle_close(b); 41 46 return 1; 42 47 } 43 48 size_t out_len = 0; ··· 45 50 if (data) { 46 51 fwrite(data, 1, out_len, stdout); 47 52 free(data); 48 - } else { 49 - fprintf(stderr, "Error: Entry '%s' not found\n", argv[3]); 50 - return 1; 51 53 } 52 54 } else if (strcmp(cmd, "add") == 0) { 53 55 if (argc < 5) { 54 - fprintf(stderr, "Error: 'add' requires <name> and <file_path>\n"); 56 + fprintf(stderr, "Usage: %s add <file> <name> <src_path>\n", argv[0]); 57 + bindle_close(b); 55 58 return 1; 56 59 } 57 - 58 60 FILE *inf = fopen(argv[4], "rb"); 59 - if (!inf) { 60 - perror("fopen"); 61 + if (inf) { 62 + fseek(inf, 0, SEEK_END); 63 + size_t len = ftell(inf); 64 + fseek(inf, 0, SEEK_SET); 65 + uint8_t *buf = malloc(len); 66 + fread(buf, 1, len, inf); 67 + fclose(inf); 68 + 69 + if (bindle_add(b, argv[3], buf, len, true)) { 70 + bindle_save(b); 71 + } 72 + free(buf); 73 + } 74 + } else if (strcmp(cmd, "pack") == 0) { 75 + if (argc < 4) { 76 + fprintf(stderr, "Usage: %s pack <file> <src_dir>\n", argv[0]); 77 + bindle_close(b); 61 78 return 1; 62 79 } 63 - 64 - fseek(inf, 0, SEEK_END); 65 - size_t len = ftell(inf); 66 - fseek(inf, 0, SEEK_SET); 67 - 68 - uint8_t *buf = malloc(len); 69 - fread(buf, 1, len, inf); 70 - fclose(inf); 71 - 72 - // We'll default to compression (true) for the CLI 73 - if (bindle_add(b, argv[3], buf, len, true)) { 74 - bindle_save(b); 75 - fprintf(stderr, "Added '%s' successfully.\n", argv[3]); 76 - } else { 77 - fprintf(stderr, "Error: Failed to add '%s' (duplicate name?)\n", argv[3]); 80 + bindle_pack(b, argv[3], true); 81 + } else if (strcmp(cmd, "unpack") == 0) { 82 + if (argc < 4) { 83 + fprintf(stderr, "Usage: %s unpack <file> <dest_dir>\n", argv[0]); 84 + bindle_close(b); 85 + return 1; 78 86 } 79 - free(buf); 80 - } else { 81 - print_usage(argv[0]); 87 + bindle_unpack(b, argv[3]); 88 + } else if (strcmp(cmd, "vacuum") == 0) { 89 + bindle_vacuum(b); 82 90 } 83 91 84 92 bindle_close(b);
+94 -29
c/bindle.c
··· 1 1 #include "bindle.h" 2 2 3 3 #include <dirent.h> 4 + #include <errno.h> 4 5 #include <limits.h> 5 6 #include <stdio.h> 6 7 #include <stdlib.h> ··· 329 330 return true; 330 331 } 331 332 332 - bool bindle_unpack(Bindle *b, const char *dest_dir) { 333 - mkdir(dest_dir, 0755); 334 - for (uint64_t i = 0; i < b->count; i++) { 335 - size_t len; 336 - uint8_t *data = bindle_read(b, b->entries[i].name, &len); 337 - if (data) { 338 - char path[1024]; 339 - snprintf(path, sizeof(path), "%s/%s", dest_dir, b->entries[i].name); 340 - FILE *f = fopen(path, "wb"); 341 - if (f) { 342 - fwrite(data, 1, len, f); 343 - fclose(f); 344 - } 345 - free(data); 333 + /* --- Helper: Recursive directory creation (mkdir -p) --- */ 334 + static void ensure_dir_exists(const char *path) { 335 + char tmp[1024]; 336 + char *p = NULL; 337 + size_t len; 338 + 339 + snprintf(tmp, sizeof(tmp), "%s", path); 340 + len = strlen(tmp); 341 + if (tmp[len - 1] == '/') 342 + tmp[len - 1] = 0; 343 + 344 + for (p = tmp + 1; *p; p++) { 345 + if (*p == '/') { 346 + *p = 0; 347 + mkdir(tmp, 0755); 348 + *p = '/'; 346 349 } 347 350 } 348 - return true; 349 351 } 350 352 351 - bool bindle_pack(Bindle *b, const char *src_dir, BindleCompress compress) { 352 - DIR *dir = opendir(src_dir); 353 + /* --- Recursive Pack Implementation --- */ 354 + static bool pack_recursive(Bindle *b, const char *base_path, 355 + const char *current_path, bool compress) { 356 + DIR *dir = opendir(current_path); 353 357 if (!dir) 354 358 return false; 355 - char path[PATH_MAX]; 359 + 356 360 struct dirent *entry; 357 - struct stat st; 358 - while ((entry = readdir(dir))) { 359 - if (entry->d_name[0] == '.') 361 + while ((entry = readdir(dir)) != NULL) { 362 + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) 360 363 continue; 361 - snprintf(path, sizeof(path), "%s/%s", src_dir, entry->d_name); 362 - if (stat(path, &st) == 0 && S_ISREG(st.st_mode)) { 363 - FILE *f = fopen(path, "rb"); 364 - uint8_t *buf = malloc(st.st_size); 365 - fread(buf, 1, st.st_size, f); 364 + 365 + char full_path[PATH_MAX]; 366 + snprintf(full_path, sizeof(full_path), "%s/%s", current_path, 367 + entry->d_name); 368 + 369 + struct stat st; 370 + if (stat(full_path, &st) == -1) 371 + continue; 372 + 373 + if (S_ISDIR(st.st_mode)) { 374 + // Recurse into subdirectory 375 + pack_recursive(b, base_path, full_path, compress); 376 + } else if (S_ISREG(st.st_mode)) { 377 + // Calculate relative name (e.g., "subdir/file.txt") 378 + // We skip the base_path length + 1 (for the slash) 379 + const char *relative_name = full_path + strlen(base_path); 380 + if (*relative_name == '/') 381 + relative_name++; 382 + 383 + FILE *f = fopen(full_path, "rb"); 384 + if (!f) 385 + continue; 386 + 387 + uint8_t *buffer = malloc(st.st_size); 388 + if (fread(buffer, 1, st.st_size, f) == (size_t)st.st_size) { 389 + bindle_add(b, relative_name, buffer, st.st_size, 390 + compress ? BindleCompressZstd : BindleCompressNone); 391 + } 366 392 fclose(f); 367 - bindle_add(b, entry->d_name, buf, st.st_size, compress); 368 - free(buf); 393 + free(buffer); 369 394 } 370 395 } 371 396 closedir(dir); 372 - return bindle_save(b); 397 + return true; 398 + } 399 + 400 + bool bindle_pack(Bindle *b, const char *src_dir, BindleCompress compress) { 401 + if (!b || !src_dir) 402 + return false; 403 + bool success = pack_recursive(b, src_dir, src_dir, compress); 404 + if (success) { 405 + return bindle_save(b); 406 + } 407 + return false; 408 + } 409 + 410 + /* --- Updated Unpack Implementation --- */ 411 + bool bindle_unpack(Bindle *b, const char *dest_dir) { 412 + if (!b || !dest_dir) 413 + return false; 414 + 415 + mkdir(dest_dir, 0755); 416 + 417 + for (uint64_t i = 0; i < b->count; i++) { 418 + size_t out_len = 0; 419 + uint8_t *data = bindle_read(b, b->entries[i].name, &out_len); 420 + 421 + if (data) { 422 + char full_path[PATH_MAX]; 423 + snprintf(full_path, sizeof(full_path), "%s/%s", dest_dir, 424 + b->entries[i].name); 425 + 426 + // Recreate directory structure before writing file 427 + ensure_dir_exists(full_path); 428 + 429 + FILE *out = fopen(full_path, "wb"); 430 + if (out) { 431 + fwrite(data, 1, out_len, out); 432 + fclose(out); 433 + } 434 + free(data); 435 + } 436 + } 437 + return true; 373 438 }
+36
c/test.py
··· 1 1 import subprocess 2 2 import os 3 3 import hashlib 4 + import shutil 4 5 5 6 def get_hash(data): 6 7 return hashlib.sha256(data).hexdigest() ··· 35 36 else: 36 37 print("❌ FAIL: Rust output does not match.") 37 38 39 + def run_pack_test(): 40 + test_file = "compat_test.bndl" 41 + src_dir = "test_dir_rust" 42 + dest_dir = "test_dir_c" 43 + 44 + # Cleanup 45 + if os.path.exists(src_dir): shutil.rmtree(src_dir) 46 + if os.path.exists(dest_dir): shutil.rmtree(dest_dir) 47 + os.makedirs(src_dir) 48 + 49 + # 1. Create files for Rust to pack 50 + with open(os.path.join(src_dir, "hello.txt"), "w") as f: 51 + f.write("Cross-language directory packing works!") 52 + 53 + print("--- Phase 3: Rust Pack -> C Unpack ---") 54 + 55 + # Rust Packs 56 + subprocess.run(["cargo", "run", "--", "pack", test_file, src_dir, "--compress"], check=True) 57 + 58 + # C Unpacks 59 + subprocess.run(["./bindle_c", "unpack", test_file, dest_dir], check=True) 60 + 61 + # Verify 62 + unpacked_file = os.path.join(dest_dir, "hello.txt") 63 + if os.path.exists(unpacked_file): 64 + with open(unpacked_file, "r") as f: 65 + content = f.read() 66 + if content == "Cross-language directory packing works!": 67 + print("✅ SUCCESS: C successfully unpacked Rust-packed directory.") 68 + else: 69 + print("❌ FAIL: Content mismatch in unpacked file.") 70 + else: 71 + print("❌ FAIL: Unpacked file missing.") 72 + 38 73 if __name__ == "__main__": 39 74 run_test() 75 + run_pack_test()
+71 -6
src/bin/bindle.rs
··· 47 47 /// Name of the entry to extract 48 48 name: String, 49 49 }, 50 + 51 + /// Pack an entire directory into the archive 52 + Pack { 53 + /// Bindle archive file 54 + #[arg(value_name = "BINDLE_FILE")] 55 + bindle_file: PathBuf, 56 + /// Local directory to pack 57 + #[arg(value_name = "SRC_DIR")] 58 + src_dir: PathBuf, 59 + /// Use zstd compression 60 + #[arg(short, long)] 61 + compress: bool, 62 + }, 63 + 64 + /// Unpack the archive to a local directory 65 + Unpack { 66 + /// Bindle archive file 67 + #[arg(value_name = "BINDLE_FILE")] 68 + bindle_file: PathBuf, 69 + /// Destination directory 70 + #[arg(value_name = "DEST_DIR")] 71 + dest_dir: PathBuf, 72 + }, 73 + 74 + /// Reclaim space by removing shadowed/deleted data 75 + Vacuum { 76 + /// Bindle archive file 77 + #[arg(value_name = "BINDLE_FILE")] 78 + bindle_file: PathBuf, 79 + }, 50 80 } 51 81 52 82 fn main() { ··· 76 106 } 77 107 78 108 println!( 79 - "{:<20} {:<12} {:<12} {:<10}", 109 + "{:<30} {:<12} {:<12} {:<10}", 80 110 "NAME", "SIZE", "PACKED", "RATIO" 81 111 ); 82 - println!("{}", "-".repeat(60)); 112 + println!("{}", "-".repeat(70)); 83 113 84 114 for (name, entry) in b.index().iter() { 85 - let size = u64::from_le_bytes(entry.uncompressed_size); 86 - let packed = u64::from_le_bytes(entry.compressed_size); 115 + let size = entry.uncompressed_size(); 116 + let packed = entry.compressed_size(); 87 117 88 118 let ratio = if size > 0 { 89 119 (packed as f64 / size as f64) * 100.0 ··· 91 121 100.0 92 122 }; 93 123 94 - println!("{:<20} {:<12} {:<12} {:.1}%", name, size, packed, ratio); 124 + println!("{:<30} {:<12} {:<12} {:.1}%", name, size, packed, ratio); 95 125 } 96 126 } 97 127 ··· 124 154 let b = init(bindle_file); 125 155 match b.read(&name) { 126 156 Some(data) => { 127 - // Write raw bytes to stdout (useful for piping to other tools or files) 128 157 io::stdout().write_all(&data)?; 129 158 } 130 159 None => { ··· 134 163 )); 135 164 } 136 165 } 166 + } 167 + 168 + Commands::Pack { 169 + bindle_file, 170 + src_dir, 171 + compress, 172 + } => { 173 + let mut b = init(bindle_file); 174 + println!("Packing directory '{:?}'...", src_dir); 175 + b.pack( 176 + src_dir, 177 + if compress { 178 + Compress::Zstd 179 + } else { 180 + Compress::None 181 + }, 182 + )?; 183 + b.save()?; 184 + println!("Done."); 185 + } 186 + 187 + Commands::Unpack { 188 + bindle_file, 189 + dest_dir, 190 + } => { 191 + let b = init(bindle_file); 192 + println!("Unpacking to '{:?}'...", dest_dir); 193 + b.unpack(dest_dir)?; 194 + println!("Done."); 195 + } 196 + 197 + Commands::Vacuum { bindle_file } => { 198 + let mut b = init(bindle_file); 199 + println!("Vacuuming archive to reclaim space..."); 200 + b.vacuum()?; 201 + println!("Vacuum complete."); 137 202 } 138 203 } 139 204 Ok(())