rakata_formats/rim/
reader.rs

1//! RIM binary reader.
2
3use std::io::Read;
4
5use rakata_core::{decode_text_strict, ResRef};
6
7use crate::binary;
8
9use super::{
10    Rim, RimBinaryError, RimReadMode, RimReadOptions, RimResource, FILE_HEADER_SIZE,
11    KEY_ENTRY_SIZE, RIM_MAGIC, RIM_TEXT_ENCODING, RIM_VERSION_V10,
12};
13
14/// Reads a RIM archive from a reader.
15///
16/// The stream is consumed from its current position.
17#[cfg_attr(
18    feature = "tracing",
19    tracing::instrument(level = "debug", skip(reader))
20)]
21pub fn read_rim<R: Read>(reader: &mut R) -> Result<Rim, RimBinaryError> {
22    read_rim_with_options(reader, RimReadOptions::default())
23}
24
25/// Reads a RIM archive from a reader with explicit read options.
26#[cfg_attr(
27    feature = "tracing",
28    tracing::instrument(level = "debug", skip(reader))
29)]
30pub fn read_rim_with_options<R: Read>(
31    reader: &mut R,
32    options: RimReadOptions,
33) -> Result<Rim, RimBinaryError> {
34    let mut bytes = Vec::new();
35    reader.read_to_end(&mut bytes)?;
36    crate::trace_debug!(bytes_len = bytes.len(), "read rim bytes from reader");
37    read_rim_from_bytes_with_options(&bytes, options)
38}
39
40/// Reads a RIM archive from bytes.
41#[cfg_attr(
42    feature = "tracing",
43    tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
44)]
45pub fn read_rim_from_bytes(bytes: &[u8]) -> Result<Rim, RimBinaryError> {
46    read_rim_from_bytes_with_options(bytes, RimReadOptions::default())
47}
48
49/// Reads a RIM archive from bytes with explicit read options.
50#[cfg_attr(
51    feature = "tracing",
52    tracing::instrument(
53        level = "debug",
54        skip(bytes),
55        fields(bytes_len = bytes.len(), input_mode = ?options.input)
56    )
57)]
58pub fn read_rim_from_bytes_with_options(
59    bytes: &[u8],
60    options: RimReadOptions,
61) -> Result<Rim, RimBinaryError> {
62    if bytes.len() < FILE_HEADER_SIZE {
63        return Err(RimBinaryError::InvalidHeader(
64            "file smaller than RIM header".into(),
65        ));
66    }
67
68    let magic = binary::read_fourcc(bytes, 0)?;
69    if magic != RIM_MAGIC {
70        return Err(RimBinaryError::InvalidMagic(magic));
71    }
72    let version = binary::read_fourcc(bytes, 4)?;
73    if version != RIM_VERSION_V10 {
74        return Err(RimBinaryError::InvalidVersion(version));
75    }
76
77    let reserved_0x08 = binary::read_u32(bytes, 8)?;
78    let entry_count = binary::checked_to_usize(binary::read_u32(bytes, 12)?, "entry_count")?;
79    let mut keys_offset = binary::checked_to_usize(binary::read_u32(bytes, 16)?, "keys_offset")?;
80    let reserved_0x14 = binary::read_u32(bytes, 20)?;
81    let mut reserved_0x18 = [0u8; 96];
82    if let Some(slice) = bytes.get(24..24 + 96) {
83        reserved_0x18.copy_from_slice(slice);
84    }
85    if keys_offset == 0 {
86        keys_offset = match options.input {
87            RimReadMode::StrictExplicitOffsets => {
88                return Err(RimBinaryError::InvalidHeader(
89                    "keys_offset is zero (strict mode requires explicit key table offset)".into(),
90                ));
91            }
92            RimReadMode::CanonicalK1 => FILE_HEADER_SIZE,
93        };
94    }
95
96    let keys_table_size =
97        entry_count
98            .checked_mul(KEY_ENTRY_SIZE)
99            .ok_or(RimBinaryError::InvalidHeader(
100                "keys table size overflow".into(),
101            ))?;
102    binary::check_slice_in_bounds(bytes, keys_offset, keys_table_size, "keys table")?;
103
104    let mut resources = Vec::with_capacity(entry_count);
105    for key_index in 0..entry_count {
106        let base = keys_offset + key_index * KEY_ENTRY_SIZE;
107        let raw_resref = bytes
108            .get(base..base + 16)
109            .ok_or_else(|| RimBinaryError::InvalidData("resref key bytes missing".into()))?;
110        let end = raw_resref.iter().position(|byte| *byte == 0).unwrap_or(16);
111        let resref_str =
112            decode_text_strict(&raw_resref[..end], RIM_TEXT_ENCODING).map_err(|source| {
113                RimBinaryError::TextDecoding {
114                    context: format!("keys[{key_index}].resref"),
115                    source,
116                }
117            })?;
118        let resref = ResRef::new(&resref_str).map_err(|source| RimBinaryError::InvalidResRef {
119            context: format!("keys[{key_index}].resref"),
120            source,
121        })?;
122        let resource_type_u32 = binary::read_u32(bytes, base + 16)?;
123        let resource_type_id = u16::try_from(resource_type_u32).map_err(|_| {
124            RimBinaryError::InvalidData(format!(
125                "keys[{key_index}] resource_type_id {resource_type_u32} exceeds u16 range"
126            ))
127        })?;
128        let data_offset =
129            binary::checked_to_usize(binary::read_u32(bytes, base + 24)?, "resource_data_offset")?;
130        let data_size =
131            binary::checked_to_usize(binary::read_u32(bytes, base + 28)?, "resource_data_size")?;
132        binary::check_slice_in_bounds(
133            bytes,
134            data_offset,
135            data_size,
136            &format!("resource data[{key_index}]"),
137        )?;
138        let data = bytes
139            .get(data_offset..(data_offset + data_size))
140            .ok_or_else(|| {
141                RimBinaryError::InvalidData(format!(
142                    "resource data slice missing for keys[{key_index}]"
143                ))
144            })?
145            .to_vec();
146        resources.push(RimResource {
147            resref,
148            resource_type: rakata_core::ResourceTypeCode::from_raw_id(resource_type_id),
149            data,
150        });
151    }
152
153    let rim = Rim {
154        reserved_0x08,
155        reserved_0x14,
156        reserved_0x18,
157        resources,
158    };
159    crate::trace_debug!(
160        resource_count = rim.resources.len(),
161        "parsed rim from bytes"
162    );
163    Ok(rim)
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::rim::write_rim_to_vec;
170
171    const TEST_RIM: &[u8] = include_bytes!(concat!(
172        env!("CARGO_MANIFEST_DIR"),
173        "/../../fixtures/test.rim"
174    ));
175    const CAPSULE_RIM: &[u8] = include_bytes!(concat!(
176        env!("CARGO_MANIFEST_DIR"),
177        "/../../fixtures/capsule.rim"
178    ));
179    const CORRUPTED_RIM: &[u8] = include_bytes!(concat!(
180        env!("CARGO_MANIFEST_DIR"),
181        "/../../fixtures/test_corrupted.rim"
182    ));
183
184    #[test]
185    fn parses_rim_fixture() {
186        let rim = read_rim_from_bytes(TEST_RIM).expect("fixture should parse");
187        assert_eq!(rim.resources.len(), 3);
188        assert_eq!(
189            rim.resource(
190                &ResRef::new("1").unwrap(),
191                rakata_core::ResourceTypeCode::from_raw_id(10)
192            ),
193            Some(b"abc".as_slice())
194        );
195        assert_eq!(
196            rim.resource(
197                &ResRef::new("2").unwrap(),
198                rakata_core::ResourceTypeCode::from_raw_id(10)
199            ),
200            Some(b"def".as_slice())
201        );
202        assert_eq!(
203            rim.resource(
204                &ResRef::new("3").unwrap(),
205                rakata_core::ResourceTypeCode::from_raw_id(10)
206            ),
207            Some(b"ghi".as_slice())
208        );
209    }
210
211    #[test]
212    fn parses_capsule_rim_fixture() {
213        let rim = read_rim_from_bytes(CAPSULE_RIM).expect("fixture should parse");
214        assert_eq!(rim.resources.len(), 3);
215        assert_eq!(rim.resources[0].resref, "m13aa");
216        assert!(!rim.resources[0].data.is_empty());
217    }
218
219    #[test]
220    fn roundtrip_synthetic_rim() {
221        let mut rim = Rim::new();
222        rim.push_resource(
223            ResRef::new("alpha").unwrap(),
224            rakata_core::ResourceTypeCode::from_raw_id(2017),
225            b"abc".to_vec(),
226        );
227        rim.push_resource(
228            ResRef::new("beta").unwrap(),
229            rakata_core::ResourceTypeCode::from_raw_id(2018),
230            b"defghi".to_vec(),
231        );
232
233        let bytes = write_rim_to_vec(&rim).expect("write should succeed");
234        let parsed = read_rim_from_bytes(&bytes).expect("read should succeed");
235        assert_eq!(parsed, rim);
236    }
237
238    #[test]
239    fn roundtrip_preserves_unknown_resource_type_ids() {
240        let mut rim = Rim::new();
241        rim.push_resource(
242            ResRef::new("mystery").unwrap(),
243            rakata_core::ResourceTypeCode::from_raw_id(42424),
244            vec![1, 2, 3],
245        );
246
247        let bytes = write_rim_to_vec(&rim).expect("write should succeed");
248        let parsed = read_rim_from_bytes(&bytes).expect("read should succeed");
249        assert_eq!(parsed.resources.len(), 1);
250        assert_eq!(parsed.resources[0].resource_type.raw_id(), 42424);
251        assert_eq!(parsed.resources[0].resource_type.known_type(), None);
252    }
253
254    #[test]
255    fn read_write_roundtrip_preserves_fixture_semantics() {
256        let parsed = read_rim_from_bytes(TEST_RIM).expect("fixture should parse");
257        let bytes = write_rim_to_vec(&parsed).expect("write should succeed");
258        let reparsed = read_rim_from_bytes(&bytes).expect("re-read should succeed");
259        assert_eq!(reparsed, parsed);
260    }
261
262    #[test]
263    fn writer_is_deterministic_for_parsed_fixture() {
264        let parsed = read_rim_from_bytes(TEST_RIM).expect("fixture should parse");
265        let first = write_rim_to_vec(&parsed).expect("first write should succeed");
266        let second = write_rim_to_vec(&parsed).expect("second write should succeed");
267        assert_eq!(first, second, "canonical RIM writer output drifted");
268    }
269
270    #[test]
271    fn rejects_invalid_magic() {
272        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
273        bytes[0..4].copy_from_slice(b"NOPE");
274        bytes[4..8].copy_from_slice(&RIM_VERSION_V10);
275        let err = read_rim_from_bytes(&bytes).expect_err("must fail");
276        assert!(matches!(err, RimBinaryError::InvalidMagic(_)));
277    }
278
279    #[test]
280    fn rejects_invalid_version() {
281        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
282        bytes[0..4].copy_from_slice(&RIM_MAGIC);
283        bytes[4..8].copy_from_slice(b"V9.9");
284        let err = read_rim_from_bytes(&bytes).expect_err("must fail");
285        assert!(matches!(err, RimBinaryError::InvalidVersion(_)));
286    }
287
288    #[test]
289    fn rejects_truncated_header() {
290        let bytes = vec![0_u8; FILE_HEADER_SIZE - 1];
291        let err = read_rim_from_bytes(&bytes).expect_err("must fail");
292        assert!(matches!(err, RimBinaryError::InvalidHeader(_)));
293    }
294
295    #[test]
296    fn canonical_mode_falls_back_when_keys_offset_is_zero() {
297        let mut bytes = TEST_RIM.to_vec();
298        bytes[16..20].copy_from_slice(&0_u32.to_le_bytes());
299
300        let rim = read_rim_from_bytes_with_options(
301            &bytes,
302            RimReadOptions {
303                input: RimReadMode::CanonicalK1,
304            },
305        )
306        .expect("canonical mode should parse with fallback offsets");
307        assert_eq!(rim.resources.len(), 3);
308        assert_eq!(
309            rim.resource(
310                &ResRef::new("1").unwrap(),
311                rakata_core::ResourceTypeCode::from_raw_id(10)
312            ),
313            Some(b"abc".as_slice())
314        );
315    }
316
317    #[test]
318    fn strict_mode_rejects_zero_keys_offset() {
319        let mut bytes = TEST_RIM.to_vec();
320        bytes[16..20].copy_from_slice(&0_u32.to_le_bytes());
321
322        let err = read_rim_from_bytes_with_options(
323            &bytes,
324            RimReadOptions {
325                input: RimReadMode::StrictExplicitOffsets,
326            },
327        )
328        .expect_err("strict mode should reject");
329        assert!(matches!(err, RimBinaryError::InvalidHeader(_)));
330    }
331
332    #[test]
333    fn rejects_malformed_fixture_with_invalid_key_offset() {
334        let err = read_rim_from_bytes(CORRUPTED_RIM).expect_err("must fail");
335        assert!(matches!(
336            err,
337            RimBinaryError::InvalidHeader(_) | RimBinaryError::InvalidData(_)
338        ));
339    }
340
341    #[test]
342    fn resref_validation_rejects_long_names() {
343        // ResRef validation happens at construction time, not write time
344        let result = ResRef::new("this_name_is_too_long");
345        assert!(result.is_err());
346    }
347
348    #[test]
349    fn reserved_fields_survive_roundtrip() {
350        let mut rim = Rim::new();
351        rim.reserved_0x08 = 0xDEADBEEF;
352        rim.reserved_0x14 = 0x12345678;
353        rim.reserved_0x18[0] = 0xAB;
354        rim.reserved_0x18[47] = 0xCD;
355        rim.reserved_0x18[95] = 0xEF;
356        rim.push_resource(
357            ResRef::new("a").unwrap(),
358            rakata_core::ResourceTypeCode::from_raw_id(10),
359            b"test".to_vec(),
360        );
361
362        let bytes = write_rim_to_vec(&rim).expect("write should succeed");
363        let parsed = read_rim_from_bytes(&bytes).expect("read should succeed");
364        assert_eq!(parsed.reserved_0x08, 0xDEADBEEF);
365        assert_eq!(parsed.reserved_0x14, 0x12345678);
366        assert_eq!(parsed.reserved_0x18[0], 0xAB);
367        assert_eq!(parsed.reserved_0x18[47], 0xCD);
368        assert_eq!(parsed.reserved_0x18[95], 0xEF);
369    }
370}