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}