brdb/brz/
mod.rs

1use std::io::{Read, Write};
2
3use crate::{
4    BrFsReader, BrReader, IntoReader, World, brz::reader::BrzIndex, compression::decompress,
5    errors::BrError, pending::BrPendingFs, tables::BrBlob,
6};
7
8mod errors;
9pub use errors::*;
10
11mod reader;
12#[cfg(test)]
13mod tests;
14
15/// As described in https://gist.github.com/Zeblote/0fc682b9df1a3e82942b613ab70d8a04
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[repr(u8)]
19pub enum FormatVersion {
20    Initial = 0,
21}
22
23impl TryFrom<u8> for FormatVersion {
24    type Error = BrzError;
25
26    fn try_from(value: u8) -> Result<Self, Self::Error> {
27        match value {
28            0 => Ok(FormatVersion::Initial),
29            _ => Err(BrzError::InvalidFormat(value)),
30        }
31    }
32}
33
34/// Compression methods used in `.brz` files.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36#[repr(u8)]
37pub enum CompressionMethod {
38    None = 0,
39    GenericZstd = 1,
40}
41
42impl TryFrom<u8> for CompressionMethod {
43    type Error = BrzError;
44
45    fn try_from(value: u8) -> Result<Self, Self::Error> {
46        match value {
47            0 => Ok(CompressionMethod::None),
48            1 => Ok(CompressionMethod::GenericZstd),
49            _ => Err(BrzError::InvalidCompressionMethod(value)),
50        }
51    }
52}
53
54pub struct BrzArchiveHeader {
55    pub version: FormatVersion,
56    pub index_method: CompressionMethod,
57    pub index_size_uncompressed: i32,
58    pub index_size_compressed: i32,
59    /// A blake 3 hash of decompressed index data.
60    pub index_hash: [u8; 32],
61}
62
63#[derive(Default, Clone, Debug)]
64pub struct BrzIndexData {
65    pub num_folders: i32,
66    pub num_files: i32,
67    pub num_blobs: i32,
68    /// i32 * num_folders
69    /// -1 if the folder is a root folder.
70    pub folder_parent_ids: Vec<i32>,
71    /// Folder names: (uint8 * Folder name lengths[i]) * Num folders
72    /// Folder names formatted as UTF-8.
73    pub folder_names: Vec<String>,
74    /// i32 * num_files
75    /// -1 if the file is a root file.
76    pub file_parent_ids: Vec<i32>,
77    /// i32 * num_files
78    /// -1 if the file is empty
79    pub file_content_ids: Vec<i32>,
80    /// File names: (uint8 * File name lengths[i]) * Num files
81    /// File names formatted as UTF-8.
82    pub file_names: Vec<String>,
83    // u8 * num_blobs
84    pub compression_methods: Vec<CompressionMethod>,
85    // i32 * num_blobs
86    pub sizes_uncompressed: Vec<i32>,
87    // i32 * num_blobs
88    pub sizes_compressed: Vec<i32>,
89    // Blob hashes: (uint8 * 32) * num_blobs
90    pub blob_hashes: Vec<[u8; 32]>,
91    // Binary data ranges for the blobs
92    pub blob_ranges: Vec<(usize, usize)>,
93    // Total size of all blobs in the archive.
94    pub blob_total_size: usize,
95}
96
97#[derive(Clone)]
98pub struct Brz {
99    pub index_data: BrzIndexData,
100    /// (Compressed) blob data as one contiguous byte array.
101    pub blob_data: Vec<u8>,
102}
103
104impl IntoReader for Brz {
105    type Inner = BrzIndex<Brz>;
106
107    fn into_reader(self) -> BrReader<Self::Inner> {
108        BrzIndex::new(self).into_reader()
109    }
110}
111
112impl AsRef<Brz> for Brz {
113    fn as_ref(&self) -> &Brz {
114        self
115    }
116}
117
118impl<'a> IntoReader for &'a Brz {
119    type Inner = BrzIndex<&'a Brz>;
120
121    fn into_reader(self) -> BrReader<Self::Inner> {
122        BrzIndex::new(self).into_reader()
123    }
124}
125
126fn read_u8(r: &mut impl Read) -> Result<u8, BrzError> {
127    let mut buf = [0u8; 1];
128    r.read_exact(&mut buf).map_err(BrzError::IO)?;
129    Ok(buf[0])
130}
131fn read_i32(r: &mut impl Read) -> Result<i32, BrzError> {
132    let mut buf = [0u8; 4];
133    r.read_exact(&mut buf).map_err(BrzError::IO)?;
134    Ok(i32::from_le_bytes(buf))
135}
136fn read_u16(r: &mut impl Read) -> Result<u16, BrzError> {
137    let mut buf = [0u8; 2];
138    r.read_exact(&mut buf).map_err(BrzError::IO)?;
139    Ok(u16::from_le_bytes(buf))
140}
141fn read_string(r: &mut impl Read, len: u16) -> Result<String, BrzError> {
142    let mut buf = vec![0u8; len as usize];
143    r.read_exact(&mut buf).map_err(BrzError::IO)?;
144    Ok(String::from_utf8(buf)?)
145}
146
147impl Brz {
148    /// Open and read a brz archive from a file path.
149    pub fn open(path: impl AsRef<std::path::Path>) -> Result<Brz, BrzError> {
150        Self::read(&mut std::fs::File::open(path).map_err(BrzError::IO)?)
151    }
152
153    /// Open and read a brz archive from a file path.
154    pub fn new(path: impl AsRef<std::path::Path>) -> Result<Brz, BrzError> {
155        Self::open(path)
156    }
157
158    /// Read a brz archive from a byte slice.
159    pub fn read_slice(buf: &[u8]) -> Result<Brz, BrzError> {
160        let mut cursor = std::io::Cursor::new(buf);
161        Self::read(&mut cursor)
162    }
163
164    // Read a brz archive from a reader.
165    pub fn read(r: &mut impl Read) -> Result<Brz, BrzError> {
166        let magic = [read_u8(r)?, read_u8(r)?, read_u8(r)?];
167        if magic != *b"BRZ" {
168            return Err(BrzError::InvalidMagic(magic));
169        }
170
171        let header = BrzArchiveHeader::read(r)?;
172
173        let index_buf = match header.index_method {
174            CompressionMethod::None => {
175                let mut buf = vec![0u8; header.index_size_uncompressed as usize];
176                r.read_exact(&mut buf).map_err(BrzError::IO)?;
177                buf
178            }
179            CompressionMethod::GenericZstd => {
180                let mut compressed_buf = vec![0u8; header.index_size_compressed as usize];
181                r.read_exact(&mut compressed_buf).map_err(BrzError::IO)?;
182                decompress(&compressed_buf, header.index_size_uncompressed as usize)
183                    .map_err(BrzError::Decompress)?
184            }
185        };
186
187        // Verify the hash of the index data
188        let index_hash = BrBlob::hash(&index_buf);
189        if index_hash != header.index_hash {
190            return Err(BrzError::InvalidIndexHash(index_hash, header.index_hash));
191        }
192
193        let index_data = BrzIndexData::read(&mut index_buf.as_slice())?;
194        let mut blob_data = Vec::with_capacity(index_data.blob_total_size);
195        r.read_to_end(&mut blob_data).map_err(BrzError::IO)?;
196
197        Ok(Brz {
198            index_data,
199            blob_data,
200        })
201    }
202
203    /// Write a brz archive to a byte vector.
204    pub fn to_vec(&self, zstd_level: Option<i32>) -> Result<Vec<u8>, BrzError> {
205        let mut buf = Vec::new();
206        self.write(&mut buf, zstd_level)?;
207        Ok(buf)
208    }
209
210    // Convert a Brz to a pending filesystem
211    pub fn to_pending(&self) -> Result<BrPendingFs, BrError> {
212        let reader = self.into_reader();
213        Ok(reader.get_fs()?.to_pending(&*reader)?)
214    }
215
216    /// Write a pending fs to a brz file.
217    pub fn write_pending(
218        path: impl AsRef<std::path::Path>,
219        pending: BrPendingFs,
220    ) -> Result<(), BrError> {
221        let mut file = std::fs::File::create(path).map_err(BrzError::IO)?;
222        pending.to_brz_data(Some(14))?.write(&mut file, Some(14))?;
223        Ok(())
224    }
225
226    /// Write a brz archive to a file.
227    pub fn save(path: impl AsRef<std::path::Path>, world: &World) -> Result<(), BrError> {
228        let mut file = std::fs::File::create(path).map_err(BrzError::IO)?;
229        world
230            .to_unsaved()?
231            .to_pending()?
232            .to_brz_data(Some(14))?
233            .write(&mut file, Some(14))?;
234        Ok(())
235    }
236
237    /// Write a brz archive to a file with no compression
238    pub fn save_uncompressed(
239        path: impl AsRef<std::path::Path>,
240        world: &World,
241    ) -> Result<(), BrError> {
242        Self::write_pending(path, world.to_unsaved()?.to_pending()?)
243    }
244
245    /// Write a brz archive to a writer.
246    pub fn write(&self, w: &mut impl Write, zstd_level: Option<i32>) -> Result<(), BrzError> {
247        w.write(b"BRZ")?;
248        let mut index_data = self.index_data.to_vec()?;
249        let index_size_uncompressed = index_data.len() as i32;
250        #[allow(unused)]
251        let mut index_size_compressed = index_size_uncompressed;
252        let mut index_method = CompressionMethod::None;
253        let index_hash = BrBlob::hash(&index_data);
254
255        if let Some(level) = zstd_level {
256            let compressed_data = crate::compression::compress(&index_data, level)?;
257            // Only use the compressed data if it improves file size
258            if (index_data.len() as i32) < index_size_uncompressed {
259                index_size_compressed = compressed_data.len() as i32;
260                index_method = CompressionMethod::GenericZstd;
261                index_data = compressed_data;
262            }
263        };
264
265        BrzArchiveHeader {
266            version: FormatVersion::Initial,
267            index_method,
268            index_size_uncompressed,
269            index_size_compressed,
270            index_hash,
271        }
272        .write(w)?;
273        w.write_all(&index_data)?;
274        w.write_all(&self.blob_data)?;
275        Ok(())
276    }
277}
278
279impl BrzArchiveHeader {
280    pub fn read(r: &mut impl Read) -> Result<BrzArchiveHeader, BrzError> {
281        let version = FormatVersion::try_from(read_u8(r)?)?;
282        let index_method = CompressionMethod::try_from(read_u8(r)?)?;
283
284        let index_size_uncompressed = read_i32(r)?;
285        if index_size_uncompressed < 0 {
286            return Err(BrzError::InvalidIndexDecompressedLength(
287                index_size_uncompressed,
288            ));
289        }
290        let index_size_compressed = read_i32(r)?;
291        if index_size_compressed < 0 {
292            return Err(BrzError::InvalidIndexCompressedLength(
293                index_size_compressed,
294            ));
295        }
296        let mut index_hash = [0u8; 32];
297        r.read_exact(&mut index_hash).map_err(BrzError::IO)?;
298
299        Ok(BrzArchiveHeader {
300            version,
301            index_method,
302            index_size_uncompressed,
303            index_size_compressed,
304            index_hash,
305        })
306    }
307
308    pub fn write(&self, buf: &mut impl Write) -> Result<(), BrzError> {
309        buf.write_all(&[self.version as u8, self.index_method as u8])?;
310        buf.write_all(&self.index_size_uncompressed.to_le_bytes())?;
311        buf.write_all(&self.index_size_compressed.to_le_bytes())?;
312        buf.write_all(&self.index_hash)?;
313        Ok(())
314    }
315}
316
317impl BrzIndexData {
318    pub fn read(r: &mut impl Read) -> Result<BrzIndexData, BrzError> {
319        let num_folders = read_i32(r)?;
320        if num_folders < 0 {
321            return Err(BrzError::InvalidNumFolders(num_folders));
322        }
323        let num_files = read_i32(r)?;
324        if num_files < 0 {
325            return Err(BrzError::InvalidNumFiles(num_files));
326        }
327        let num_blobs = read_i32(r)?;
328        if num_blobs < 0 {
329            return Err(BrzError::InvalidNumBlobs(num_blobs));
330        }
331
332        let mut folder_parent_ids = Vec::with_capacity(num_folders as usize);
333        for _ in 0..num_folders {
334            folder_parent_ids.push(read_i32(r)?);
335        }
336        let folder_name_lengths = (0..num_folders)
337            .map(|_| read_u16(r))
338            .collect::<Result<Vec<_>, _>>()?;
339        let folder_names = folder_name_lengths
340            .iter()
341            .map(|&len| read_string(r, len))
342            .collect::<Result<Vec<_>, _>>()?;
343        let mut file_parent_ids = Vec::with_capacity(num_files as usize);
344        for _ in 0..num_files {
345            file_parent_ids.push(read_i32(r)?);
346        }
347        let mut file_content_ids = Vec::with_capacity(num_files as usize);
348        for _ in 0..num_files {
349            file_content_ids.push(read_i32(r)?);
350        }
351        let file_name_lengths = (0..num_files)
352            .map(|_| read_u16(r))
353            .collect::<Result<Vec<_>, _>>()?;
354        let file_names = file_name_lengths
355            .iter()
356            .map(|&len| read_string(r, len))
357            .collect::<Result<Vec<_>, _>>()?;
358        let mut compression_methods = Vec::with_capacity(num_blobs as usize);
359        for _ in 0..num_blobs {
360            compression_methods.push(CompressionMethod::try_from(read_u8(r)?)?);
361        }
362        let mut sizes_uncompressed = Vec::with_capacity(num_blobs as usize);
363        for _ in 0..num_blobs {
364            let len = read_i32(r)?;
365            if len < 0 {
366                return Err(BrzError::InvalidBlobDecompressedLength(len));
367            }
368            sizes_uncompressed.push(len);
369        }
370        let mut sizes_compressed = Vec::with_capacity(num_blobs as usize);
371        for _ in 0..num_blobs {
372            let len = read_i32(r)?;
373            if len < 0 {
374                return Err(BrzError::InvalidBlobCompressedLength(len));
375            }
376            sizes_compressed.push(len);
377        }
378        let mut blob_hashes = Vec::with_capacity(num_blobs as usize);
379        for _ in 0..num_blobs {
380            let mut hash = [0u8; 32];
381            r.read_exact(&mut hash).map_err(BrzError::IO)?;
382            blob_hashes.push(hash);
383        }
384
385        let mut blob_ranges = Vec::with_capacity(num_blobs as usize);
386        let mut current_offset = 0;
387        for i in 0..num_blobs as usize {
388            let start = current_offset;
389            // Index safety: `compression_methods`, `decompressed_lengths`, and `compressed_lengths`
390            // are all guaranteed to have at least `num_blobs` elements.
391            let length = match compression_methods[i] {
392                CompressionMethod::None => sizes_uncompressed[i] as usize,
393                CompressionMethod::GenericZstd => sizes_compressed[i] as usize,
394            };
395            let end = start + length;
396            blob_ranges.push((start, end));
397            current_offset = end;
398        }
399
400        Ok(BrzIndexData {
401            num_folders,
402            num_files,
403            num_blobs,
404            folder_parent_ids,
405            folder_names,
406            file_parent_ids,
407            file_content_ids,
408            file_names,
409            compression_methods,
410            sizes_uncompressed,
411            sizes_compressed,
412            blob_hashes,
413            blob_ranges,
414            blob_total_size: current_offset,
415        })
416    }
417
418    pub fn to_vec(&self) -> Result<Vec<u8>, BrzError> {
419        let mut buf = Vec::new();
420        buf.write_all(&self.num_folders.to_le_bytes())?;
421        buf.write_all(&self.num_files.to_le_bytes())?;
422        buf.write_all(&self.num_blobs.to_le_bytes())?;
423
424        for &parent_id in &self.folder_parent_ids {
425            buf.write_all(&parent_id.to_le_bytes())?;
426        }
427
428        // Write folder lengths and names
429        for name in &self.folder_names {
430            buf.write_all(&(name.len() as u16).to_le_bytes())?;
431        }
432        for name in &self.folder_names {
433            buf.write_all(name.as_bytes())?;
434        }
435
436        for &parent_id in &self.file_parent_ids {
437            buf.write_all(&parent_id.to_le_bytes())?;
438        }
439        for &content_id in &self.file_content_ids {
440            buf.write_all(&content_id.to_le_bytes())?;
441        }
442
443        // Write file lengths and names
444        for name in &self.file_names {
445            buf.write_all(&(name.len() as u16).to_le_bytes())?;
446        }
447        for name in &self.file_names {
448            buf.write_all(name.as_bytes())?;
449        }
450
451        for method in &self.compression_methods {
452            buf.write_all(&[*method as u8])?;
453        }
454        for &size in &self.sizes_uncompressed {
455            buf.write_all(&size.to_le_bytes())?;
456        }
457        for &size in &self.sizes_compressed {
458            buf.write_all(&size.to_le_bytes())?;
459        }
460        for hash in &self.blob_hashes {
461            buf.write_all(hash)?;
462        }
463
464        Ok(buf)
465    }
466}