Skip to main content

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}