rakata_formats/bif/
reader.rs

1//! BIF binary reader.
2
3use std::io::Read;
4
5#[cfg(feature = "bzf")]
6use std::io::Cursor;
7
8#[cfg(feature = "bzf")]
9use lzma_rust2::{LzmaOptions, LzmaReader};
10use rakata_core::{ResourceId, ResourceTypeCode};
11
12use super::{
13    binary, Bif, BifBinaryError, BifContainer, BifReadMode, BifReadOptions, BifResource,
14    BifResourceStorage, BIF_MAGIC, BIF_VERSION_V10, BIF_VERSION_V11, BZF_MAGIC, FILE_HEADER_SIZE,
15    FIXED_ENTRY_SIZE, VARIABLE_ENTRY_SIZE,
16};
17
18/// Reads a BIF archive from a reader.
19///
20/// The stream is consumed from its current position.
21#[cfg_attr(
22    feature = "tracing",
23    tracing::instrument(level = "debug", skip(reader))
24)]
25pub fn read_bif<R: Read>(reader: &mut R) -> Result<Bif, BifBinaryError> {
26    read_bif_with_options(reader, BifReadOptions::default())
27}
28
29/// Reads a BIF archive from a reader with explicit options.
30#[cfg_attr(
31    feature = "tracing",
32    tracing::instrument(level = "debug", skip(reader))
33)]
34pub fn read_bif_with_options<R: Read>(
35    reader: &mut R,
36    options: BifReadOptions,
37) -> Result<Bif, BifBinaryError> {
38    let mut bytes = Vec::new();
39    reader.read_to_end(&mut bytes)?;
40    crate::trace_debug!(bytes_len = bytes.len(), "read bif/bzf bytes from reader");
41    read_bif_from_bytes_with_options(&bytes, options)
42}
43
44/// Reads a BIF archive from bytes.
45#[cfg_attr(
46    feature = "tracing",
47    tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
48)]
49pub fn read_bif_from_bytes(bytes: &[u8]) -> Result<Bif, BifBinaryError> {
50    read_bif_from_bytes_with_options(bytes, BifReadOptions::default())
51}
52
53/// Reads a BIF archive from bytes with explicit options.
54#[cfg_attr(
55    feature = "tracing",
56    tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
57)]
58pub fn read_bif_from_bytes_with_options(
59    bytes: &[u8],
60    options: BifReadOptions,
61) -> Result<Bif, BifBinaryError> {
62    if bytes.len() < FILE_HEADER_SIZE {
63        return Err(BifBinaryError::InvalidHeader(
64            "file smaller than BIF header".into(),
65        ));
66    }
67
68    let magic = binary::read_fourcc(bytes, 0)?;
69    let container = if magic == BZF_MAGIC {
70        #[cfg(not(feature = "bzf"))]
71        {
72            return Err(BifBinaryError::BzfFeatureDisabled);
73        }
74        #[cfg(feature = "bzf")]
75        {
76            BifContainer::Bzf
77        }
78    } else {
79        binary::expect_fourcc(magic, BIF_MAGIC).map_err(BifBinaryError::InvalidMagic)?;
80        BifContainer::Biff
81    };
82
83    let version = binary::read_fourcc(bytes, 4)?;
84    match options.input {
85        BifReadMode::CanonicalK1 => binary::expect_fourcc(version, BIF_VERSION_V10)
86            .map_err(BifBinaryError::InvalidVersion)?,
87        BifReadMode::CompatibilityAurora => {
88            binary::expect_any_fourcc(version, &[BIF_VERSION_V10, BIF_VERSION_V11])
89                .map_err(BifBinaryError::InvalidVersion)?
90        }
91    }
92
93    let variable_count = binary::checked_to_usize(binary::read_u32(bytes, 8)?, "variable_count")?;
94    let fixed_count = binary::checked_to_usize(binary::read_u32(bytes, 12)?, "fixed_count")?;
95
96    let variable_table_offset =
97        binary::checked_to_usize(binary::read_u32(bytes, 16)?, "variable_table_offset")?;
98    let variable_table_size = variable_count
99        .checked_mul(VARIABLE_ENTRY_SIZE)
100        .ok_or_else(|| BifBinaryError::InvalidHeader("variable table size overflow".into()))?;
101    binary::check_slice_in_bounds(
102        bytes,
103        variable_table_offset,
104        variable_table_size,
105        "variable resource table",
106    )?;
107
108    let parse_fixed_entries = matches!(options.input, BifReadMode::CompatibilityAurora);
109    let fixed_table_offset = variable_table_offset
110        .checked_add(variable_table_size)
111        .ok_or_else(|| BifBinaryError::InvalidHeader("fixed table offset overflow".into()))?;
112    if parse_fixed_entries {
113        let fixed_table_size = fixed_count
114            .checked_mul(FIXED_ENTRY_SIZE)
115            .ok_or_else(|| BifBinaryError::InvalidHeader("fixed table size overflow".into()))?;
116        binary::check_slice_in_bounds(
117            bytes,
118            fixed_table_offset,
119            fixed_table_size,
120            "fixed resource table",
121        )?;
122    }
123
124    let total_entry_count = if parse_fixed_entries {
125        variable_count
126            .checked_add(fixed_count)
127            .ok_or_else(|| BifBinaryError::InvalidHeader("resource count overflow".into()))?
128    } else {
129        variable_count
130    };
131    let mut resource_entries = Vec::with_capacity(total_entry_count);
132    for resource_index in 0..variable_count {
133        let base = variable_table_offset + resource_index * VARIABLE_ENTRY_SIZE;
134        let resource_id = binary::read_u32(bytes, base)?;
135        let data_offset_u32 = binary::read_u32(bytes, base + 4)?;
136        let data_offset = binary::checked_to_usize(data_offset_u32, "resource_data_offset")?;
137        let data_size =
138            binary::checked_to_usize(binary::read_u32(bytes, base + 8)?, "resource_data_size")?;
139        let resource_type_u32 = binary::read_u32(bytes, base + 12)?;
140        let resource_type_id = u16::try_from(resource_type_u32).map_err(|_| {
141            BifBinaryError::InvalidData(format!(
142                "resource[{resource_index}] type id {resource_type_u32} exceeds u16 range"
143            ))
144        })?;
145
146        resource_entries.push((
147            resource_id,
148            ResourceTypeCode::from_raw_id(resource_type_id),
149            data_offset,
150            data_size,
151            BifResourceStorage::Variable,
152            data_offset_u32,
153        ));
154    }
155
156    if parse_fixed_entries {
157        for fixed_index in 0..fixed_count {
158            let base = fixed_table_offset + fixed_index * FIXED_ENTRY_SIZE;
159            let resource_id = binary::read_u32(bytes, base)?;
160            let data_offset_u32 = binary::read_u32(bytes, base + 4)?;
161            let data_offset =
162                binary::checked_to_usize(data_offset_u32, "fixed_resource_data_offset")?;
163            let part_count = binary::read_u32(bytes, base + 8)?;
164            let data_size = binary::checked_to_usize(
165                binary::read_u32(bytes, base + 12)?,
166                "fixed_resource_data_size",
167            )?;
168            let resource_type_u32 = binary::read_u32(bytes, base + 16)?;
169            let resource_type_id = u16::try_from(resource_type_u32).map_err(|_| {
170                BifBinaryError::InvalidData(format!(
171                    "fixed resource[{fixed_index}] type id {resource_type_u32} exceeds u16 range"
172                ))
173            })?;
174
175            resource_entries.push((
176                resource_id,
177                ResourceTypeCode::from_raw_id(resource_type_id),
178                data_offset,
179                data_size,
180                BifResourceStorage::Fixed { part_count },
181                data_offset_u32,
182            ));
183        }
184    }
185
186    let packed_sizes = if container == BifContainer::Bzf {
187        Some(compute_bzf_packed_sizes(&resource_entries, bytes.len())?)
188    } else {
189        None
190    };
191
192    let mut resources = Vec::with_capacity(resource_entries.len());
193    for (
194        resource_index,
195        (resource_id, resource_type, data_offset, data_size, storage, data_offset_u32),
196    ) in resource_entries.into_iter().enumerate()
197    {
198        let data = if let Some(packed_sizes) = &packed_sizes {
199            let packed_size = packed_sizes[resource_index];
200            check_data_slice_in_bounds(
201                bytes,
202                data_offset,
203                packed_size,
204                &format!("resource packed data[{resource_index}]"),
205            )?;
206            let packed_data = bytes
207                .get(data_offset..data_offset + packed_size)
208                .ok_or_else(|| {
209                    BifBinaryError::InvalidData(format!(
210                        "resource packed data slice missing for index {resource_index}"
211                    ))
212                })?;
213            #[cfg(feature = "bzf")]
214            {
215                decode_bzf_payload(packed_data, data_size)?
216            }
217            #[cfg(not(feature = "bzf"))]
218            {
219                let _ = packed_data;
220                return Err(BifBinaryError::BzfFeatureDisabled);
221            }
222        } else {
223            check_data_slice_in_bounds(
224                bytes,
225                data_offset,
226                data_size,
227                &format!("resource data[{resource_index}]"),
228            )?;
229            bytes
230                .get(data_offset..data_offset + data_size)
231                .ok_or_else(|| {
232                    BifBinaryError::InvalidData(format!(
233                        "resource data slice missing for index {resource_index}"
234                    ))
235                })?
236                .to_vec()
237        };
238
239        resources.push(BifResource {
240            resource_id: ResourceId::from_raw(resource_id),
241            resource_type,
242            storage,
243            data,
244            source_data_offset: Some(data_offset_u32),
245        });
246    }
247
248    let bif = Bif {
249        container,
250        resources,
251    };
252    crate::trace_debug!(
253        container = ?bif.container,
254        resource_count = bif.resources.len(),
255        "parsed bif/bzf from bytes"
256    );
257    Ok(bif)
258}
259
260fn check_data_slice_in_bounds(
261    bytes: &[u8],
262    offset: usize,
263    size: usize,
264    label: &str,
265) -> Result<(), BifBinaryError> {
266    binary::check_slice_in_bounds(bytes, offset, size, label)
267        .map_err(|error| BifBinaryError::InvalidData(error.to_string()))
268}
269
270fn compute_bzf_packed_sizes(
271    entries: &[(u32, ResourceTypeCode, usize, usize, BifResourceStorage, u32)],
272    file_len: usize,
273) -> Result<Vec<usize>, BifBinaryError> {
274    let mut by_offset: Vec<(usize, usize)> = entries
275        .iter()
276        .enumerate()
277        .map(|(index, (_, _, offset, _, _, _))| (index, *offset))
278        .collect();
279    by_offset.sort_by_key(|(_, offset)| *offset);
280
281    let mut packed_sizes = vec![0_usize; entries.len()];
282    for (entry_position, (entry_index, offset)) in by_offset.iter().enumerate() {
283        let end = if let Some((_, next_offset)) = by_offset.get(entry_position + 1) {
284            *next_offset
285        } else {
286            file_len
287        };
288        let packed_size = end.checked_sub(*offset).ok_or_else(|| {
289            BifBinaryError::InvalidData(format!(
290                "resource packed offset ordering invalid at entry index {entry_index}"
291            ))
292        })?;
293        packed_sizes[*entry_index] = packed_size;
294    }
295
296    Ok(packed_sizes)
297}
298
299#[cfg(feature = "bzf")]
300fn decode_bzf_payload(payload: &[u8], expected_size: usize) -> Result<Vec<u8>, BifBinaryError> {
301    if expected_size == 0 {
302        return Ok(Vec::new());
303    }
304
305    let mut attempt_len = payload.len();
306    loop {
307        let attempt = &payload[..attempt_len];
308        let decoded = decode_bzf_payload_raw(attempt, expected_size);
309        if let Ok(value) = decoded {
310            return Ok(value);
311        }
312
313        let trimmed_len = attempt
314            .iter()
315            .rposition(|byte| *byte != 0)
316            .map_or(0, |index| index + 1);
317        if trimmed_len == attempt_len {
318            return decoded;
319        }
320        attempt_len = trimmed_len;
321    }
322}
323
324#[cfg(feature = "bzf")]
325fn decode_bzf_payload_raw(payload: &[u8], expected_size: usize) -> Result<Vec<u8>, BifBinaryError> {
326    let mut reader = LzmaReader::new(
327        Cursor::new(payload),
328        u64::try_from(expected_size).expect("expected_size fits in u64"),
329        LzmaOptions::LC_DEFAULT,
330        LzmaOptions::LP_DEFAULT,
331        LzmaOptions::PB_DEFAULT,
332        LzmaOptions::DICT_SIZE_DEFAULT,
333        None,
334    )
335    .map_err(|error| {
336        BifBinaryError::InvalidData(format!("failed to initialize BZF decoder: {error}"))
337    })?;
338
339    let mut out = Vec::new();
340    reader.read_to_end(&mut out).map_err(|error| {
341        BifBinaryError::InvalidData(format!("failed to decode BZF payload: {error}"))
342    })?;
343    if out.len() != expected_size {
344        return Err(BifBinaryError::InvalidData(format!(
345            "decoded BZF payload length mismatch (expected {expected_size}, got {})",
346            out.len()
347        )));
348    }
349    Ok(out)
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355    use crate::bif::{write_bif_to_vec, BifReadMode};
356    use rakata_core::ResourceId;
357
358    const K1_PLAYER_BIF: &[u8] = include_bytes!(concat!(
359        env!("CARGO_MANIFEST_DIR"),
360        "/../../fixtures/k1_player.bif"
361    ));
362
363    #[test]
364    fn parses_bif_fixture() {
365        let bif = read_bif_from_bytes(K1_PLAYER_BIF).expect("fixture should parse");
366        assert_eq!(bif.resources.len(), 3);
367
368        let first = &bif.resources[0];
369        assert_eq!(first.resource_id, ResourceId::from_raw(0x0140_0000));
370        assert_eq!(first.resource_type.raw_id(), 2002);
371
372        let second = &bif.resources[1];
373        assert_eq!(second.resource_id, ResourceId::from_raw(0x0140_0001));
374        assert_eq!(second.resource_type.raw_id(), 3008);
375    }
376
377    #[test]
378    fn roundtrip_synthetic_bif() {
379        let mut bif = Bif::new();
380        bif.push_resource(
381            ResourceId::from_raw(0x0010_0000),
382            ResourceTypeCode::from_raw_id(2017),
383            b"abc".to_vec(),
384        );
385        bif.push_resource(
386            ResourceId::from_raw(0x0010_0001),
387            ResourceTypeCode::from_raw_id(2018),
388            b"defghi".to_vec(),
389        );
390
391        let bytes = write_bif_to_vec(&bif).expect("write should succeed");
392        let parsed = read_bif_from_bytes(&bytes).expect("read should succeed");
393
394        assert_eq!(parsed, bif);
395    }
396
397    #[test]
398    fn writer_is_deterministic_for_synthetic_bif() {
399        let mut bif = Bif::new();
400        bif.push_resource(
401            ResourceId::from_raw(0x0010_0000),
402            ResourceTypeCode::from_raw_id(2017),
403            b"abc".to_vec(),
404        );
405        bif.push_resource(
406            ResourceId::from_raw(0x0010_0001),
407            ResourceTypeCode::from_raw_id(2018),
408            b"defghi".to_vec(),
409        );
410        let first = write_bif_to_vec(&bif).expect("first write should succeed");
411        let second = write_bif_to_vec(&bif).expect("second write should succeed");
412        assert_eq!(first, second, "canonical BIF writer output drifted");
413    }
414
415    #[test]
416    fn writer_aligns_payload_offsets_to_four_bytes() {
417        let mut bif = Bif::new();
418        bif.push_resource(
419            ResourceId::from_raw(1),
420            ResourceTypeCode::from_raw_id(10),
421            vec![1, 2, 3],
422        );
423        bif.push_resource(
424            ResourceId::from_raw(2),
425            ResourceTypeCode::from_raw_id(10),
426            vec![4, 5],
427        );
428
429        let bytes = write_bif_to_vec(&bif).expect("write should succeed");
430
431        let first_offset = u32::from_le_bytes(bytes[24..28].try_into().expect("slice"));
432        let second_offset = u32::from_le_bytes(bytes[40..44].try_into().expect("slice"));
433        assert_eq!(first_offset, 52);
434        assert_eq!(second_offset, 56);
435    }
436
437    #[test]
438    fn roundtrip_preserves_unknown_resource_type_ids() {
439        let mut bif = Bif::new();
440        bif.push_resource(
441            ResourceId::from_raw(7),
442            ResourceTypeCode::from_raw_id(42424),
443            vec![1, 2, 3, 4],
444        );
445
446        let bytes = write_bif_to_vec(&bif).expect("write should succeed");
447        let parsed = read_bif_from_bytes(&bytes).expect("read should succeed");
448
449        assert_eq!(parsed.resources.len(), 1);
450        assert_eq!(parsed.resources[0].resource_type.raw_id(), 42424);
451        assert_eq!(parsed.resources[0].resource_type.known_type(), None);
452    }
453
454    #[test]
455    fn roundtrip_includes_fixed_resource_entries() {
456        let mut bif = Bif::new();
457        bif.push_resource(
458            ResourceId::from_raw(0x0010_0000),
459            ResourceTypeCode::from_raw_id(2017),
460            b"abc".to_vec(),
461        );
462        bif.push_fixed_resource(
463            ResourceId::from_raw(0x0010_4000),
464            ResourceTypeCode::from_raw_id(2027),
465            3,
466            b"fixed".to_vec(),
467        );
468
469        let bytes = write_bif_to_vec(&bif).expect("write should succeed");
470        assert_eq!(
471            u32::from_le_bytes(bytes[8..12].try_into().expect("slice")),
472            1
473        );
474        assert_eq!(
475            u32::from_le_bytes(bytes[12..16].try_into().expect("slice")),
476            1
477        );
478
479        let parsed = read_bif_from_bytes_with_options(
480            &bytes,
481            BifReadOptions {
482                input: BifReadMode::CompatibilityAurora,
483            },
484        )
485        .expect("read should succeed");
486        assert_eq!(parsed, bif);
487        assert_eq!(
488            parsed.resources[1].storage,
489            BifResourceStorage::Fixed { part_count: 3 }
490        );
491    }
492
493    #[test]
494    fn parses_fixed_resource_table_entries() {
495        let mut bytes = vec![0_u8; FILE_HEADER_SIZE + FIXED_ENTRY_SIZE + 4];
496        bytes[0..4].copy_from_slice(&BIF_MAGIC);
497        bytes[4..8].copy_from_slice(&BIF_VERSION_V10);
498        bytes[12..16].copy_from_slice(&1_u32.to_le_bytes()); // fixed_count
499        bytes[16..20].copy_from_slice(
500            &u32::try_from(FILE_HEADER_SIZE)
501                .expect("FILE_HEADER_SIZE fits in u32")
502                .to_le_bytes(),
503        );
504
505        let fixed_base = FILE_HEADER_SIZE;
506        bytes[fixed_base..fixed_base + 4].copy_from_slice(&0x0010_4000_u32.to_le_bytes()); // id
507        bytes[fixed_base + 4..fixed_base + 8].copy_from_slice(
508            &u32::try_from(FILE_HEADER_SIZE + FIXED_ENTRY_SIZE)
509                .expect("header + entry size fits in u32")
510                .to_le_bytes(),
511        ); // offset
512        bytes[fixed_base + 8..fixed_base + 12].copy_from_slice(&2_u32.to_le_bytes()); // part_count
513        bytes[fixed_base + 12..fixed_base + 16].copy_from_slice(&4_u32.to_le_bytes()); // size
514        bytes[fixed_base + 16..fixed_base + 20].copy_from_slice(&2027_u32.to_le_bytes()); // type
515        bytes[FILE_HEADER_SIZE + FIXED_ENTRY_SIZE..FILE_HEADER_SIZE + FIXED_ENTRY_SIZE + 4]
516            .copy_from_slice(&[1, 2, 3, 4]);
517
518        let bif = read_bif_from_bytes_with_options(
519            &bytes,
520            BifReadOptions {
521                input: BifReadMode::CompatibilityAurora,
522            },
523        )
524        .expect("fixed entries should parse");
525        assert_eq!(bif.resources.len(), 1);
526        assert_eq!(
527            bif.resources[0].storage,
528            BifResourceStorage::Fixed { part_count: 2 }
529        );
530        assert_eq!(bif.resources[0].data, vec![1, 2, 3, 4]);
531        assert_eq!(
532            bif.resource_by_id(rakata_core::ResourceId::from_raw(0x0010_4000_u32)),
533            Some([1, 2, 3, 4].as_slice())
534        );
535    }
536
537    #[test]
538    fn canonical_reader_accepts_fixed_resource_table_entries() {
539        let mut bytes = vec![0_u8; FILE_HEADER_SIZE + FIXED_ENTRY_SIZE];
540        bytes[0..4].copy_from_slice(&BIF_MAGIC);
541        bytes[4..8].copy_from_slice(&BIF_VERSION_V10);
542        bytes[12..16].copy_from_slice(&1_u32.to_le_bytes()); // fixed_count
543        bytes[16..20].copy_from_slice(
544            &u32::try_from(FILE_HEADER_SIZE)
545                .expect("FILE_HEADER_SIZE fits in u32")
546                .to_le_bytes(),
547        );
548
549        let bif = read_bif_from_bytes(&bytes).expect("canonical mode should parse");
550        assert_eq!(bif.resources.len(), 0);
551    }
552
553    #[test]
554    fn canonical_reader_ignores_fixed_table_bounds_when_not_loading_fixed_entries() {
555        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
556        bytes[0..4].copy_from_slice(&BIF_MAGIC);
557        bytes[4..8].copy_from_slice(&BIF_VERSION_V10);
558        bytes[12..16].copy_from_slice(&1_u32.to_le_bytes()); // fixed_count
559        bytes[16..20].copy_from_slice(
560            &u32::try_from(FILE_HEADER_SIZE)
561                .expect("FILE_HEADER_SIZE fits in u32")
562                .to_le_bytes(),
563        );
564
565        let bif = read_bif_from_bytes(&bytes).expect("canonical mode should ignore fixed table");
566        assert_eq!(bif.resources.len(), 0);
567
568        let err = read_bif_from_bytes_with_options(
569            &bytes,
570            BifReadOptions {
571                input: BifReadMode::CompatibilityAurora,
572            },
573        )
574        .expect_err("compat mode validates fixed table bounds");
575        assert!(matches!(err, BifBinaryError::InvalidHeader(_)));
576    }
577
578    #[test]
579    fn compatibility_reader_accepts_v11_bif_version() {
580        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
581        bytes[0..4].copy_from_slice(&BIF_MAGIC);
582        bytes[4..8].copy_from_slice(&BIF_VERSION_V11);
583        bytes[16..20].copy_from_slice(
584            &u32::try_from(FILE_HEADER_SIZE)
585                .expect("FILE_HEADER_SIZE fits in u32")
586                .to_le_bytes(),
587        );
588
589        let bif = read_bif_from_bytes_with_options(
590            &bytes,
591            BifReadOptions {
592                input: BifReadMode::CompatibilityAurora,
593            },
594        )
595        .expect("compat mode should accept v1.1");
596        assert_eq!(bif.resources.len(), 0);
597    }
598
599    #[test]
600    fn rejects_invalid_magic() {
601        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
602        bytes[0..4].copy_from_slice(b"NOPE");
603        bytes[4..8].copy_from_slice(&BIF_VERSION_V10);
604        let err = read_bif_from_bytes(&bytes).expect_err("must fail");
605        assert!(matches!(err, BifBinaryError::InvalidMagic(_)));
606    }
607
608    #[test]
609    fn rejects_invalid_version() {
610        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
611        bytes[0..4].copy_from_slice(&BIF_MAGIC);
612        bytes[4..8].copy_from_slice(b"V9.9");
613        let err = read_bif_from_bytes(&bytes).expect_err("must fail");
614        assert!(matches!(err, BifBinaryError::InvalidVersion(_)));
615    }
616
617    #[test]
618    fn rejects_truncated_header() {
619        let bytes = vec![0_u8; FILE_HEADER_SIZE - 1];
620        let err = read_bif_from_bytes(&bytes).expect_err("must fail");
621        assert!(matches!(err, BifBinaryError::InvalidHeader(_)));
622    }
623
624    #[cfg(not(feature = "bzf"))]
625    #[test]
626    fn bzf_requires_feature_when_disabled() {
627        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
628        bytes[0..4].copy_from_slice(&BZF_MAGIC);
629        bytes[4..8].copy_from_slice(&BIF_VERSION_V10);
630
631        let err = read_bif_from_bytes(&bytes).expect_err("must fail");
632        assert!(matches!(err, BifBinaryError::BzfFeatureDisabled));
633    }
634
635    #[cfg(feature = "bzf")]
636    #[test]
637    fn roundtrip_synthetic_bzf() {
638        let mut bif = Bif::new();
639        bif.container = BifContainer::Bzf;
640        bif.push_resource(
641            ResourceId::from_raw(0x0010_0000),
642            ResourceTypeCode::from_raw_id(2017),
643            b"Hello World 1".to_vec(),
644        );
645        bif.push_resource(
646            ResourceId::from_raw(0x0010_0001),
647            ResourceTypeCode::from_raw_id(2017),
648            b"Hello World 2".to_vec(),
649        );
650        bif.push_fixed_resource(
651            ResourceId::from_raw(0x0010_4000),
652            ResourceTypeCode::from_raw_id(2017),
653            2,
654            b"Hello Fixed".to_vec(),
655        );
656
657        let bytes = write_bif_to_vec(&bif).expect("write should succeed");
658        assert_eq!(&bytes[0..4], b"BZF ");
659        let parsed = read_bif_from_bytes_with_options(
660            &bytes,
661            BifReadOptions {
662                input: BifReadMode::CompatibilityAurora,
663            },
664        )
665        .expect("read should succeed");
666        assert_eq!(parsed, bif);
667    }
668
669    #[cfg(feature = "bzf")]
670    #[test]
671    fn bzf_reader_tolerates_trailing_zero_padding() {
672        let mut bif = Bif::new();
673        bif.container = BifContainer::Bzf;
674        bif.push_resource(
675            ResourceId::from_raw(0x0010_0000),
676            ResourceTypeCode::from_raw_id(2017),
677            b"Hello World".to_vec(),
678        );
679
680        let mut bytes = write_bif_to_vec(&bif).expect("write should succeed");
681        bytes.extend_from_slice(&[0, 0, 0, 0]);
682        let parsed = read_bif_from_bytes(&bytes).expect("read should succeed");
683        assert_eq!(parsed, bif);
684    }
685
686    #[test]
687    fn rejects_out_of_bounds_resource_data() {
688        let mut bytes = vec![0_u8; FILE_HEADER_SIZE + VARIABLE_ENTRY_SIZE];
689        bytes[0..4].copy_from_slice(&BIF_MAGIC);
690        bytes[4..8].copy_from_slice(&BIF_VERSION_V10);
691        bytes[8..12].copy_from_slice(&1_u32.to_le_bytes());
692        bytes[16..20].copy_from_slice(
693            &u32::try_from(FILE_HEADER_SIZE)
694                .expect("FILE_HEADER_SIZE fits in u32")
695                .to_le_bytes(),
696        );
697
698        let base = FILE_HEADER_SIZE;
699        bytes[base..base + 4].copy_from_slice(&1_u32.to_le_bytes());
700        bytes[base + 4..base + 8].copy_from_slice(&999_u32.to_le_bytes());
701        bytes[base + 8..base + 12].copy_from_slice(&10_u32.to_le_bytes());
702        bytes[base + 12..base + 16].copy_from_slice(&2017_u32.to_le_bytes());
703
704        let err = read_bif_from_bytes(&bytes).expect_err("must fail");
705        assert!(matches!(err, BifBinaryError::InvalidData(_)));
706    }
707
708    #[test]
709    fn resource_lookup_by_id_accepts_raw_and_typed_values() {
710        let mut bif = Bif::new();
711        let resource_id = ResourceId::from_raw(0x0010_0000);
712        bif.push_resource(
713            resource_id,
714            ResourceTypeCode::from_raw_id(2017),
715            b"abc".to_vec(),
716        );
717
718        assert_eq!(bif.resource_by_id(resource_id), Some(b"abc".as_slice()));
719        assert_eq!(
720            bif.resource_by_id(rakata_core::ResourceId::from_raw(resource_id.raw())),
721            Some(b"abc".as_slice())
722        );
723    }
724}