···11+use clap::{Parser, Subcommand};
22+use std::io::{self, Write};
33+use std::path::PathBuf;
44+use std::process;
55+66+use bindle_file::Bindle;
77+88+#[derive(Parser)]
99+#[command(name = "bindle")]
1010+#[command(version = "1.0")]
1111+#[command(author = "zshipko")]
1212+#[command(about = "Append-only file collection")]
1313+struct Cli {
1414+ #[command(subcommand)]
1515+ command: Commands,
1616+}
1717+1818+#[derive(Subcommand)]
1919+enum Commands {
2020+ /// List all entries in the archive
2121+ List {
2222+ /// Bindle archive file
2323+ #[arg(value_name = "BINDLE_FILE")]
2424+ bindle_file: PathBuf,
2525+ },
2626+2727+ /// Add a file to the archive
2828+ Add {
2929+ /// Bindle archive file
3030+ #[arg(value_name = "BINDLE_FILE")]
3131+ bindle_file: PathBuf,
3232+3333+ /// Name of the entry inside the archive
3434+ name: String,
3535+ /// Path to the local file to read from
3636+ file_path: PathBuf,
3737+ /// Use zstd compression
3838+ #[arg(short, long)]
3939+ compress: bool,
4040+ },
4141+4242+ /// Extract an entry's data to stdout
4343+ Cat {
4444+ /// Bindle archive file
4545+ #[arg(value_name = "BINDLE_FILE")]
4646+ bindle_file: PathBuf,
4747+ /// Name of the entry to extract
4848+ name: String,
4949+ },
5050+}
5151+5252+fn main() {
5353+ let cli = Cli::parse();
5454+5555+ if let Err(e) = handle_command(cli.command) {
5656+ eprintln!("Operation failed: {}", e);
5757+ process::exit(1);
5858+ }
5959+}
6060+6161+fn handle_command(command: Commands) -> io::Result<()> {
6262+ let init = |path| match Bindle::open(path) {
6363+ Ok(bindle) => bindle,
6464+ Err(e) => {
6565+ eprintln!("Error: unable to open '{}'", e);
6666+ process::exit(1);
6767+ }
6868+ };
6969+7070+ match command {
7171+ Commands::List { bindle_file } => {
7272+ let b = init(bindle_file);
7373+ if b.is_empty() {
7474+ println!("Archive is empty.");
7575+ return Ok(());
7676+ }
7777+7878+ println!(
7979+ "{:<20} {:<12} {:<12} {:<10}",
8080+ "NAME", "SIZE", "PACKED", "RATIO"
8181+ );
8282+ println!("{}", "-".repeat(60));
8383+8484+ for (entry, name) in b.entries() {
8585+ let size = u64::from_le_bytes(entry.uncompressed_size);
8686+ let packed = u64::from_le_bytes(entry.compressed_size);
8787+8888+ let ratio = if size > 0 {
8989+ (packed as f64 / size as f64) * 100.0
9090+ } else {
9191+ 100.0
9292+ };
9393+9494+ println!("{:<20} {:<12} {:<12} {:.1}%", name, size, packed, ratio);
9595+ }
9696+ }
9797+9898+ Commands::Add {
9999+ name,
100100+ file_path,
101101+ compress,
102102+ bindle_file,
103103+ } => {
104104+ let mut b = init(bindle_file);
105105+ let data = std::fs::read(&file_path)?;
106106+107107+ println!("Adding '{}' ({} bytes)...", name, data.len());
108108+109109+ b.add(&name, &data, compress)?;
110110+ b.save()?;
111111+112112+ println!("Successfully saved to archive.");
113113+ }
114114+115115+ Commands::Cat { name, bindle_file } => {
116116+ let b = init(bindle_file);
117117+ match b.read(&name) {
118118+ Some(data) => {
119119+ // Write raw bytes to stdout (useful for piping to other tools or files)
120120+ io::stdout().write_all(&data)?;
121121+ }
122122+ None => {
123123+ return Err(io::Error::new(
124124+ io::ErrorKind::NotFound,
125125+ format!("Entry '{}' not found in archive", name),
126126+ ));
127127+ }
128128+ }
129129+ }
130130+ }
131131+ Ok(())
132132+}
+8-4
src/lib.rs
···168168 }
169169170170 pub fn add(&mut self, name: &str, data: &[u8], compress: bool) -> io::Result<()> {
171171- // 1. Prevent Duplicate Keys
171171+ // Prevent Duplicate Keys
172172 if self
173173 .entries
174174 .iter()
···180180 ));
181181 }
182182183183- // 2. Position the file pointer at the end of valid data
183183+ // Position the file pointer at the end of valid data
184184 // If data_end is 0, we start after the 8-byte Magic Header
185185 let write_pos = if self.data_end >= HEADER_SIZE {
186186 self.data_end
···190190191191 self.file.seek(SeekFrom::Start(write_pos))?;
192192193193- // 3. Prepare and write data
193193+ // Prepare and write data
194194 let write_data = if compress {
195195 zstd::encode_all(data, 3)?
196196 } else {
···200200 let start_offset = self.file.stream_position()?;
201201 self.file.write_all(&write_data)?;
202202203203- // 4. Align to 8 bytes for the next entry or index
203203+ // Align to 8 bytes for the next entry or index
204204 let current_pos = self.file.stream_position()?;
205205 let pad = (BNDL_ALIGN as u64 - (current_pos % BNDL_ALIGN as u64)) % BNDL_ALIGN as u64;
206206 if pad > 0 {
···252252 /// Returns a list of all entry names in the archive.
253253 pub fn list(&self) -> Vec<&str> {
254254 self.entries.iter().map(|(_, name)| name.as_str()).collect()
255255+ }
256256+257257+ pub fn entries(&self) -> &[(Entry, String)] {
258258+ &self.entries
255259 }
256260257261 /// Returns the number of entries.