Skip to main content

rakata_formats/bif/
writer.rs

1//! BIF binary writer.
2
3use std::io::{Cursor, Write};
4
5#[cfg(feature = "bzf")]
6use lzma_rust2::{LzmaOptions, LzmaWriter};
7
8#[cfg(feature = "bzf")]
9use super::LZMA_ALONE_HEADER_SIZE;
10use super::{
11    binary::write_u32, Bif, BifBinaryError, BifContainer, BifResourceStorage, BIF_MAGIC,
12    BIF_VERSION_V10, FILE_HEADER_SIZE, FIXED_ENTRY_SIZE, VARIABLE_ENTRY_SIZE,
13};
14
15/// Writes a BIF archive to a writer.
16///
17/// # Errors
18///
19/// [`BifBinaryError::BzfFeatureDisabled`] when `bif.container` is
20/// [`BifContainer::Bzf`](crate::bif::BifContainer::Bzf) and the crate was
21/// built without the `bzf` feature, raised before anything is written.
22///
23/// [`BifBinaryError::ValueOverflow`] when either entry count, a table offset
24/// or size, or the running data offset will not fit its `u32`, naming the
25/// field. [`BifBinaryError::Io`] when the writer fails, which can leave a
26/// partial archive.
27#[cfg_attr(
28    feature = "tracing",
29    tracing::instrument(level = "debug", skip(writer, bif))
30)]
31pub fn write_bif<W: Write>(writer: &mut W, bif: &Bif) -> Result<(), BifBinaryError> {
32    let result = match bif.container {
33        BifContainer::Biff => write_bif_impl(writer, bif, false),
34        BifContainer::Bzf => {
35            #[cfg(not(feature = "bzf"))]
36            {
37                Err(BifBinaryError::BzfFeatureDisabled)
38            }
39            #[cfg(feature = "bzf")]
40            {
41                write_bif_impl(writer, bif, true)
42            }
43        }
44    };
45    if result.is_ok() {
46        crate::trace_debug!(
47            container = ?bif.container,
48            resource_count = bif.resources.len(),
49            "wrote bif/bzf to writer"
50        );
51    }
52    result
53}
54
55/// Serializes a BIF archive to bytes.
56///
57/// # Errors
58///
59/// Every non-I/O failure [`write_bif`] describes. The `Vec` target has no I/O
60/// to fail at.
61#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(bif), fields(container = ?bif.container)))]
62pub fn write_bif_to_vec(bif: &Bif) -> Result<Vec<u8>, BifBinaryError> {
63    let mut cursor = Cursor::new(Vec::new());
64    write_bif(&mut cursor, bif)?;
65    let bytes = cursor.into_inner();
66    crate::trace_debug!(
67        container = ?bif.container,
68        bytes_len = bytes.len(),
69        "serialized bif/bzf to vec"
70    );
71    Ok(bytes)
72}
73
74#[cfg_attr(
75    feature = "tracing",
76    tracing::instrument(level = "debug", skip(writer, bif), fields(as_bzf, resource_count = bif.resources.len()))
77)]
78fn write_bif_impl<W: Write>(writer: &mut W, bif: &Bif, as_bzf: bool) -> Result<(), BifBinaryError> {
79    // Without the compression feature this can only ever be false: the
80    // dispatcher rejects compressed archives before reaching here.
81    #[cfg(not(feature = "bzf"))]
82    let _ = as_bzf;
83    let variable_resources: Vec<_> = bif
84        .resources
85        .iter()
86        .filter(|resource| matches!(resource.storage, BifResourceStorage::Variable))
87        .collect();
88    let fixed_resources: Vec<_> = bif
89        .resources
90        .iter()
91        .filter(|resource| matches!(resource.storage, BifResourceStorage::Fixed { .. }))
92        .collect();
93
94    let variable_count = u32::try_from(variable_resources.len())
95        .map_err(|_| BifBinaryError::ValueOverflow("variable_count"))?;
96    let fixed_count = u32::try_from(fixed_resources.len())
97        .map_err(|_| BifBinaryError::ValueOverflow("fixed_count"))?;
98    let variable_table_offset = u32::try_from(FILE_HEADER_SIZE)
99        .map_err(|_| BifBinaryError::ValueOverflow("variable_table_offset"))?;
100
101    let variable_table_size = variable_resources
102        .len()
103        .checked_mul(VARIABLE_ENTRY_SIZE)
104        .ok_or(BifBinaryError::ValueOverflow("variable_table_size"))?;
105    let fixed_table_size = fixed_resources
106        .len()
107        .checked_mul(FIXED_ENTRY_SIZE)
108        .ok_or(BifBinaryError::ValueOverflow("fixed_table_size"))?;
109
110    #[cfg(feature = "bzf")]
111    let variable_payloads = if as_bzf {
112        variable_resources
113            .iter()
114            .map(|resource| encode_bzf_payload(&resource.data))
115            .collect::<Result<Vec<_>, _>>()?
116    } else {
117        Vec::new()
118    };
119    #[cfg(feature = "bzf")]
120    let fixed_payloads = if as_bzf {
121        fixed_resources
122            .iter()
123            .map(|resource| encode_bzf_payload(&resource.data))
124            .collect::<Result<Vec<_>, _>>()?
125    } else {
126        Vec::new()
127    };
128
129    let mut variable_offsets = Vec::with_capacity(variable_resources.len());
130    let mut fixed_offsets = Vec::with_capacity(fixed_resources.len());
131    let mut next_data_offset = FILE_HEADER_SIZE
132        .checked_add(variable_table_size)
133        .and_then(|offset| offset.checked_add(fixed_table_size))
134        .ok_or(BifBinaryError::ValueOverflow("data_offset"))?;
135
136    #[cfg(feature = "bzf")]
137    for (index, resource) in variable_resources.iter().enumerate() {
138        let target = if let Some(source) = resource.source_data_offset {
139            usize::try_from(source).expect("source offset fits in usize")
140        } else {
141            align_to_four(next_data_offset)
142                .ok_or(BifBinaryError::ValueOverflow("data_offset_alignment"))?
143        };
144        variable_offsets.push(target);
145        let payload_len = if as_bzf {
146            variable_payloads[index].len()
147        } else {
148            resource.data.len()
149        };
150        next_data_offset = target
151            .checked_add(payload_len)
152            .ok_or(BifBinaryError::ValueOverflow("data_offset"))?;
153    }
154    #[cfg(not(feature = "bzf"))]
155    for resource in &variable_resources {
156        let target = if let Some(source) = resource.source_data_offset {
157            usize::try_from(source).expect("source offset fits in usize")
158        } else {
159            align_to_four(next_data_offset)
160                .ok_or(BifBinaryError::ValueOverflow("data_offset_alignment"))?
161        };
162        variable_offsets.push(target);
163        next_data_offset = target
164            .checked_add(resource.data.len())
165            .ok_or(BifBinaryError::ValueOverflow("data_offset"))?;
166    }
167
168    #[cfg(feature = "bzf")]
169    for (index, resource) in fixed_resources.iter().enumerate() {
170        let target = if let Some(source) = resource.source_data_offset {
171            usize::try_from(source).expect("source offset fits in usize")
172        } else {
173            align_to_four(next_data_offset)
174                .ok_or(BifBinaryError::ValueOverflow("data_offset_alignment"))?
175        };
176        fixed_offsets.push(target);
177        let payload_len = if as_bzf {
178            fixed_payloads[index].len()
179        } else {
180            resource.data.len()
181        };
182        next_data_offset = target
183            .checked_add(payload_len)
184            .ok_or(BifBinaryError::ValueOverflow("data_offset"))?;
185    }
186    #[cfg(not(feature = "bzf"))]
187    for resource in &fixed_resources {
188        let target = if let Some(source) = resource.source_data_offset {
189            usize::try_from(source).expect("source offset fits in usize")
190        } else {
191            align_to_four(next_data_offset)
192                .ok_or(BifBinaryError::ValueOverflow("data_offset_alignment"))?
193        };
194        fixed_offsets.push(target);
195        next_data_offset = target
196            .checked_add(resource.data.len())
197            .ok_or(BifBinaryError::ValueOverflow("data_offset"))?;
198    }
199
200    // Both kinds carry the same signature: compression is signalled by the
201    // filename, never by the container.
202    writer.write_all(&BIF_MAGIC)?;
203    writer.write_all(&BIF_VERSION_V10)?;
204    write_u32(writer, variable_count)?;
205    write_u32(writer, fixed_count)?;
206    write_u32(writer, variable_table_offset)?;
207
208    for (resource, data_offset) in variable_resources.iter().zip(variable_offsets.iter()) {
209        write_u32(writer, resource.resource_id.raw())?;
210        write_u32(
211            writer,
212            u32::try_from(*data_offset)
213                .map_err(|_| BifBinaryError::ValueOverflow("data_offset"))?,
214        )?;
215        write_u32(
216            writer,
217            u32::try_from(resource.data.len())
218                .map_err(|_| BifBinaryError::ValueOverflow("data_size"))?,
219        )?;
220        write_u32(writer, u32::from(resource.resource_type.raw_id()))?;
221    }
222
223    for (resource, data_offset) in fixed_resources.iter().zip(fixed_offsets.iter()) {
224        let part_count = match resource.storage {
225            BifResourceStorage::Fixed { part_count } => part_count,
226            BifResourceStorage::Variable => {
227                unreachable!("fixed resource vector must only contain fixed entries")
228            }
229        };
230        write_u32(writer, resource.resource_id.raw())?;
231        write_u32(
232            writer,
233            u32::try_from(*data_offset)
234                .map_err(|_| BifBinaryError::ValueOverflow("fixed_data_offset"))?,
235        )?;
236        write_u32(writer, part_count)?;
237        write_u32(
238            writer,
239            u32::try_from(resource.data.len())
240                .map_err(|_| BifBinaryError::ValueOverflow("fixed_data_size"))?,
241        )?;
242        write_u32(writer, u32::from(resource.resource_type.raw_id()))?;
243    }
244
245    let mut written_offset = FILE_HEADER_SIZE
246        .checked_add(variable_table_size)
247        .and_then(|offset| offset.checked_add(fixed_table_size))
248        .ok_or(BifBinaryError::ValueOverflow("written_offset"))?;
249    #[cfg(feature = "bzf")]
250    for (index, (resource, target_offset)) in variable_resources
251        .iter()
252        .zip(variable_offsets.iter())
253        .enumerate()
254    {
255        if written_offset < *target_offset {
256            let pad_len = target_offset
257                .checked_sub(written_offset)
258                .ok_or(BifBinaryError::ValueOverflow("padding"))?;
259            writer.write_all(&vec![0_u8; pad_len])?;
260            written_offset = *target_offset;
261        }
262
263        let payload = if as_bzf {
264            variable_payloads[index].as_slice()
265        } else {
266            resource.data.as_slice()
267        };
268
269        writer.write_all(payload)?;
270        written_offset = written_offset
271            .checked_add(payload.len())
272            .ok_or(BifBinaryError::ValueOverflow("written_offset"))?;
273    }
274    #[cfg(not(feature = "bzf"))]
275    for (resource, target_offset) in variable_resources.iter().zip(variable_offsets.iter()) {
276        if written_offset < *target_offset {
277            let pad_len = target_offset
278                .checked_sub(written_offset)
279                .ok_or(BifBinaryError::ValueOverflow("padding"))?;
280            writer.write_all(&vec![0_u8; pad_len])?;
281            written_offset = *target_offset;
282        }
283
284        let payload = resource.data.as_slice();
285
286        writer.write_all(payload)?;
287        written_offset = written_offset
288            .checked_add(payload.len())
289            .ok_or(BifBinaryError::ValueOverflow("written_offset"))?;
290    }
291
292    #[cfg(feature = "bzf")]
293    for (index, (resource, target_offset)) in
294        fixed_resources.iter().zip(fixed_offsets.iter()).enumerate()
295    {
296        if written_offset < *target_offset {
297            let pad_len = target_offset
298                .checked_sub(written_offset)
299                .ok_or(BifBinaryError::ValueOverflow("padding"))?;
300            writer.write_all(&vec![0_u8; pad_len])?;
301            written_offset = *target_offset;
302        }
303
304        let payload = if as_bzf {
305            fixed_payloads[index].as_slice()
306        } else {
307            resource.data.as_slice()
308        };
309
310        writer.write_all(payload)?;
311        written_offset = written_offset
312            .checked_add(payload.len())
313            .ok_or(BifBinaryError::ValueOverflow("written_offset"))?;
314    }
315    #[cfg(not(feature = "bzf"))]
316    for (resource, target_offset) in fixed_resources.iter().zip(fixed_offsets.iter()) {
317        if written_offset < *target_offset {
318            let pad_len = target_offset
319                .checked_sub(written_offset)
320                .ok_or(BifBinaryError::ValueOverflow("padding"))?;
321            writer.write_all(&vec![0_u8; pad_len])?;
322            written_offset = *target_offset;
323        }
324
325        let payload = resource.data.as_slice();
326
327        writer.write_all(payload)?;
328        written_offset = written_offset
329            .checked_add(payload.len())
330            .ok_or(BifBinaryError::ValueOverflow("written_offset"))?;
331    }
332
333    Ok(())
334}
335
336fn align_to_four(value: usize) -> Option<usize> {
337    let padding = (4 - (value % 4)) % 4;
338    value.checked_add(padding)
339}
340
341#[cfg(feature = "bzf")]
342/// Encodes one payload as a bare LZMA-alone stream.
343///
344/// The five-byte header (properties byte, then dictionary size) is written
345/// explicitly because the reader takes the coder parameters from it. The
346/// uncompressed length is deliberately absent: it lives in the entry table,
347/// which is what makes these streams shorter than a standalone `.lzma` file.
348///
349/// Byte-parity with the shipping encoder is not a goal — only the framing is
350/// contractual, since a decoder reads the parameters rather than assuming
351/// them.
352fn encode_bzf_payload(payload: &[u8]) -> Result<Vec<u8>, BifBinaryError> {
353    let options = LzmaOptions::with_preset(6);
354    let literal_context = LzmaOptions::LC_DEFAULT;
355    let literal_position = LzmaOptions::LP_DEFAULT;
356    let position = LzmaOptions::PB_DEFAULT;
357    let dictionary_size = options.dict_size;
358
359    // Standard LZMA packing of the three coder parameters into one byte.
360    let properties = (position * 5 + literal_position) * 9 + literal_context;
361    let properties =
362        u8::try_from(properties).map_err(|_| BifBinaryError::ValueOverflow("lzma_properties"))?;
363
364    let mut out = Vec::with_capacity(payload.len() / 2 + LZMA_ALONE_HEADER_SIZE);
365    out.push(properties);
366    out.extend_from_slice(&dictionary_size.to_le_bytes());
367
368    let mut writer =
369        LzmaWriter::new_no_header(Cursor::new(Vec::new()), &options, true).map_err(|error| {
370            BifBinaryError::InvalidData(format!(
371                "failed to initialize the payload encoder: {error}"
372            ))
373        })?;
374    writer.write_all(payload).map_err(|error| {
375        BifBinaryError::InvalidData(format!("failed to encode a payload: {error}"))
376    })?;
377    let cursor = writer.finish().map_err(|error| {
378        BifBinaryError::InvalidData(format!("failed to finalize a payload: {error}"))
379    })?;
380    out.extend_from_slice(&cursor.into_inner());
381    Ok(out)
382}