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