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