use std::io::{self, Write};
mod bindle;
mod compress;
mod entry;
mod reader;
mod writer;
pub(crate) mod ffi;
pub use bindle::Bindle;
pub use compress::Compress;
pub use entry::Entry;
pub use reader::Reader;
pub use writer::Writer;
pub(crate) const BNDL_MAGIC: &[u8; 8] = b"BINDL001";
pub(crate) const BNDL_ALIGN: usize = 8;
pub(crate) const ENTRY_SIZE: usize = std::mem::size_of::<Entry>();
pub(crate) const FOOTER_SIZE: usize = std::mem::size_of::<entry::Footer>();
pub(crate) const HEADER_SIZE: usize = 8;
pub(crate) const AUTO_COMPRESS_THRESHOLD: usize = 2048;
pub(crate) const FOOTER_MAGIC: u32 = 0x62626262;
const ZEROS: &[u8; 64] = &[0u8; 64];
pub(crate) fn pad<
const SIZE: usize,
T: Copy + TryFrom<usize> + std::ops::Sub<T, Output = T> + std::ops::Rem<T, Output = T>,
>(
n: T,
) -> T
where
<T as std::ops::Sub>::Output: std::ops::Rem<T>,
{
if let Ok(size) = T::try_from(SIZE) {
return (size - (n % size)) % size;
}
unreachable!()
}
pub(crate) fn write_padding<W: Write>(writer: &mut W, len: usize) -> io::Result<()> {
let mut remaining = len;
while remaining > 0 {
let chunk = remaining.min(ZEROS.len());
writer.write_all(&ZEROS[..chunk])?;
remaining -= chunk;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::fs::OpenOptions;
use std::io::{Seek, SeekFrom};
#[test]
fn test_create_and_read() {
let path = "test_basic.bindl";
let data = b"Hello, Bindle World!";
{
let mut fp = Bindle::open(path).expect("Failed to open");
fp.add("hello.txt", data, Compress::None)
.expect("Failed to add");
fp.save().expect("Failed to commit");
}
{
let fp = Bindle::open(path).expect("Failed to re-open");
let result = fp.read("hello.txt").expect("File not found");
assert_eq!(result.as_ref(), data);
}
fs::remove_file(path).ok();
}
#[test]
fn test_zstd_compression() {
let path = "test_zstd.bindl";
let data = vec![b'A'; 1000];
{
let mut fp = Bindle::open(path).expect("Failed to open");
fp.add("large.bin", &data, Compress::Zstd)
.expect("Failed to add");
fp.save().expect("Failed to commit");
}
let fp = Bindle::open(path).expect("Failed to re-open");
let result = fp.read("large.bin").expect("File not found");
assert_eq!(result, data);
let meta = fs::metadata(path).unwrap();
assert!(meta.len() < 1000);
fs::remove_file(path).ok();
}
#[test]
fn test_append_functionality() {
let path = "test_append.bindl";
let _ = std::fs::remove_file(path);
{
let mut fp = Bindle::open(path).expect("Fail open 1");
fp.add("1.txt", b"First", Compress::Zstd).unwrap();
fp.save().expect("Fail commit 1");
}
{
let mut fp = Bindle::open(path).expect("Fail open 2");
fp.add("2.txt", b"Second", Compress::None).unwrap();
fp.save().expect("Fail commit 2");
let first = fp.read("1.txt").expect("Could not find 1.txt");
let second = fp.read("2.txt").expect("Could not find 2.txt");
assert_eq!(first.as_ref(), b"First");
assert_eq!(second.as_ref(), b"Second");
}
let _ = std::fs::remove_file(path);
}
#[test]
fn test_invalid_magic() {
let path = "invalid.bindl";
fs::write(path, b"NOT_A_PACK_FILE_AT_ALL").unwrap();
let res = Bindle::open(path);
assert!(res.is_err());
fs::remove_file(path).ok();
}
#[test]
fn test_key_shadowing() {
let path = "test_shadow.bindl";
let _ = fs::remove_file(path);
let mut b = Bindle::open(path).expect("Failed to open");
b.add("config.txt", b"v1", Compress::None).unwrap();
b.save().unwrap();
b.add("config.txt", b"version_2_is_longer", Compress::None)
.unwrap();
b.save().unwrap();
let b2 = Bindle::open(path).expect("Failed to reopen");
let result = b2.read("config.txt").unwrap();
assert_eq!(result.as_ref(), b"version_2_is_longer");
assert_eq!(b2.len(), 1);
fs::remove_file(path).ok();
}
#[test]
fn test_vacuum_reclaims_space() {
let path = "test_vacuum.bindl";
let _ = fs::remove_file(path);
let mut b = Bindle::open(path).expect("Failed to open");
let large_data = vec![0u8; 1024];
b.add("large.bin", &large_data, Compress::None).unwrap();
b.save().unwrap();
let size_v1 = fs::metadata(path).unwrap().len();
b.add("large.bin", b"tiny", Compress::None).unwrap();
b.save().unwrap();
let size_v2 = fs::metadata(path).unwrap().len();
assert!(size_v2 > size_v1);
b.vacuum().expect("Vacuum failed");
let size_v3 = fs::metadata(path).unwrap().len();
assert!(size_v3 < size_v2);
let b2 = Bindle::open(path).unwrap();
assert_eq!(b2.read("large.bin").unwrap().as_ref(), b"tiny");
fs::remove_file(path).ok();
}
#[test]
fn test_directory_pack_unpack_roundtrip() {
let bindle_path = "roundtrip.bindl";
let src_dir = "test_src";
let out_dir = "test_out";
let _ = fs::remove_dir_all(src_dir);
let _ = fs::remove_dir_all(out_dir);
let _ = fs::remove_file(bindle_path);
fs::create_dir_all(format!("{}/subdir", src_dir)).unwrap();
fs::write(format!("{}/file1.txt", src_dir), b"Hello World").unwrap();
fs::write(
format!("{}/subdir/file2.txt", src_dir),
b"Compressed Data Content",
)
.unwrap();
{
let mut b = Bindle::open(bindle_path).unwrap();
b.pack(src_dir, Compress::Zstd).expect("Pack failed");
b.save().expect("Save failed");
}
{
let b = Bindle::open(bindle_path).unwrap();
b.unpack(out_dir).expect("Unpack failed");
}
let content1 = fs::read_to_string(format!("{}/file1.txt", out_dir)).unwrap();
let content2 = fs::read_to_string(format!("{}/subdir/file2.txt", out_dir)).unwrap();
assert_eq!(content1, "Hello World");
assert_eq!(content2, "Compressed Data Content");
fs::remove_dir_all(src_dir).ok();
fs::remove_dir_all(out_dir).ok();
fs::remove_file(bindle_path).ok();
}
#[test]
fn test_streaming_manual_chunks() {
let path = "test_stream.bindl";
let _ = std::fs::remove_file(path);
let chunk1 = b"Hello ";
let chunk2 = b"Streaming ";
let chunk3 = b"World!";
let expected = b"Hello Streaming World!";
{
let mut b = Bindle::open(path).expect("Failed to open");
let mut s = b
.writer("streamed_file.txt", Compress::None)
.expect("Failed to start stream");
s.write_chunk(chunk1).unwrap();
s.write_chunk(chunk2).unwrap();
s.write_chunk(chunk3).unwrap();
s.close().expect("Failed to finish stream");
b.save().expect("Failed to save");
}
let b = Bindle::open(path).expect("Failed to reopen");
let result = b.read("streamed_file.txt").expect("Entry not found");
assert_eq!(result.as_ref(), expected);
assert_eq!(result.len(), expected.len());
let _ = std::fs::remove_file(path);
}
#[test]
fn test_crc32_corruption_detection() {
let path = "test_crc32.bindl";
let _ = std::fs::remove_file(path);
let data = b"Test data for CRC32 verification";
{
let mut b = Bindle::open(path).expect("Failed to open");
b.add("test.txt", data, Compress::None).unwrap();
b.save().unwrap();
}
{
let b = Bindle::open(path).expect("Failed to reopen");
let result = b.read("test.txt").expect("Should read successfully");
assert_eq!(result.as_ref(), data);
}
{
let mut file = OpenOptions::new()
.write(true)
.read(true)
.open(path)
.unwrap();
file.seek(SeekFrom::Start(HEADER_SIZE as u64)).unwrap();
file.write(&[b'X']).unwrap(); file.flush().unwrap();
}
{
let b = Bindle::open(path).expect("Failed to reopen after corruption");
let result = b.read("test.txt");
assert!(result.is_none(), "Read should fail due to CRC32 mismatch");
}
let _ = std::fs::remove_file(path);
}
#[test]
fn test_crc32_with_compression() {
let path = "test_crc32_compressed.bindl";
let _ = std::fs::remove_file(path);
let data = vec![b'A'; 2000];
{
let mut b = Bindle::open(path).expect("Failed to open");
b.add("compressed.bin", &data, Compress::Zstd).unwrap();
b.save().unwrap();
}
{
let b = Bindle::open(path).expect("Failed to reopen");
let result = b.read("compressed.bin").expect("Should read successfully");
assert_eq!(result.as_ref(), data.as_slice());
}
{
let b = Bindle::open(path).expect("Failed to reopen");
let mut reader = b.reader("compressed.bin").unwrap();
let mut output = Vec::new();
std::io::copy(&mut reader, &mut output).unwrap();
reader.verify_crc32().expect("CRC32 should match");
assert_eq!(output, data);
}
let _ = std::fs::remove_file(path);
}
#[test]
fn test_remove_entry() {
let path = "test_remove.bindl";
let _ = fs::remove_file(path);
let mut b = Bindle::open(path).expect("Failed to open");
b.add("file1.txt", b"Content 1", Compress::None).unwrap();
b.add("file2.txt", b"Content 2", Compress::None).unwrap();
b.add("file3.txt", b"Content 3", Compress::None).unwrap();
b.save().unwrap();
assert_eq!(b.len(), 3);
assert!(b.exists("file2.txt"));
assert!(b.remove("file2.txt"));
assert_eq!(b.len(), 2);
assert!(!b.exists("file2.txt"));
assert!(!b.remove("nonexistent.txt"));
b.save().unwrap();
let b2 = Bindle::open(path).unwrap();
assert_eq!(b2.len(), 2);
assert!(b2.exists("file1.txt"));
assert!(!b2.exists("file2.txt"));
assert!(b2.exists("file3.txt"));
assert_eq!(b2.read("file1.txt").unwrap().as_ref(), b"Content 1");
assert_eq!(b2.read("file3.txt").unwrap().as_ref(), b"Content 3");
fs::remove_file(path).ok();
}
}