1use std::{
5 collections::HashMap,
6 fs::{self, DirEntry, ReadDir},
7 path::PathBuf,
8};
9
10use crate::{
11 ByteBuffer,
12 common::{Platform, read_version},
13 patch::{PatchError, ZiPatch},
14 repository::{Category, Repository, string_to_category},
15 sqpack::{IndexEntry, SqPackData, SqPackIndex},
16};
17
18use super::Resource;
19
20#[derive(Debug)]
22pub enum RepairAction {
23 VersionFileMissing,
25 VersionFileCanRestore,
27}
28
29#[derive(Debug)]
30pub enum RepairError<'a> {
32 FailedRepair(&'a Repository),
34}
35
36pub struct SqPackResource {
38 pub game_directory: String,
40
41 pub repositories: Vec<Repository>,
43
44 index_files: HashMap<String, SqPackIndex>,
45}
46
47impl SqPackResource {
48 pub fn from_existing(platform: Platform, directory: &str) -> Self {
49 match is_valid(directory) {
50 true => {
51 let mut data = Self {
52 game_directory: String::from(directory),
53 repositories: vec![],
54 index_files: HashMap::new(),
55 };
56 data.reload_repositories(platform);
57 data
58 }
59 false => {
60 Self {
62 game_directory: String::from(directory),
63 repositories: vec![],
64 index_files: HashMap::new(),
65 }
66 }
67 }
68 }
69
70 fn reload_repositories(&mut self, platform: Platform) {
71 self.repositories.clear();
72
73 let mut d = PathBuf::from(self.game_directory.as_str());
74
75 if let Some(base_repository) =
77 Repository::from_existing_base(platform.clone(), d.to_str().unwrap())
78 {
79 self.repositories.push(base_repository);
80 }
81
82 d.push("sqpack");
84
85 if let Ok(repository_paths) = fs::read_dir(d.as_path()) {
86 let repository_paths: ReadDir = repository_paths;
87
88 let repository_paths: Vec<DirEntry> = repository_paths
89 .filter_map(Result::ok)
90 .filter(|s| s.file_type().unwrap().is_dir())
91 .collect();
92
93 for repository_path in repository_paths {
94 if let Some(expansion_repository) = Repository::from_existing_expansion(
95 platform.clone(),
96 repository_path.path().to_str().unwrap(),
97 ) {
98 self.repositories.push(expansion_repository);
99 }
100 }
101 }
102
103 self.repositories.sort();
104 }
105
106 fn get_dat_file(&self, path: &str, chunk: u8, data_file_id: u32) -> Option<SqPackData> {
107 let (repository, category) = self.parse_repository_category(path).unwrap();
108
109 let dat_path: PathBuf = [
110 self.game_directory.clone(),
111 "sqpack".to_string(),
112 repository.name.clone(),
113 repository.dat_filename(chunk, category, data_file_id),
114 ]
115 .iter()
116 .collect();
117
118 SqPackData::from_existing(dat_path.to_str()?)
119 }
120
121 pub fn find_offset(&mut self, path: &str) -> Option<u64> {
123 let slice = self.find_entry(path);
124 slice.map(|(entry, _)| entry.offset)
125 }
126
127 fn parse_repository_category(&self, path: &str) -> Option<(&Repository, Category)> {
129 if self.repositories.is_empty() {
130 return None;
131 }
132
133 let tokens: Vec<&str> = path.split('/').collect();
134
135 let repository_token = tokens[1];
137 for repository in &self.repositories {
138 if repository.name == repository_token {
139 return Some((repository, string_to_category(tokens[0])?));
140 }
141 }
142
143 Some((&self.repositories[0], string_to_category(tokens[0])?))
145 }
146
147 fn get_index_filenames(&self, path: &str) -> Option<Vec<(String, u8)>> {
148 let (repository, category) = self.parse_repository_category(path)?;
149
150 let mut index_filenames = vec![];
151
152 for chunk in 0..255 {
153 let index_path: PathBuf = [
154 &self.game_directory,
155 "sqpack",
156 &repository.name,
157 &repository.index_filename(chunk, category),
158 ]
159 .iter()
160 .collect();
161
162 index_filenames.push((index_path.into_os_string().into_string().unwrap(), chunk));
163
164 let index2_path: PathBuf = [
165 &self.game_directory,
166 "sqpack",
167 &repository.name,
168 &repository.index2_filename(chunk, category),
169 ]
170 .iter()
171 .collect();
172
173 index_filenames.push((index2_path.into_os_string().into_string().unwrap(), chunk));
174 }
175
176 Some(index_filenames)
177 }
178
179 pub fn apply_patch(&self, patch_path: &str) -> Result<(), PatchError> {
181 ZiPatch::apply(&self.game_directory, patch_path)
182 }
183
184 pub fn needs_repair(&self) -> Option<Vec<(&Repository, RepairAction)>> {
188 let mut repositories: Vec<(&Repository, RepairAction)> = Vec::new();
189 for repository in &self.repositories {
190 if repository.version.is_none() {
191 let ver_bak_path: PathBuf = [
193 self.game_directory.clone(),
194 "sqpack".to_string(),
195 repository.name.clone(),
196 format!("{}.bck", repository.name),
197 ]
198 .iter()
199 .collect();
200
201 let repair_action = if read_version(&ver_bak_path).is_some() {
202 RepairAction::VersionFileCanRestore
203 } else {
204 RepairAction::VersionFileMissing
205 };
206
207 repositories.push((repository, repair_action));
208 }
209 }
210
211 if repositories.is_empty() {
212 None
213 } else {
214 Some(repositories)
215 }
216 }
217
218 pub fn perform_repair<'a>(
222 &self,
223 repositories: &Vec<(&'a Repository, RepairAction)>,
224 ) -> Result<(), RepairError<'a>> {
225 for (repository, action) in repositories {
226 let ver_path: PathBuf = [
227 self.game_directory.clone(),
228 "sqpack".to_string(),
229 repository.name.clone(),
230 format!("{}.ver", repository.name),
231 ]
232 .iter()
233 .collect();
234
235 let new_version: String = match action {
236 RepairAction::VersionFileMissing => {
237 let repo_path: PathBuf = [
238 self.game_directory.clone(),
239 "sqpack".to_string(),
240 repository.name.clone(),
241 ]
242 .iter()
243 .collect();
244
245 fs::remove_dir_all(&repo_path)
246 .ok()
247 .ok_or(RepairError::FailedRepair(repository))?;
248
249 fs::create_dir_all(&repo_path)
250 .ok()
251 .ok_or(RepairError::FailedRepair(repository))?;
252
253 "2012.01.01.0000.0000".to_string() }
255 RepairAction::VersionFileCanRestore => {
256 let ver_bak_path: PathBuf = [
257 self.game_directory.clone(),
258 "sqpack".to_string(),
259 repository.name.clone(),
260 format!("{}.bck", repository.name),
261 ]
262 .iter()
263 .collect();
264
265 read_version(&ver_bak_path).ok_or(RepairError::FailedRepair(repository))?
266 }
267 };
268
269 fs::write(ver_path, new_version)
270 .ok()
271 .ok_or(RepairError::FailedRepair(repository))?;
272 }
273
274 Ok(())
275 }
276
277 fn cache_index_file(&mut self, filename: &str) {
278 if !self.index_files.contains_key(filename) {
279 if let Some(index_file) = SqPackIndex::from_existing(filename) {
280 self.index_files.insert(filename.to_string(), index_file);
281 }
282 }
283 }
284
285 fn get_index_file(&self, filename: &str) -> Option<&SqPackIndex> {
286 self.index_files.get(filename)
287 }
288
289 fn find_entry(&mut self, path: &str) -> Option<(IndexEntry, u8)> {
290 let index_paths = self.get_index_filenames(path)?;
291
292 for (index_path, chunk) in index_paths {
293 self.cache_index_file(&index_path);
294
295 if let Some(index_file) = self.get_index_file(&index_path) {
296 if let Some(entry) = index_file.find_entry(path) {
297 return Some((entry, chunk));
298 }
299 }
300 }
301
302 None
303 }
304}
305
306impl Resource for SqPackResource {
307 fn read(&mut self, path: &str) -> Option<ByteBuffer> {
308 let slice = self.find_entry(path);
309 match slice {
310 Some((entry, chunk)) => {
311 let mut dat_file = self.get_dat_file(path, chunk, entry.data_file_id.into())?;
312
313 dat_file.read_from_offset(entry.offset)
314 }
315 None => None,
316 }
317 }
318
319 fn exists(&mut self, path: &str) -> bool {
320 let Some(_) = self.get_index_filenames(path) else {
321 return false;
322 };
323
324 self.find_entry(path).is_some()
325 }
326}
327
328fn is_valid(path: &str) -> bool {
329 let d = PathBuf::from(path);
330
331 if fs::metadata(d.as_path()).is_err() {
332 return false;
333 }
334
335 true
336}
337
338#[cfg(test)]
339mod tests {
340 use crate::repository::Category::*;
341
342 use super::*;
343
344 fn common_setup_data() -> SqPackResource {
345 let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
346 d.push("resources/tests");
347 d.push("valid_sqpack");
348 d.push("game");
349
350 SqPackResource::from_existing(Platform::Win32, d.to_str().unwrap())
351 }
352
353 #[test]
354 fn repository_ordering() {
355 let data = common_setup_data();
356
357 assert_eq!(data.repositories[0].name, "ffxiv");
358 assert_eq!(data.repositories[1].name, "ex1");
359 assert_eq!(data.repositories[2].name, "ex2");
360 }
361
362 #[test]
363 fn repository_and_category_parsing() {
364 let data = common_setup_data();
365
366 assert_eq!(
368 data.parse_repository_category("exd/root.exl").unwrap(),
369 (&data.repositories[0], EXD)
370 );
371 assert_eq!(
373 data.parse_repository_category("bg/ex1/01_roc_r2/twn/r2t1/level/planevent.lgb")
374 .unwrap(),
375 (&data.repositories[1], Background)
376 );
377 assert_eq!(
379 data.parse_repository_category("bg/ex2/01_gyr_g3/fld/g3fb/level/planner.lgb")
380 .unwrap(),
381 (&data.repositories[2], Background)
382 );
383 assert!(
385 data.parse_repository_category("what/some_font.dat")
386 .is_none()
387 );
388 }
389}