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