an efficient binary archive format
at main 129 lines 4.0 kB view raw
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}