an efficient binary archive format
0
fork

Configure Feed

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

add cli

zach 90fa97c3 60e8d5ea

+151 -5
+11 -1
Cargo.toml
··· 1 1 [package] 2 - name = "bindle" 2 + name = "bindle-file" 3 3 version = "0.1.0" 4 4 edition = "2024" 5 5 6 6 [lib] 7 7 crate-type = ["cdylib", "staticlib", "rlib"] 8 8 9 + [[bin]] 10 + name = "bindle" 11 + required-features = ["cli"] 12 + 13 + 9 14 [dependencies] 10 15 crc32fast = "1.5.0" 11 16 memmap2 = "0.9.9" 12 17 zerocopy = { version = "0.8.38", features = ["std", "derive"] } 13 18 zstd = "0.13.3" 19 + clap = { version = "4.5", features = ["derive"], optional = true } 20 + 21 + [features] 22 + default = ["cli"] 23 + cli = ["clap"] 14 24 15 25 [build-dependencies] 16 26 cbindgen = "0.29"
+132
src/bin/bindle.rs
··· 1 + use clap::{Parser, Subcommand}; 2 + use std::io::{self, Write}; 3 + use std::path::PathBuf; 4 + use std::process; 5 + 6 + use bindle_file::Bindle; 7 + 8 + #[derive(Parser)] 9 + #[command(name = "bindle")] 10 + #[command(version = "1.0")] 11 + #[command(author = "zshipko")] 12 + #[command(about = "Append-only file collection")] 13 + struct Cli { 14 + #[command(subcommand)] 15 + command: Commands, 16 + } 17 + 18 + #[derive(Subcommand)] 19 + enum Commands { 20 + /// List all entries in the archive 21 + List { 22 + /// Bindle archive file 23 + #[arg(value_name = "BINDLE_FILE")] 24 + bindle_file: PathBuf, 25 + }, 26 + 27 + /// Add a file to the archive 28 + Add { 29 + /// Bindle archive file 30 + #[arg(value_name = "BINDLE_FILE")] 31 + bindle_file: PathBuf, 32 + 33 + /// Name of the entry inside the archive 34 + name: String, 35 + /// Path to the local file to read from 36 + file_path: PathBuf, 37 + /// Use zstd compression 38 + #[arg(short, long)] 39 + compress: bool, 40 + }, 41 + 42 + /// Extract an entry's data to stdout 43 + Cat { 44 + /// Bindle archive file 45 + #[arg(value_name = "BINDLE_FILE")] 46 + bindle_file: PathBuf, 47 + /// Name of the entry to extract 48 + name: String, 49 + }, 50 + } 51 + 52 + fn main() { 53 + let cli = Cli::parse(); 54 + 55 + if let Err(e) = handle_command(cli.command) { 56 + eprintln!("Operation failed: {}", e); 57 + process::exit(1); 58 + } 59 + } 60 + 61 + fn handle_command(command: Commands) -> io::Result<()> { 62 + let init = |path| match Bindle::open(path) { 63 + Ok(bindle) => bindle, 64 + Err(e) => { 65 + eprintln!("Error: unable to open '{}'", e); 66 + process::exit(1); 67 + } 68 + }; 69 + 70 + match command { 71 + Commands::List { bindle_file } => { 72 + let b = init(bindle_file); 73 + if b.is_empty() { 74 + println!("Archive is empty."); 75 + return Ok(()); 76 + } 77 + 78 + println!( 79 + "{:<20} {:<12} {:<12} {:<10}", 80 + "NAME", "SIZE", "PACKED", "RATIO" 81 + ); 82 + println!("{}", "-".repeat(60)); 83 + 84 + for (entry, name) in b.entries() { 85 + let size = u64::from_le_bytes(entry.uncompressed_size); 86 + let packed = u64::from_le_bytes(entry.compressed_size); 87 + 88 + let ratio = if size > 0 { 89 + (packed as f64 / size as f64) * 100.0 90 + } else { 91 + 100.0 92 + }; 93 + 94 + println!("{:<20} {:<12} {:<12} {:.1}%", name, size, packed, ratio); 95 + } 96 + } 97 + 98 + Commands::Add { 99 + name, 100 + file_path, 101 + compress, 102 + bindle_file, 103 + } => { 104 + let mut b = init(bindle_file); 105 + let data = std::fs::read(&file_path)?; 106 + 107 + println!("Adding '{}' ({} bytes)...", name, data.len()); 108 + 109 + b.add(&name, &data, compress)?; 110 + b.save()?; 111 + 112 + println!("Successfully saved to archive."); 113 + } 114 + 115 + Commands::Cat { name, bindle_file } => { 116 + let b = init(bindle_file); 117 + match b.read(&name) { 118 + Some(data) => { 119 + // Write raw bytes to stdout (useful for piping to other tools or files) 120 + io::stdout().write_all(&data)?; 121 + } 122 + None => { 123 + return Err(io::Error::new( 124 + io::ErrorKind::NotFound, 125 + format!("Entry '{}' not found in archive", name), 126 + )); 127 + } 128 + } 129 + } 130 + } 131 + Ok(()) 132 + }
+8 -4
src/lib.rs
··· 168 168 } 169 169 170 170 pub fn add(&mut self, name: &str, data: &[u8], compress: bool) -> io::Result<()> { 171 - // 1. Prevent Duplicate Keys 171 + // Prevent Duplicate Keys 172 172 if self 173 173 .entries 174 174 .iter() ··· 180 180 )); 181 181 } 182 182 183 - // 2. Position the file pointer at the end of valid data 183 + // Position the file pointer at the end of valid data 184 184 // If data_end is 0, we start after the 8-byte Magic Header 185 185 let write_pos = if self.data_end >= HEADER_SIZE { 186 186 self.data_end ··· 190 190 191 191 self.file.seek(SeekFrom::Start(write_pos))?; 192 192 193 - // 3. Prepare and write data 193 + // Prepare and write data 194 194 let write_data = if compress { 195 195 zstd::encode_all(data, 3)? 196 196 } else { ··· 200 200 let start_offset = self.file.stream_position()?; 201 201 self.file.write_all(&write_data)?; 202 202 203 - // 4. Align to 8 bytes for the next entry or index 203 + // Align to 8 bytes for the next entry or index 204 204 let current_pos = self.file.stream_position()?; 205 205 let pad = (BNDL_ALIGN as u64 - (current_pos % BNDL_ALIGN as u64)) % BNDL_ALIGN as u64; 206 206 if pad > 0 { ··· 252 252 /// Returns a list of all entry names in the archive. 253 253 pub fn list(&self) -> Vec<&str> { 254 254 self.entries.iter().map(|(_, name)| name.as_str()).collect() 255 + } 256 + 257 + pub fn entries(&self) -> &[(Entry, String)] { 258 + &self.entries 255 259 } 256 260 257 261 /// Returns the number of entries.