snowbinary/
lib.rs

1#![warn(missing_docs)]
2
3//! Easy to use binary file writer and reader with its own format.
4
5mod error;
6mod reader;
7mod writer;
8
9#[cfg(test)]
10mod tests;
11
12use std::{
13    fs::File,
14    io::{Seek, SeekFrom},
15    path::PathBuf,
16};
17
18pub use crate::error::SnowBinError;
19
20/// The version of the Spec that this library can interact with.
21pub const VERSION_SPEC: u64 = 2; // Snow Binary File Format
22
23const DEFAULT_HEADER_SIZE: u32 = 8;
24const DATA_SIZES: [u8; 4] = [8, 16, 32, 64];
25const DEFAULT_DATA_SIZE: usize = 3;
26
27// In bytes.
28const DATA_START: u64 = 21;
29const HASH_SIZE: u32 = 32;
30
31/// Holds information used by `SnowBinWriter` to create and write to files.
32/// Default returns `SnowBinInfo` with a header size of 8 and a data size of 64.
33/// # Example
34/// ```
35/// use snowbinary::SnowBinInfo;
36///
37/// let info = SnowBinInfo::default();
38/// ```
39#[derive(Copy, Clone, Eq, PartialEq, Debug)]
40pub struct SnowBinInfo {
41    header_size: u32,
42    data_size: u8,
43}
44
45impl SnowBinInfo {
46    /// Creates a new `SnowBinInfo` with `header_size` and `data_size`.
47    /// # Example
48    /// ```
49    /// use snowbinary::SnowBinInfo;
50    ///
51    /// let info = SnowBinInfo::new(8, 64);
52    /// ```
53    /// # Errors
54    /// Returns `SnowBinError` if the `header_size` is < 8 or if `data_size` is not 8, 16, 32, or 64.
55    pub const fn new(header_size: u32, data_size: u8) -> Result<Self, SnowBinError> {
56        let header_size = if header_size >= 8 {
57            header_size
58        }
59        else {
60            return Err(SnowBinError::HeaderSizeTooSmall);
61        };
62
63        let data_size = match data_size {
64            8 => 8,
65            16 => 16,
66            32 => 32,
67            64 => 64,
68            _ => return Err(SnowBinError::DataSizeNotAllowed),
69        };
70
71        Ok(Self {
72            header_size,
73            data_size,
74        })
75    }
76}
77
78impl Default for SnowBinInfo {
79    fn default() -> Self {
80        Self {
81            header_size: DEFAULT_HEADER_SIZE,
82            data_size: DATA_SIZES[DEFAULT_DATA_SIZE],
83        }
84    }
85}
86
87/// Allows writing to a `SnowBinary` file.
88#[derive(Debug)]
89pub struct SnowBinWriter {
90    info: SnowBinInfo,
91    file: File,
92    hasher: blake3::Hasher,
93    done: bool,
94}
95
96impl SnowBinWriter {
97    /// Creates a new `SnowBinWriter` using the params of `SnowBinInfo`.
98    /// # Example
99    /// ```
100    /// use std::path::PathBuf;
101    ///
102    /// use snowbinary::{SnowBinInfo, SnowBinWriter};
103    ///
104    /// let writer = SnowBinWriter::new(SnowBinInfo::default(), PathBuf::from("file.temp"));
105    /// ```
106    /// # Errors
107    /// Returns `SnowBinError` if the file could not be created or opened, or the file cannot be written to.
108    pub fn new(info: SnowBinInfo, path: PathBuf) -> Result<Self, SnowBinError> {
109        let Ok(mut file) = File::create(path)
110        else {
111            return Err(SnowBinError::CouldNotCreateOrOpenFile);
112        };
113
114        let mut hasher = blake3::Hasher::new();
115
116        Self::init_file(&mut file, &mut hasher, info)?;
117
118        Ok(Self {
119            info,
120            file,
121            hasher,
122            done: false,
123        })
124    }
125
126    fn init_file(
127        file: &mut File,
128        hasher: &mut blake3::Hasher,
129        info: SnowBinInfo,
130    ) -> Result<(), SnowBinError> {
131        file.rewind().map_err(|_| SnowBinError::IOWriteError)?;
132
133        writer::write_header(file, "SNOW_BIN", 8)?;
134        writer::write_u64(file, VERSION_SPEC)?;
135        writer::write_u32(file, info.header_size)?;
136        writer::write_u8(file, info.data_size)?;
137
138        hasher.update(b"SNOW_BIN");
139        hasher.update(&VERSION_SPEC.to_le_bytes());
140        hasher.update(&info.header_size.to_le_bytes());
141        hasher.update(&info.data_size.to_le_bytes());
142
143        Ok(())
144    }
145
146    /// Writes a header and some data to a `SnowBinary` file.
147    /// allowed, or the file could not be written to.
148    /// # Example
149    /// ```
150    /// use std::path::PathBuf;
151    ///
152    /// use snowbinary::{SnowBinError, SnowBinInfo, SnowBinWriter};
153    ///
154    /// let mut writer = SnowBinWriter::new(SnowBinInfo::default(), PathBuf::from("file.temp"));
155    /// match &mut writer {
156    ///     Ok(writer) => {
157    ///         writer.write("Header", "This is data!".as_bytes()).unwrap();
158    ///     }
159    ///     Err(_) => {}
160    /// }
161    /// ```
162    /// # Errors
163    /// Returns `SnowBinError` if the header is too long, the data is too long, the data size is not
164    //TODO Should this check for headers of the same name?
165    pub fn write(&mut self, header: &str, data: &[u8]) -> Result<(), SnowBinError> {
166        if !self.done {
167            // Check
168            if header.len() > self.info.header_size as usize {
169                return Err(SnowBinError::HeaderTooLong);
170            }
171            let max = self.get_max_size()?;
172            if data.len() as u64 > max {
173                return Err(SnowBinError::DataTooLong);
174            }
175
176            // Write Data
177            let header = writer::write_header(&mut self.file, header, self.info.header_size)?;
178            self.hasher.update(&header);
179
180            #[allow(clippy::cast_possible_truncation)]
181            match self.info.data_size {
182                8 => {
183                    writer::write_u8(&mut self.file, data.len() as u8)?;
184                    self.hasher.update(&(data.len() as u8).to_le_bytes());
185                }
186                16 => {
187                    writer::write_u16(&mut self.file, data.len() as u16)?;
188                    self.hasher.update(&(data.len() as u16).to_le_bytes());
189                }
190                32 => {
191                    writer::write_u32(&mut self.file, data.len() as u32)?;
192                    self.hasher.update(&(data.len() as u32).to_le_bytes());
193                }
194                64 => {
195                    writer::write_u64(&mut self.file, data.len() as u64)?;
196                    self.hasher.update(&(data.len() as u64).to_le_bytes());
197                }
198                _ => return Err(SnowBinError::DataSizeNotAllowed),
199            }
200
201            writer::write_bytes(&mut self.file, data)?;
202            self.hasher.update(data);
203
204            return Ok(());
205        }
206
207        Err(SnowBinError::IOWriterClosed)
208    }
209
210    fn get_max_size(&self) -> Result<u64, SnowBinError> {
211        Ok(match self.info.data_size {
212            8 => u64::from(u8::MAX),
213            16 => u64::from(u16::MAX),
214            32 => u64::from(u32::MAX),
215            64 => u64::MAX,
216            _ => return Err(SnowBinError::DataSizeNotAllowed),
217        })
218    }
219
220    /// Closes the writer. (Alt: you could drop the writer, but this could cause a panic)
221    /// # Example
222    /// ```
223    /// use std::path::PathBuf;
224    ///
225    /// use snowbinary::{SnowBinError, SnowBinInfo, SnowBinWriter};
226    ///
227    /// let mut writer = SnowBinWriter::new(SnowBinInfo::default(), PathBuf::from("file.temp"));
228    /// match &mut writer {
229    ///     Ok(writer) => {
230    ///         writer.write("Header", "This is data!".as_bytes()).unwrap();
231    ///         writer.close().unwrap(); // Or let the writer drop.
232    ///     }
233    ///     Err(_) => {}
234    /// }
235    /// ```
236    /// # Errors
237    /// Returns `SnowBinError` if the file cannot be written to or the writer was already closed.
238    pub fn close(&mut self) -> Result<(), SnowBinError> {
239        use std::io::Write;
240
241        if !self.done {
242            let header = writer::write_header(&mut self.file, "SNOW_END", self.info.header_size)?;
243
244            // Write hash
245            self.hasher.update(&header);
246            let hash = self.hasher.finalize();
247            let hash = hash.as_bytes();
248            writer::write_bytes(&mut self.file, hash)?;
249
250            self.file.flush().map_err(|_| SnowBinError::IOWriteError)?;
251
252            self.done = true;
253
254            return Ok(());
255        }
256
257        Err(SnowBinError::IOWriterClosed)
258    }
259}
260
261impl Drop for SnowBinWriter {
262    fn drop(&mut self) {
263        if !self.done {
264            self.close()
265                .expect("Could not properly drop SnowBinWriter.");
266        }
267    }
268}
269
270/// Allows reading from a `SnowBinary` file.
271#[derive(Debug)]
272pub struct SnowBinReader {
273    info: SnowBinInfo,
274    file: File,
275}
276
277//TODO: Allow dumping of headers.
278impl SnowBinReader {
279    /// Creates a new `SnowBinReader`. Params are pulled from the file info.
280    /// # Example
281    /// ```
282    /// use std::path::PathBuf;
283    ///
284    /// use snowbinary::{SnowBinInfo, SnowBinReader};
285    ///
286    /// let reader = SnowBinReader::new(PathBuf::from("file.temp")).unwrap();
287    /// ```
288    /// # Errors
289    /// Returns `SnowBinError` if the file could not be created or opened, or the file cannot be read from.
290    pub fn new(path: PathBuf) -> Result<Self, SnowBinError> {
291        let Ok(mut file) = File::open(path)
292        else {
293            return Err(SnowBinError::CouldNotCreateOrOpenFile);
294        };
295
296        let info = Self::read_info(&mut file)?;
297
298        Ok(Self { info, file })
299    }
300
301    fn read_info(file: &mut File) -> Result<SnowBinInfo, SnowBinError> {
302        use std::io::Read;
303
304        // Check hash
305        let hash = {
306            file.rewind().map_err(|_| SnowBinError::IOReadError)?;
307            let mut buffer = Vec::new();
308            file.read_to_end(&mut buffer)
309                .map_err(|_| SnowBinError::IOReadError)?;
310            buffer.drain((buffer.len() - HASH_SIZE as usize)..buffer.len());
311
312            blake3::hash(&buffer)
313        };
314        let hash = hash.as_bytes();
315
316        {
317            file.seek(SeekFrom::End(-(i64::from(HASH_SIZE))))
318                .map_err(|_| SnowBinError::IOReadError)?;
319            let read_hash = reader::read_bytes(file, u64::from(HASH_SIZE))?;
320
321            if !read_hash.eq(hash) {
322                return Err(SnowBinError::HashDoesNotMatch);
323            }
324        }
325
326        // Read file config
327        file.rewind().map_err(|_| SnowBinError::IOReadError)?;
328
329        let snow_header = reader::read_header(file, 8)?;
330        if !snow_header.eq("SNOW_BIN") {
331            return Err(SnowBinError::MalformedHeader);
332        }
333
334        let version = reader::read_u64(file)?;
335        if version != VERSION_SPEC {
336            return Err(SnowBinError::WrongSpecVersion);
337        }
338
339        let header_size = reader::read_u32(file)?;
340
341        let data_size = reader::read_u8(file)?;
342        match data_size {
343            8 | 16 | 32 | 64 => (),
344            _ => return Err(SnowBinError::DataSizeNotAllowed),
345        };
346
347        Ok(SnowBinInfo {
348            header_size,
349            data_size,
350        })
351    }
352
353    /// Reads data from the file using the header.
354    /// # Example
355    /// ```
356    /// use std::path::PathBuf;
357    ///
358    /// use snowbinary::{SnowBinError, SnowBinInfo, SnowBinReader};
359    ///
360    /// let mut reader = SnowBinReader::new(PathBuf::from("file.temp"));
361    /// match &mut reader {
362    ///     Ok(reader) => {
363    ///         let data = reader.read("Header"); // May return error
364    ///     }
365    ///     Err(_) => {}
366    /// }
367    /// ```
368    /// # Errors
369    /// Returns `SnowBinError` if the file cannot be read from or the end of the file was reached.
370    /// # Panics
371    /// Panics **could** happen if data size is over `i64::MAX`.
372    pub fn read(&mut self, header: &str) -> Result<Vec<u8>, SnowBinError> {
373        self.file
374            .seek(SeekFrom::Start(DATA_START))
375            .map_err(|_| SnowBinError::IOReadError)?;
376
377        let mut buffer = vec![32_u8; self.info.header_size as usize];
378        buffer.splice(0..header.len(), header.as_bytes().iter().copied());
379        let header = String::from_utf8(buffer).map_err(|_| SnowBinError::MalformedHeader)?;
380
381        let mut f_header = String::new();
382        let mut data = Vec::new();
383        let mut store = 0_u64;
384        while !f_header.eq(&header) {
385            f_header = reader::read_header(&mut self.file, self.info.header_size)?;
386
387            if f_header.starts_with("SNOW_END") {
388                return Err(SnowBinError::ReachedEOF);
389            }
390
391            let size = match self.info.data_size {
392                8 => u64::from(reader::read_u8(&mut self.file)?),
393                16 => u64::from(reader::read_u16(&mut self.file)?),
394                32 => u64::from(reader::read_u32(&mut self.file)?),
395                64 => reader::read_u64(&mut self.file)?,
396                _ => return Err(SnowBinError::DataSizeNotAllowed),
397            };
398
399            if f_header.eq(&header) {
400                data = reader::read_bytes(&mut self.file, size)?;
401            }
402            else {
403                let mut file = &self.file;
404
405                let tmp = if size > i64::MAX as u64 {
406                    let s = size - i64::MAX as u64;
407                    file.seek(SeekFrom::Current(i64::MAX))
408                        .map_err(|_| SnowBinError::IOReadError)?;
409                    file.seek(SeekFrom::Current(s.try_into().unwrap()))
410                        .map_err(|_| SnowBinError::IOReadError)?
411                }
412                else {
413                    file.seek(SeekFrom::Current(size.try_into().unwrap()))
414                        .map_err(|_| SnowBinError::IOReadError)?
415                };
416
417                if tmp == store {
418                    return Err(SnowBinError::ReachedEOF);
419                }
420                store = tmp;
421            }
422        }
423
424        Ok(data)
425    }
426}