1use std::collections::HashMap;
12use std::fs;
13use std::path::{Path, PathBuf};
14
15use thiserror::Error;
16
17use rakata_core::ResRef;
18use rakata_core::ResourceTypeCode;
19use rakata_formats::{
20 read_erf_from_bytes, read_rim_from_bytes, read_save_archive_from_bytes, Erf, ErfBinaryError,
21 ErfResource, Rim, RimBinaryError, RimResource,
22};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct CapsuleResourceRef<'a> {
27 pub resref: &'a ResRef,
30 pub resource_type: ResourceTypeCode,
32 pub data: &'a [u8],
34}
35
36#[derive(Debug, Clone)]
38pub struct Capsule {
39 path: Option<PathBuf>,
40 archive: CapsuleArchive,
41 resource_index: HashMap<(ResRef, ResourceTypeCode), usize>,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum CapsuleArchive {
48 Erf(Erf),
50 Rim(Rim),
52}
53
54impl Capsule {
55 pub fn read_from_bytes(bytes: &[u8]) -> Result<Self, CapsuleError> {
57 if bytes.len() < 4 {
58 return Err(CapsuleError::InvalidMagic {
59 magic: [0, 0, 0, 0],
60 });
61 }
62 let magic = [bytes[0], bytes[1], bytes[2], bytes[3]];
63 let archive = match &magic {
64 b"RIM " => CapsuleArchive::Rim(read_rim_from_bytes(bytes)?),
65 b"ERF " | b"MOD " | b"HAK " => CapsuleArchive::Erf(read_erf_from_bytes(bytes)?),
66 b"SAV " => CapsuleArchive::Erf(read_save_archive_from_bytes(bytes)?),
69 _ => return Err(CapsuleError::InvalidMagic { magic }),
70 };
71 let resource_index = build_capsule_index(&archive);
72 Ok(Self {
73 path: None,
74 archive,
75 resource_index,
76 })
77 }
78
79 pub fn read_from_file(path: impl AsRef<Path>) -> Result<Self, CapsuleError> {
81 let path = path.as_ref();
82 let bytes = fs::read(path).map_err(|source| CapsuleError::Io {
83 path: path.to_path_buf(),
84 source,
85 })?;
86 let mut capsule = Self::read_from_bytes(&bytes)?;
87 capsule.path = Some(path.to_path_buf());
88 Ok(capsule)
89 }
90
91 pub fn path(&self) -> Option<&Path> {
93 self.path.as_deref()
94 }
95
96 pub fn archive(&self) -> &CapsuleArchive {
98 &self.archive
99 }
100
101 pub fn resource(&self, resref: &ResRef, resource_type: ResourceTypeCode) -> Option<&[u8]> {
103 let &i = self.resource_index.get(&(*resref, resource_type))?;
104 match &self.archive {
105 CapsuleArchive::Erf(erf) => Some(erf.resources.get(i)?.data.as_slice()),
106 CapsuleArchive::Rim(rim) => Some(rim.resources.get(i)?.data.as_slice()),
107 }
108 }
109
110 pub fn resources(&self) -> impl Iterator<Item = CapsuleResourceRef<'_>> {
112 let (erf_iter, rim_iter) = match &self.archive {
113 CapsuleArchive::Erf(erf) => (Some(erf.resources.iter().map(erf_resource_ref)), None),
114 CapsuleArchive::Rim(rim) => (None, Some(rim.resources.iter().map(rim_resource_ref))),
115 };
116 erf_iter
117 .into_iter()
118 .flatten()
119 .chain(rim_iter.into_iter().flatten())
120 }
121
122 pub fn resource_count(&self) -> usize {
124 match &self.archive {
125 CapsuleArchive::Erf(erf) => erf.resources.len(),
126 CapsuleArchive::Rim(rim) => rim.resources.len(),
127 }
128 }
129}
130
131fn build_capsule_index(archive: &CapsuleArchive) -> HashMap<(ResRef, ResourceTypeCode), usize> {
133 match archive {
134 CapsuleArchive::Erf(erf) => {
135 let mut index = HashMap::with_capacity(erf.resources.len());
136 for (i, resource) in erf.resources.iter().enumerate() {
137 index
138 .entry((resource.resref, resource.resource_type))
139 .or_insert(i);
140 }
141 index
142 }
143 CapsuleArchive::Rim(rim) => {
144 let mut index = HashMap::with_capacity(rim.resources.len());
145 for (i, resource) in rim.resources.iter().enumerate() {
146 index
147 .entry((resource.resref, resource.resource_type))
148 .or_insert(i);
149 }
150 index
151 }
152 }
153}
154
155fn erf_resource_ref(resource: &ErfResource) -> CapsuleResourceRef<'_> {
156 CapsuleResourceRef {
157 resref: &resource.resref,
158 resource_type: resource.resource_type,
159 data: &resource.data,
160 }
161}
162
163fn rim_resource_ref(resource: &RimResource) -> CapsuleResourceRef<'_> {
164 CapsuleResourceRef {
165 resref: &resource.resref,
166 resource_type: resource.resource_type,
167 data: &resource.data,
168 }
169}
170
171#[derive(Debug, Error)]
173pub enum CapsuleError {
174 #[error("I/O failure for `{path}`: {source}")]
176 Io {
177 path: PathBuf,
179 #[source]
181 source: std::io::Error,
182 },
183 #[error("unsupported capsule magic: {magic:?}")]
185 InvalidMagic {
186 magic: [u8; 4],
188 },
189 #[error(transparent)]
191 Erf(#[from] ErfBinaryError),
192 #[error(transparent)]
194 Rim(#[from] RimBinaryError),
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 use rakata_core::ResourceType;
202 use rakata_formats::{write_erf_to_vec, write_rim_to_vec, ErfFileType};
203
204 #[test]
205 fn reads_erf_capsule_and_queries_resource() {
206 let mut erf = Erf::new(ErfFileType::Erf);
207 let dlg_type = ResourceTypeCode::from(ResourceType::Dlg);
208 erf.push_resource(
209 ResRef::new("module").expect("valid resref"),
210 dlg_type,
211 b"dialog".to_vec(),
212 );
213 let bytes = write_erf_to_vec(&erf).expect("write erf");
214
215 let capsule = Capsule::read_from_bytes(&bytes).expect("read capsule");
216 let resref = ResRef::new("module").expect("valid resref");
217 assert_eq!(capsule.resource(&resref, dlg_type), Some(&b"dialog"[..]));
218 assert_eq!(capsule.resource_count(), 1);
219 }
220
221 #[test]
222 fn reads_rim_capsule_and_queries_resource() {
223 let mut rim = Rim::new();
224 let git_type = ResourceTypeCode::from(ResourceType::Git);
225 rim.push_resource(
226 ResRef::new("module").expect("valid resref"),
227 git_type,
228 b"git".to_vec(),
229 );
230 let bytes = write_rim_to_vec(&rim).expect("write rim");
231
232 let capsule = Capsule::read_from_bytes(&bytes).expect("read capsule");
233 let resref = ResRef::new("module").expect("valid resref");
234 assert_eq!(capsule.resource(&resref, git_type), Some(&b"git"[..]));
235 assert_eq!(capsule.resource_count(), 1);
236 }
237
238 #[test]
239 fn rejects_unknown_capsule_magic() {
240 let err = Capsule::read_from_bytes(b"ABCDpayload").expect_err("must fail");
241 assert!(matches!(err, CapsuleError::InvalidMagic { .. }));
242 }
243}