rakata_formats/rim/
mod.rs

1//! RIM binary reader and writer.
2//!
3//! RIM is a compact resource archive format used by KotOR module assets.
4//! This module implements strict parsing plus deterministic serialization.
5//!
6//! ## Format Layout
7//! ```text
8//! +------------------------------+ 0x0000
9//! | Header (120 bytes)           |
10//! +------------------------------+ keys_offset
11//! | Key table                    |
12//! | 32 bytes * entry_count       |
13//! +------------------------------+ offsets from key entries
14//! | Resource payload blob        |
15//! | (concatenated file data)     |
16//! +------------------------------+
17//! ```
18//!
19//! ## Header (120 bytes)
20//! ```text
21//! 0x00..0x04  magic          ("RIM ")
22//! 0x04..0x08  version        ("V1.0")
23//! 0x08..0x0C  reserved_0x08  (u32, preserved verbatim)
24//! 0x0C..0x10  entry_count    (u32)
25//! 0x10..0x14  keys_offset    (u32)
26//! 0x14..0x18  reserved_0x14  (u32, preserved verbatim)
27//! 0x18..0x78  reserved_0x18  (96 bytes, preserved verbatim)
28//! ```
29//!
30//! ## Key Entry (32 bytes)
31//! ```text
32//! 0x00..0x10  resref[16]   (null padded)
33//! 0x10..0x14  type_id      (u32; validated to u16 range)
34//! 0x14..0x18  resource_id  (u32)
35//! 0x18..0x1C  data_offset  (u32)
36//! 0x1C..0x20  data_size    (u32)
37//! ```
38
39mod reader;
40mod writer;
41
42pub use reader::{
43    read_rim, read_rim_from_bytes, read_rim_from_bytes_with_options, read_rim_with_options,
44};
45pub use writer::{write_rim, write_rim_to_vec};
46
47use thiserror::Error;
48
49use rakata_core::{
50    DecodeTextError, EncodeTextError, ResRef, ResRefError, ResourceTypeCode, TextEncoding,
51};
52
53use crate::binary::{self, DecodeBinary, EncodeBinary};
54
55/// RIM binary header size.
56const FILE_HEADER_SIZE: usize = 120;
57/// RIM resource key entry size.
58const KEY_ENTRY_SIZE: usize = 32;
59/// RIM container signature used by KotOR.
60const RIM_MAGIC: [u8; 4] = *b"RIM ";
61/// RIM container version used by KotOR.
62const RIM_VERSION_V10: [u8; 4] = *b"V1.0";
63/// RIM resref text encoding.
64const RIM_TEXT_ENCODING: TextEncoding = TextEncoding::Windows1252;
65
66/// Reader options for RIM parsing.
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub struct RimReadOptions {
69    /// Input-profile behavior for tolerance rules.
70    pub input: RimReadMode,
71}
72
73impl Default for RimReadOptions {
74    fn default() -> Self {
75        Self {
76            input: RimReadMode::CanonicalK1,
77        }
78    }
79}
80
81/// Input-profile behavior for RIM readers.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
83pub enum RimReadMode {
84    /// Canonical KotOR profile:
85    /// - tolerates `keys_offset == 0` by falling back to 120-byte header size.
86    ///   Some files rely on this implicit layout convention.
87    #[default]
88    CanonicalK1,
89    /// Strict profile:
90    /// - requires nonzero `keys_offset` and rejects implicit-offset files.
91    StrictExplicitOffsets,
92}
93
94/// One resource entry stored in a RIM archive.
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct RimResource {
97    /// Resource name (up to 16 bytes on disk).
98    pub resref: ResRef,
99    /// Resource type code from the key table.
100    ///
101    /// Unknown numeric IDs are preserved as raw values.
102    pub resource_type: ResourceTypeCode,
103    /// Resource payload bytes.
104    pub data: Vec<u8>,
105}
106
107/// In-memory RIM archive.
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct Rim {
110    /// Reserved u32 at header offset 0x08.
111    ///
112    /// Confirmed structurally inert by Ghidra analysis of `AddResourceImageContents`
113    /// (0x0040f990): the field is not accessed by the K1 key-table loading path.
114    /// Preserved verbatim for lossless roundtrip; new files initialize to zero.
115    pub reserved_0x08: u32,
116    /// Reserved u32 at header offset 0x14.
117    ///
118    /// Vanilla tools write 0 here (implicit resources offset). Not accessed by the
119    /// K1 key-table loader. Preserved verbatim for lossless roundtrip; new files
120    /// initialize to zero.
121    pub reserved_0x14: u32,
122    /// Reserved 96-byte block at header offsets 0x18-0x77.
123    ///
124    /// Never accessed by the engine. Preserved verbatim for lossless roundtrip;
125    /// new files initialize to zero.
126    pub reserved_0x18: [u8; 96],
127    /// Ordered archive resources.
128    pub resources: Vec<RimResource>,
129}
130
131impl Default for Rim {
132    fn default() -> Self {
133        Self {
134            reserved_0x08: 0,
135            reserved_0x14: 0,
136            reserved_0x18: [0u8; 96],
137            resources: Vec::new(),
138        }
139    }
140}
141
142impl Rim {
143    /// Creates an empty RIM archive.
144    pub fn new() -> Self {
145        Self::default()
146    }
147
148    /// Appends one resource entry.
149    pub fn push_resource(
150        &mut self,
151        resref: ResRef,
152        resource_type: ResourceTypeCode,
153        data: Vec<u8>,
154    ) {
155        self.resources.push(RimResource {
156            resref,
157            resource_type,
158            data,
159        });
160    }
161
162    /// Returns the first matching resource payload.
163    pub fn resource(&self, resref: &ResRef, resource_type: ResourceTypeCode) -> Option<&[u8]> {
164        self.resources
165            .iter()
166            .find(|resource| resource.resref == *resref && resource.resource_type == resource_type)
167            .map(|resource| resource.data.as_slice())
168    }
169}
170
171impl DecodeBinary for Rim {
172    type Error = RimBinaryError;
173
174    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
175        read_rim_from_bytes(bytes)
176    }
177}
178
179impl EncodeBinary for Rim {
180    type Error = RimBinaryError;
181
182    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
183        write_rim_to_vec(self)
184    }
185}
186
187/// Errors produced while parsing or serializing RIM binary data.
188#[derive(Debug, Error)]
189pub enum RimBinaryError {
190    /// I/O read/write failure.
191    #[error(transparent)]
192    Io(#[from] std::io::Error),
193    /// Header signature is not `RIM `.
194    #[error("invalid RIM magic: {0:?}")]
195    InvalidMagic([u8; 4]),
196    /// Header version is unsupported.
197    #[error("invalid RIM version: {0:?}")]
198    InvalidVersion([u8; 4]),
199    /// Header/body layout is invalid or truncated.
200    #[error("invalid RIM header: {0}")]
201    InvalidHeader(String),
202    /// Archive content is structurally invalid.
203    #[error("invalid RIM data: {0}")]
204    InvalidData(String),
205    /// Value cannot fit on-disk integer width.
206    #[error("value overflow while writing field `{0}`")]
207    ValueOverflow(&'static str),
208    /// Resource name validation failed during read.
209    #[error("invalid resref at {context}: {source}")]
210    InvalidResRef {
211        /// Field context.
212        context: String,
213        /// Validation error details.
214        #[source]
215        source: ResRefError,
216    },
217    /// Text cannot be represented as RIM encoding.
218    #[error("RIM text encoding failed for {context}: {source}")]
219    TextEncoding {
220        /// Value context.
221        context: String,
222        /// Encoding error details.
223        #[source]
224        source: EncodeTextError,
225    },
226    /// Text bytes cannot be decoded as RIM encoding.
227    #[error("RIM text decoding failed for {context}: {source}")]
228    TextDecoding {
229        /// Value context.
230        context: String,
231        /// Decoding error details.
232        #[source]
233        source: DecodeTextError,
234    },
235}
236
237impl From<binary::BinaryLayoutError> for RimBinaryError {
238    fn from(error: binary::BinaryLayoutError) -> Self {
239        Self::InvalidHeader(error.to_string())
240    }
241}