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