Skip to main content

bindle_file/
writer.rs

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}