an efficient binary archive format
1use crc32fast::Hasher;
2use std::io::{self, Seek, SeekFrom, Write};
3
4use crate::bindle::Bindle;
5use crate::entry::Entry;
6
7/// A streaming writer for adding entries to an archive.
8///
9/// Created by [`Bindle::writer()`]. Automatically compresses data if requested and computes CRC32 for integrity verification.
10///
11/// The writer must be closed with [`close()`](Writer::close) or will be automatically closed when dropped. After closing, call [`Bindle::save()`] to commit the index.
12///
13/// # Example
14///
15/// ```no_run
16/// use std::io::Write;
17/// use bindle_file::{Bindle, Compress};
18///
19/// let mut archive = Bindle::open("data.bndl")?;
20/// let mut writer = archive.writer("file.txt", Compress::None)?;
21/// writer.write_all(b"data")?;
22/// writer.close()?;
23/// archive.save()?;
24/// # Ok::<(), std::io::Error>(())
25/// ```
26pub struct Writer<'a> {
27 pub(crate) bindle: &'a mut Bindle,
28 pub(crate) encoder: Option<zstd::Encoder<'a, std::fs::File>>,
29 pub(crate) name: String,
30 pub(crate) start_offset: u64,
31 pub(crate) uncompressed_size: u64,
32 pub(crate) crc32_hasher: Hasher,
33}
34
35impl<'a> Drop for Writer<'a> {
36 fn drop(&mut self) {
37 let _ = self.close_drop();
38 }
39}
40
41impl<'a> std::io::Write for Writer<'a> {
42 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
43 self.write_chunk(buf)?;
44 Ok(buf.len())
45 }
46
47 fn flush(&mut self) -> io::Result<()> {
48 Ok(())
49 }
50}
51
52impl<'a> Writer<'a> {
53 pub fn write_chunk(&mut self, data: &[u8]) -> io::Result<()> {
54 if self.name.is_empty() {
55 return Err(std::io::Error::new(std::io::ErrorKind::Other, "closed"));
56 }
57
58 self.uncompressed_size += data.len() as u64;
59 self.crc32_hasher.update(data);
60
61 match &mut self.encoder {
62 Some(encoder) => {
63 // Compressed: write to zstd encoder
64 encoder.write_all(data)?;
65 }
66 None => {
67 // Uncompressed: write directly to file
68 self.bindle.file.write_all(data)?;
69 }
70 }
71
72 Ok(())
73 }
74
75 fn close_drop(&mut self) -> io::Result<()> {
76 if self.name.is_empty() {
77 return Ok(());
78 }
79
80 let (compression_type, current_pos) = match self.encoder.take() {
81 Some(encoder) => {
82 // Compressed: finish encoder and sync position
83 let mut f = encoder.finish()?;
84 let pos = f.stream_position()?;
85 self.bindle.file.seek(SeekFrom::Start(pos))?;
86 (1, pos)
87 }
88 None => {
89 // Uncompressed: already wrote directly to file, just get position
90 let pos = self.bindle.file.stream_position()?;
91 (0, pos)
92 }
93 };
94
95 let compressed_size = current_pos - self.start_offset;
96
97 // Handle 8-byte alignment padding
98 let pad_len = crate::pad::<8, u64>(current_pos);
99 if pad_len > 0 {
100 crate::write_padding(&mut self.bindle.file, pad_len as usize)?;
101 }
102
103 self.bindle.data_end = current_pos + pad_len;
104
105 let crc32_value = self.crc32_hasher.clone().finalize();
106
107 let mut entry = Entry::default();
108 entry.set_offset(self.start_offset);
109 entry.set_compressed_size(compressed_size);
110 entry.set_uncompressed_size(self.uncompressed_size);
111 entry.set_crc32(crc32_value);
112 entry.set_name_len(self.name.len() as u16);
113 entry.compression_type = compression_type;
114
115 self.bindle.index.insert(self.name.clone(), entry);
116 self.name.clear(); // Mark as closed
117
118 // Downgrade to shared lock after write completes
119 self.bindle.file.lock_shared()?;
120 Ok(())
121 }
122
123 /// Closes the writer and finalizes the entry.
124 ///
125 /// Automatically called when the writer is dropped, but calling explicitly allows error handling.
126 pub fn close(mut self) -> io::Result<()> {
127 self.close_drop()
128 }
129}