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//! ## 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}