Skip to main content

rakata_formats/rim/
reader.rs

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