1use std::io::Write;
17use thiserror::Error;
18
19use rakata_core::{encode_text, EncodeTextError, TextEncoding};
20
21#[derive(Debug, Clone, PartialEq, Eq, Error)]
23pub enum BinaryLayoutError {
24 #[error("{0} overflow")]
26 Overflow(&'static str),
27 #[error("unexpected EOF while reading {0}")]
29 UnexpectedEof(&'static str),
30 #[error("{0} exceeds file bounds")]
32 BoundsExceeded(String),
33}
34
35pub trait DecodeBinary: Sized {
37 type Error;
39
40 fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error>;
47}
48
49pub trait EncodeBinary {
51 type Error;
53
54 fn encode_binary(&self) -> Result<Vec<u8>, Self::Error>;
61}
62
63pub fn check_range_in_bounds(
70 total_len: usize,
71 offset: usize,
72 size: usize,
73 label: &str,
74) -> Result<(), BinaryLayoutError> {
75 if offset.checked_add(size).is_none_or(|end| end > total_len) {
76 return Err(BinaryLayoutError::BoundsExceeded(label.to_string()));
77 }
78 Ok(())
79}
80
81pub fn check_slice_in_bounds(
88 bytes: &[u8],
89 offset: usize,
90 size: usize,
91 label: &str,
92) -> Result<(), BinaryLayoutError> {
93 check_range_in_bounds(bytes.len(), offset, size, label)
94}
95
96pub fn checked_to_usize(value: u32, field: &'static str) -> Result<usize, BinaryLayoutError> {
104 usize::try_from(value).map_err(|_| BinaryLayoutError::Overflow(field))
105}
106
107pub fn read_fourcc(bytes: &[u8], offset: usize) -> Result<[u8; 4], BinaryLayoutError> {
113 read_array::<4>(bytes, offset, "fourcc")
114}
115
116pub fn expect_fourcc(actual: [u8; 4], expected: [u8; 4]) -> Result<(), [u8; 4]> {
123 if actual == expected {
124 Ok(())
125 } else {
126 Err(actual)
127 }
128}
129
130pub fn expect_any_fourcc(actual: [u8; 4], expected: &[[u8; 4]]) -> Result<(), [u8; 4]> {
136 if expected.contains(&actual) {
137 Ok(())
138 } else {
139 Err(actual)
140 }
141}
142
143pub fn read_u16(bytes: &[u8], offset: usize) -> Result<u16, BinaryLayoutError> {
149 Ok(u16::from_le_bytes(read_array::<2>(bytes, offset, "u16")?))
150}
151
152pub fn read_u8(bytes: &[u8], offset: usize) -> Result<u8, BinaryLayoutError> {
158 read_array::<1>(bytes, offset, "u8").map(|[b]| b)
159}
160
161pub fn read_u32(bytes: &[u8], offset: usize) -> Result<u32, BinaryLayoutError> {
167 Ok(u32::from_le_bytes(read_array::<4>(bytes, offset, "u32")?))
168}
169
170pub fn read_u64(bytes: &[u8], offset: usize) -> Result<u64, BinaryLayoutError> {
176 Ok(u64::from_le_bytes(read_array::<8>(bytes, offset, "u64")?))
177}
178
179pub fn read_i32(bytes: &[u8], offset: usize) -> Result<i32, BinaryLayoutError> {
185 Ok(i32::from_le_bytes(read_array::<4>(bytes, offset, "i32")?))
186}
187
188pub fn read_f32(bytes: &[u8], offset: usize) -> Result<f32, BinaryLayoutError> {
195 Ok(f32::from_le_bytes(read_array::<4>(bytes, offset, "f32")?))
196}
197
198pub fn write_u8<W: Write>(writer: &mut W, value: u8) -> std::io::Result<()> {
204 writer.write_all(&[value])
205}
206
207pub fn write_u16<W: Write>(writer: &mut W, value: u16) -> std::io::Result<()> {
213 writer.write_all(&value.to_le_bytes())
214}
215
216pub fn write_u32<W: Write>(writer: &mut W, value: u32) -> std::io::Result<()> {
222 writer.write_all(&value.to_le_bytes())
223}
224
225pub fn write_i32<W: Write>(writer: &mut W, value: i32) -> std::io::Result<()> {
231 writer.write_all(&value.to_le_bytes())
232}
233
234pub fn write_u64<W: Write>(writer: &mut W, value: u64) -> std::io::Result<()> {
240 writer.write_all(&value.to_le_bytes())
241}
242
243pub fn write_f32<W: Write>(writer: &mut W, value: f32) -> std::io::Result<()> {
249 writer.write_all(&value.to_le_bytes())
250}
251
252pub fn write_fourcc<W: Write>(writer: &mut W, tag: [u8; 4]) -> std::io::Result<()> {
258 writer.write_all(&tag)
259}
260
261pub fn read_fixed_c_string(bytes: &[u8], offset: usize, max_len: usize) -> String {
270 let end = (offset + max_len).min(bytes.len());
271 let slice = &bytes[offset..end];
272 let nul_pos = slice.iter().position(|&b| b == 0).unwrap_or(slice.len());
273 rakata_core::text::decode_text(&slice[..nul_pos], TextEncoding::Windows1252)
274}
275
276pub fn read_c_string(bytes: &[u8], offset: usize) -> String {
281 let end = bytes[offset..]
282 .iter()
283 .position(|&b| b == 0)
284 .unwrap_or(bytes.len() - offset);
285 rakata_core::text::decode_text(&bytes[offset..offset + end], TextEncoding::Windows1252)
286}
287
288pub fn write_fixed_c_string<W: Write>(
298 writer: &mut W,
299 s: &str,
300 field_size: usize,
301) -> std::io::Result<()> {
302 let bytes = s.as_bytes();
303 let write_len = bytes.len().min(field_size.saturating_sub(1));
304 writer.write_all(&bytes[..write_len])?;
305 let pad = field_size - write_len;
306 for _ in 0..pad {
307 writer.write_all(&[0])?;
308 }
309 Ok(())
310}
311
312pub fn write_cp1252<W: Write, E, F>(
321 writer: &mut W,
322 text: &str,
323 context: String,
324 map_text_error: F,
325) -> Result<(), E>
326where
327 E: From<std::io::Error>,
328 F: FnOnce(String, EncodeTextError) -> E,
329{
330 let encoded = encode_text(text, TextEncoding::Windows1252)
331 .map_err(|source| map_text_error(context, source))?;
332 writer.write_all(&encoded).map_err(E::from)
333}
334
335pub fn matches_resource_key<T: Eq>(
338 entry_resref: &str,
339 entry_type: T,
340 query_resref: &str,
341 query_type: T,
342) -> bool {
343 entry_type == query_type && entry_resref.eq_ignore_ascii_case(query_resref)
344}
345
346fn read_array<const N: usize>(
347 bytes: &[u8],
348 offset: usize,
349 context: &'static str,
350) -> Result<[u8; N], BinaryLayoutError> {
351 let end = offset
352 .checked_add(N)
353 .ok_or(BinaryLayoutError::Overflow(context))?;
354 let raw = bytes
355 .get(offset..end)
356 .ok_or(BinaryLayoutError::UnexpectedEof(context))?;
357 let mut out = [0_u8; N];
358 out.copy_from_slice(raw);
359 Ok(out)
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn reads_primitives_from_little_endian_bytes() {
368 let bytes = [
369 b'R', b'I', b'M', b' ', 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, 0x00, 0x80, 0x3f, ];
375
376 assert_eq!(read_fourcc(&bytes, 0).expect("fourcc"), *b"RIM ");
377 assert_eq!(read_u16(&bytes, 4).expect("u16"), 0x1234);
378 assert_eq!(read_u32(&bytes, 6).expect("u32"), 0x1234_5678);
379 assert_eq!(read_u64(&bytes, 10).expect("u64"), 0x0102_0304_0506_0708);
380 assert_eq!(read_f32(&bytes, 18).expect("f32"), 1.0);
381 }
382
383 #[test]
384 fn validates_single_or_multiple_fourcc_values() {
385 assert_eq!(expect_fourcc(*b"RIM ", *b"RIM "), Ok(()));
386 assert_eq!(expect_fourcc(*b"RIM ", *b"GFF "), Err(*b"RIM "));
387
388 assert_eq!(expect_any_fourcc(*b"V1.1", &[*b"V1 ", *b"V1.1"]), Ok(()));
389 assert_eq!(
390 expect_any_fourcc(*b"V9.9", &[*b"V1 ", *b"V1.1"]),
391 Err(*b"V9.9")
392 );
393 }
394
395 #[test]
396 fn reports_eof_and_overflow_for_invalid_reads() {
397 let bytes = [0_u8; 4];
398
399 let eof = read_u32(&bytes, 2).expect_err("expected EOF");
400 assert!(matches!(eof, BinaryLayoutError::UnexpectedEof("u32")));
401
402 let overflow = read_u32(&bytes, usize::MAX).expect_err("expected overflow");
403 assert!(matches!(overflow, BinaryLayoutError::Overflow("u32")));
404 }
405
406 #[test]
407 fn validates_slice_bounds() {
408 let bytes = [0_u8; 16];
409
410 check_slice_in_bounds(&bytes, 4, 8, "table").expect("in bounds");
411
412 let err = check_slice_in_bounds(&bytes, 12, 8, "table").expect_err("must fail");
413 assert_eq!(err, BinaryLayoutError::BoundsExceeded("table".into()));
414 }
415
416 #[test]
417 fn converts_u32_to_usize_with_error() {
418 assert_eq!(checked_to_usize(42, "count").expect("convert"), 42usize);
419 }
420
421 #[test]
422 fn writes_little_endian_primitives() {
423 let mut out = Vec::new();
424 write_u8(&mut out, 0xAB).expect("write u8");
425 write_u16(&mut out, 0x1234).expect("write u16");
426 write_u32(&mut out, 0x1234_5678).expect("write u32");
427 write_u64(&mut out, 0x0102_0304_0506_0708).expect("write u64");
428 write_f32(&mut out, 1.0_f32).expect("write f32");
429 write_fourcc(&mut out, *b"RIM ").expect("write fourcc");
430 assert_eq!(
431 out,
432 vec![
433 0xAB, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12, 0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, 0x00, 0x80, 0x3f, b'R', b'I', b'M', b' ', ]
440 );
441 }
442
443 #[derive(Debug, PartialEq, Eq)]
444 enum Cp1252TestError {
445 Io,
446 Encode { context: String },
447 }
448
449 impl From<std::io::Error> for Cp1252TestError {
450 fn from(_value: std::io::Error) -> Self {
451 Self::Io
452 }
453 }
454
455 #[test]
456 fn writes_cp1252_text_with_context_mapping() {
457 let mut out = Vec::new();
458 write_cp1252(&mut out, "café", "payload".into(), |context, _source| {
459 Cp1252TestError::Encode { context }
460 })
461 .expect("cp1252 should encode");
462 assert_eq!(out, b"caf\xe9");
463 }
464
465 #[test]
466 fn reports_cp1252_encoding_failures_with_context() {
467 let mut out = Vec::new();
468 let err = write_cp1252(
469 &mut out,
470 "emoji \u{1f600}",
471 "payload".into(),
472 |context, _source| Cp1252TestError::Encode { context },
473 )
474 .expect_err("must fail");
475 assert_eq!(
476 err,
477 Cp1252TestError::Encode {
478 context: "payload".into()
479 }
480 );
481 }
482
483 #[test]
484 fn matches_resource_keys_case_insensitively() {
485 assert!(matches_resource_key(
486 "P_Bastila",
487 2014_u16,
488 "p_bastila",
489 2014_u16
490 ));
491 assert!(!matches_resource_key(
492 "P_Bastila",
493 2014_u16,
494 "p_bastila",
495 2015_u16
496 ));
497 assert!(!matches_resource_key(
498 "P_Bastila",
499 2014_u16,
500 "p_carth",
501 2014_u16
502 ));
503 }
504}