1use 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#[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#[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#[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#[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 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}