···9999bool bindle_exists(const struct Bindle *ctx, const char *name);
100100101101/**
102102+ * Remove an entry from the index.
103103+ * The data remains in the file until bindle_vacuum is called.
104104+ * Returns true if the entry existed and was removed, false otherwise.
105105+ */
106106+bool bindle_remove(struct Bindle *ctx, const char *name);
107107+108108+/**
102109 * Create a new Writer, while the stream is active (until bindle_stream_finish is called), the
103110 * Bindle struct should not be accessed.
104111 */
+56-1
src/bin/bindle.rs
···3737 /// Use zstd compression
3838 #[arg(short, long)]
3939 compress: bool,
4040+ /// Run vacuum after adding
4141+ #[arg(long)]
4242+ vacuum: bool,
4043 },
41444245 /// Extract an entry's data to stdout
···4649 bindle_file: PathBuf,
4750 /// Name of the entry to extract
4851 name: String,
5252+ },
5353+5454+ /// Remove an entry from the archive
5555+ Remove {
5656+ /// Bindle archive file
5757+ #[arg(value_name = "BINDLE_FILE")]
5858+ bindle_file: PathBuf,
5959+ /// Name of the entry to remove
6060+ name: String,
6161+ /// Run vacuum after removing
6262+ #[arg(long)]
6363+ vacuum: bool,
4964 },
50655166 /// Pack an entire directory into the archive
···6277 /// Append to existing file
6378 #[arg(short, long)]
6479 append: bool,
8080+ /// Run vacuum after packing
8181+ #[arg(long)]
8282+ vacuum: bool,
6583 },
66846785 /// Unpack the archive to a local directory
···131149 file_path,
132150 compress,
133151 bindle_file,
152152+ vacuum,
134153 } => {
135154 let mut b = init(bindle_file.clone());
136155 let data = std::fs::read(&file_path)?;
···151170 data.len()
152171 );
153172 b.save()?;
173173+174174+ if vacuum {
175175+ println!("VACUUM {}", bindle_file.display());
176176+ b.vacuum()?;
177177+ }
154178155179 println!("OK");
156180 }
···170194 }
171195 }
172196197197+ Commands::Remove {
198198+ name,
199199+ bindle_file,
200200+ vacuum,
201201+ } => {
202202+ let mut b = init(bindle_file.clone());
203203+ if b.remove(&name) {
204204+ println!("REMOVE '{}' from {}", name, bindle_file.display());
205205+ b.save()?;
206206+207207+ if vacuum {
208208+ println!("VACUUM {}", bindle_file.display());
209209+ b.vacuum()?;
210210+ }
211211+212212+ println!("OK");
213213+ } else {
214214+ return Err(io::Error::new(
215215+ io::ErrorKind::NotFound,
216216+ format!("ERROR '{}' not found in {}", name, bindle_file.display()),
217217+ ));
218218+ }
219219+ }
220220+173221 Commands::Pack {
174222 bindle_file,
175223 src_dir,
176224 compress,
177225 append,
226226+ vacuum,
178227 } => {
179228 println!("PACK {} -> {}", src_dir.display(), bindle_file.display());
180180- let mut b = init(bindle_file);
229229+ let mut b = init(bindle_file.clone());
181230 if !append {
182231 b.clear();
183232 }
···190239 },
191240 )?;
192241 b.save()?;
242242+243243+ if vacuum {
244244+ println!("VACUUM {}", bindle_file.display());
245245+ b.vacuum()?;
246246+ }
247247+193248 println!("OK");
194249 }
195250
+20
src/ffi.rs
···305305 b.exists(name_str)
306306}
307307308308+/// Remove an entry from the index.
309309+/// The data remains in the file until bindle_vacuum is called.
310310+/// Returns true if the entry existed and was removed, false otherwise.
311311+#[unsafe(no_mangle)]
312312+pub unsafe extern "C" fn bindle_remove(ctx: *mut Bindle, name: *const c_char) -> bool {
313313+ if ctx.is_null() || name.is_null() {
314314+ return false;
315315+ }
316316+317317+ let b = unsafe { &mut *ctx };
318318+ let name_str = unsafe {
319319+ match CStr::from_ptr(name).to_str() {
320320+ Ok(s) => s,
321321+ Err(_) => return false,
322322+ }
323323+ };
324324+325325+ b.remove(name_str)
326326+}
327327+308328/// Create a new Writer, while the stream is active (until bindle_stream_finish is called), the
309329/// Bindle struct should not be accessed.
310330#[unsafe(no_mangle)]
+51
src/lib.rs
···405405 magic: FOOTER_MAGIC,
406406 };
407407 self.file.write_all(footer.as_bytes())?;
408408+409409+ // Truncate file to current position to remove any old data
410410+ let current_pos = self.file.stream_position()?;
411411+ self.file.set_len(current_pos)?;
408412 self.file.flush()?;
413413+409414 self.mmap = Some(unsafe { Mmap::map(&self.file)? });
410415 self.file.lock_shared()?;
411416 Ok(())
···582587 /// Checks if an entry exists in the archive index.
583588 pub fn exists(&self, name: &str) -> bool {
584589 self.index.contains_key(name)
590590+ }
591591+592592+ /// Remove an entry from the index.
593593+ /// The data remains in the file until vacuum() is called.
594594+ /// Returns true if the entry existed and was removed.
595595+ pub fn remove(&mut self, name: &str) -> bool {
596596+ self.index.remove(name).is_some()
585597 }
586598587599 /// Recursively packs a directory into the archive.
···968980 }
969981970982 let _ = std::fs::remove_file(path);
983983+ }
984984+985985+ #[test]
986986+ fn test_remove_entry() {
987987+ let path = "test_remove.bindl";
988988+ let _ = fs::remove_file(path);
989989+990990+ let mut b = Bindle::open(path).expect("Failed to open");
991991+992992+ // Add some entries
993993+ b.add("file1.txt", b"Content 1", Compress::None).unwrap();
994994+ b.add("file2.txt", b"Content 2", Compress::None).unwrap();
995995+ b.add("file3.txt", b"Content 3", Compress::None).unwrap();
996996+ b.save().unwrap();
997997+998998+ assert_eq!(b.len(), 3);
999999+ assert!(b.exists("file2.txt"));
10001000+10011001+ // Remove an entry
10021002+ assert!(b.remove("file2.txt"));
10031003+ assert_eq!(b.len(), 2);
10041004+ assert!(!b.exists("file2.txt"));
10051005+10061006+ // Try to remove non-existent entry
10071007+ assert!(!b.remove("nonexistent.txt"));
10081008+10091009+ // Save and reload to verify persistence
10101010+ b.save().unwrap();
10111011+ let b2 = Bindle::open(path).unwrap();
10121012+ assert_eq!(b2.len(), 2);
10131013+ assert!(b2.exists("file1.txt"));
10141014+ assert!(!b2.exists("file2.txt"));
10151015+ assert!(b2.exists("file3.txt"));
10161016+10171017+ // Verify data still readable for remaining entries
10181018+ assert_eq!(b2.read("file1.txt").unwrap().as_ref(), b"Content 1");
10191019+ assert_eq!(b2.read("file3.txt").unwrap().as_ref(), b"Content 3");
10201020+10211021+ fs::remove_file(path).ok();
9711022 }
9721023}