1 //! This file contains code to generate resource packs.
2 //!
3 //! A resource pack identifies the resources in a particular game version/mod.
4 //!
5 //! Required data lives directly in the structs, the rest is optional and goes in the properties.
6 //!
7 //! The properties are arbitrary on purpose. It is the only place where whoever manages the file can
8 //! place additional information like an author, an url, a revision, a license, any valid json really.
9 //!
10 //! Some properties have specific meanings.
11 //!
12 //!
13 //! # `with_archive_{format}` (bool, default = false)
14 //!
15 //! An archive is a file that holds a collection of files inside it.
16 //!
17 //! When this pack property is set to `true`, the resource pack includes the files inside that type of archive as resources.
18 //!
19 //! Supported formats:
20 //!  * `slf` - SLF files from the Jagged Alliance 2 series or mods
21 //!
22 //! Resource properties:
23 //!  * `archive_{format}` (bool) - `true` on resources that represent archives of the specified format
24 //!  * `archive_{format}_num_resources` (integer) - number of resources that were included from inside the archive
25 //!  * `archive_path` (string) - path of the archive resource that contains this file
26 //!
27 //! NOTE archives inside others archives are not supported, they are treated as regular files
28 //!
29 //!
30 //! # `with_file_size` (bool, default = false)
31 //!
32 //! Files have data.
33 //!
34 //! When this pack property is set to `true`, the resource properties include the size of the file data.
35 //!
36 //! Resource properties:
37 //!  * `file_size` (integer) - size of the file data.
38 //!
39 //!
40 //! # `with_hash_{algorithm}` (bool, default = false)
41 //!
42 //! File data can be hashed.
43 //! A hasher digests the data into a small fixed size and the same input produces the same output.
44 //!
45 //! When this pack property is true, the resource properties include the hash of the data in the specified algorithm.
46 //!
47 //! | Algorithms        | Notes |
48 //! |-------------------|-------|
49 //! | md5               | 128 bits, MD5, output of md5sum |
50 //!
51 //! Resource properties:
52 //!  * `hash_{algorithm}` (string) - hash of the specified algorithm
53 //!
54 
55 use std::collections::{HashSet, VecDeque};
56 use std::convert::From;
57 use std::error::Error;
58 use std::fmt;
59 use std::io;
60 use std::path::{Path, PathBuf};
61 
62 use digest::Digest;
63 use hex;
64 use md5::Md5;
65 use rayon::prelude::*;
66 use serde::{Deserialize, Serialize};
67 use serde_json::{json, Map, Value};
68 
69 use crate::file_formats::slf::{SlfEntryState, SlfHeader};
70 use crate::unicode::Nfc;
71 
72 /// A pack of game resources.
73 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
74 pub struct ResourcePack {
75     /// A friendly name of the resource pack for display purposes.
76     pub name: String,
77 
78     /// The properties of the resource pack.
79     pub properties: Map<String, Value>,
80 
81     /// The resources in this pack.
82     pub resources: Vec<Resource>,
83 }
84 
85 /// A resource in the pack.
86 ///
87 /// A resource always maps to raw data, which can be a file or data inside an archive.
88 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
89 pub struct Resource {
90     /// The identity of the resource as a relative path.
91     pub path: String,
92 
93     /// The properties of the resource.
94     pub properties: Map<String, Value>,
95 }
96 
97 #[derive(Clone, Debug, Default)]
98 pub struct ResourcePackBuilder {
99     /// Include archive contents as resources.
100     with_archives: Vec<Nfc>,
101 
102     /// Add file size to the resource properties.
103     with_file_size: bool,
104 
105     /// Add hashes of the data to the resource properties.
106     with_hashes: Vec<Nfc>,
107 
108     /// Add paths to the pack (base, path).
109     with_paths: VecDeque<(PathBuf, PathBuf)>,
110 
111     /// Resource being built.
112     pack: ResourcePack,
113 }
114 
115 impl ResourcePack {
116     /// Constructor.
117     #[allow(dead_code)]
new(name: &str) -> Self118     fn new(name: &str) -> Self {
119         let mut pack = ResourcePack::default();
120         pack.name = name.to_owned();
121         pack
122     }
123 
124     /// Get properties that are enabled (true)
125     #[allow(dead_code)]
get_enabled_properties(&self) -> impl Iterator<Item = &String>126     fn get_enabled_properties(&self) -> impl Iterator<Item = &String> {
127         self.properties
128             .iter()
129             .filter(|(_, v)| v.as_bool() == Some(true))
130             .map(|(k, _)| k)
131     }
132 
133     /// Returns wether the resource pack has been built with file sizes
134     #[allow(dead_code)]
has_file_size(&self) -> bool135     pub fn has_file_size(&self) -> bool {
136         self.get_enabled_properties()
137             .any(|k| k.as_str() == "with_file_size")
138     }
139 
140     /// Returns the hashes that are used in the resource pack
141     #[allow(dead_code)]
get_hashes(&self) -> HashSet<String>142     pub fn get_hashes(&self) -> HashSet<String> {
143         self.get_enabled_properties()
144             .filter(|k| k.starts_with("with_hash_"))
145             .map(|k| k["with_hash_".len()..].to_owned())
146             .collect()
147     }
148 
149     /// Returns which archives were inspected when building the resource pack
150     #[allow(dead_code)]
get_archives(&self) -> HashSet<String>151     pub fn get_archives(&self) -> HashSet<String> {
152         self.get_enabled_properties()
153             .filter(|k| k.starts_with("with_archive_"))
154             .map(|k| k["with_archive_".len()..].to_owned())
155             .collect()
156     }
157 }
158 
159 impl Resource {
160     // Constructor.
161     #[allow(dead_code)]
new(path: &str) -> Self162     fn new(path: &str) -> Self {
163         let mut resource = Resource::default();
164         resource.path = path.to_owned();
165         resource
166     }
167 }
168 
169 impl ResourcePackBuilder {
170     // Constructor.
171     #[allow(dead_code)]
new() -> Self172     pub fn new() -> Self {
173         Self::default()
174     }
175 
176     /// Adds archive contents.
177     #[allow(dead_code)]
with_archive(&mut self, extension: &str) -> &mut Self178     pub fn with_archive(&mut self, extension: &str) -> &mut Self {
179         self.with_archives.push(extension.into());
180         self
181     }
182 
183     /// Adds file sizes.
184     #[allow(dead_code)]
with_file_size(&mut self) -> &mut Self185     pub fn with_file_size(&mut self) -> &mut Self {
186         self.with_file_size = true;
187         self
188     }
189 
190     /// Adds a hash algorithm.
191     #[allow(dead_code)]
with_hash(&mut self, algorithm: &str) -> &mut Self192     pub fn with_hash(&mut self, algorithm: &str) -> &mut Self {
193         self.with_hashes.push(algorithm.into());
194         self
195     }
196 
197     /// Adds a directory or an archive.
198     #[allow(dead_code)]
with_path(&mut self, base: &Path, path: &Path) -> &mut Self199     pub fn with_path(&mut self, base: &Path, path: &Path) -> &mut Self {
200         for (b, p) in &self.with_paths {
201             if b == base && p == path {
202                 return self; // avoid duplicates
203             }
204         }
205         let tuple = (base.to_owned(), path.to_owned());
206         self.with_paths.push_back(tuple);
207         self
208     }
209 
210     /// Returns a resource pack or an error.
211     #[allow(dead_code)]
execute(&mut self, name: &str) -> Result<ResourcePack, ResourceError>212     pub fn execute(&mut self, name: &str) -> Result<ResourcePack, ResourceError> {
213         self.pack = ResourcePack::new(name);
214         self.with_archives.sort();
215         self.with_archives.dedup();
216         for extension in &self.with_archives {
217             match extension.as_str() {
218                 "slf" => {}
219                 _ => {
220                     return Err(format!("{:?} archives are not supported", extension).into());
221                 }
222             }
223             let prop = "with_archive_".to_owned() + extension;
224             self.pack.set_property(&prop, true);
225         }
226         if self.with_file_size {
227             self.pack.set_property("with_file_size", true);
228         }
229         self.with_hashes.sort();
230         self.with_hashes.dedup();
231         for algorithm in &self.with_hashes {
232             match algorithm.as_str() {
233                 "md5" => {}
234                 _ => return Err(format!("{:?} hashes are not supported", algorithm).into()),
235             }
236             let prop = "with_hash_".to_owned() + algorithm;
237             self.pack.set_property(&prop, true);
238         }
239 
240         let resources: Result<Vec<Vec<Resource>>, ResourceError> = self
241             .with_paths
242             .par_iter()
243             .map(|paths| {
244                 let (base, path) = paths;
245                 let metadata = path.metadata()?;
246                 if metadata.is_file() {
247                     self.get_resources_for_file(&base, &path)
248                 } else if metadata.is_dir() {
249                     self.get_resources_for_dir(&base, &path)
250                 } else {
251                     Ok(vec![])
252                 }
253             })
254             .collect();
255         let mut resources: Vec<_> = resources?.into_iter().flatten().collect();
256         self.pack.resources.append(&mut resources);
257 
258         let pack = self.pack.to_owned();
259         self.pack = ResourcePack::default();
260         Ok(pack)
261     }
262 
263     /// Adds the contents of an OS directory.
get_resources_for_dir( &self, base: &Path, dir: &Path, ) -> Result<Vec<Resource>, ResourceError>264     fn get_resources_for_dir(
265         &self,
266         base: &Path,
267         dir: &Path,
268     ) -> Result<Vec<Resource>, ResourceError> {
269         let files: Vec<_> = FSIterator::new(dir).collect();
270         let resources: Result<Vec<Vec<Resource>>, _> = files
271             .par_iter()
272             .map(|file| self.get_resources_for_file(base, &file))
273             .collect();
274         Ok(resources?.into_iter().flatten().collect())
275     }
276 
277     /// Adds an OS file as a resource.
get_resources_for_file( &self, base: &Path, path: &Path, ) -> Result<Vec<Resource>, ResourceError>278     fn get_resources_for_file(
279         &self,
280         base: &Path,
281         path: &Path,
282     ) -> Result<Vec<Resource>, ResourceError> {
283         // must have a valid resource path
284         let mut resources = vec![];
285         let resource_path = resource_path(base, path)?;
286         if resource_path.to_ascii_lowercase().starts_with("temp/") {
287             // the game can create/modify/remove files in the temp folder, ignore it
288             return Ok(resources);
289         }
290         let mut resource = Resource::new(&resource_path);
291         if self.with_file_size {
292             resource.set_property("file_size", path.metadata()?.len());
293         }
294         let extension = lowercase_extension(path);
295         let wants_archive = self.with_archives.binary_search(&extension).is_ok();
296         let wants_hashes = !self.with_hashes.is_empty();
297         if wants_archive || wants_hashes {
298             let data = std::fs::read(path)?;
299             if wants_archive {
300                 match extension.as_str() {
301                     "slf" => {
302                         let mut slf_resources = self.get_resources_for_slf(&mut resource, &data)?;
303                         resources.append(&mut slf_resources);
304                     }
305                     _ => panic!(), // execute() must be fixed
306                 }
307             }
308             if wants_hashes {
309                 self.add_hashes(&mut resource, &data)?;
310             }
311         }
312         resources.push(resource);
313         Ok(resources)
314     }
315 
316     // Adds the contents of a SLF archive.
get_resources_for_slf( &self, slf: &mut Resource, data: &[u8], ) -> Result<Vec<Resource>, ResourceError>317     fn get_resources_for_slf(
318         &self,
319         slf: &mut Resource,
320         data: &[u8],
321     ) -> Result<Vec<Resource>, ResourceError> {
322         slf.set_property("archive_slf", true);
323         let mut input = io::Cursor::new(&data);
324         let header = SlfHeader::from_input(&mut input)?;
325         let entries = header.entries_from_input(&mut input)?;
326         let resources: Result<Vec<Resource>, ResourceError> = entries
327             .par_iter()
328             .filter(|entry| entry.state == SlfEntryState::Ok)
329             .map(|entry| {
330                 let mut resource = Resource::default();
331                 let path = header.library_path.clone() + &entry.file_path;
332                 resource.path = path.replace("\\", "/");
333                 resource.set_property("archive_path", &slf.path);
334                 // record file size
335                 if self.with_file_size {
336                     resource.set_property("file_size", entry.length);
337                 }
338                 let wants_hashes = !self.with_hashes.is_empty();
339                 if wants_hashes {
340                     let from = entry.offset as usize;
341                     let to = from + entry.length as usize;
342                     self.add_hashes(&mut resource, &data[from..to])?;
343                 }
344                 Ok(resource)
345             })
346             .collect();
347         let resources = resources?;
348         slf.set_property("archive_slf_num_resources", resources.len());
349         Ok(resources)
350     }
351 
352     /// Adds hashes of the resource data.
353     fn add_hashes(&self, resource: &mut Resource, data: &[u8]) -> Result<(), ResourceError> {
354         for algorithm in &self.with_hashes {
355             let hash = match algorithm.as_str() {
356                 "md5" => hex::encode(Md5::digest(&data)),
357                 _ => panic!(), // execute() must be fixed
358             };
359             let prop = "hash_".to_owned() + algorithm;
360             resource.set_property(&prop, hash);
361         }
362         Ok(())
363     }
364 }
365 
366 /// Error that originates from this module.
367 #[derive(Debug)]
368 pub enum ResourceError {
369     /// Simple text error.
370     Text(String),
371     /// Original error was an IO error.
372     IoError(io::Error),
373 }
374 
375 impl Error for ResourceError {
376     fn description(&self) -> &str {
377         match self {
378             ResourceError::Text(desc) => desc,
379             ResourceError::IoError(err) => err.description(),
380         }
381     }
382 }
383 
384 impl fmt::Display for ResourceError {
385     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
386         write!(f, "ResourceError({})", self.description())
387     }
388 }
389 
390 impl From<String> for ResourceError {
391     fn from(desc: String) -> ResourceError {
392         ResourceError::Text(desc)
393     }
394 }
395 
396 impl From<io::Error> for ResourceError {
397     fn from(err: io::Error) -> ResourceError {
398         ResourceError::IoError(err)
399     }
400 }
401 
402 /// Iterator that returns the paths of all the filesystem files in breadth-first order, files before dirs.
403 #[derive(Debug, Default)]
404 struct FSIterator {
405     files: VecDeque<PathBuf>,
406     dirs: VecDeque<PathBuf>,
407 }
408 
409 impl FSIterator {
410     fn new(path: &Path) -> Self {
411         let mut all_files = Self::default();
412         all_files.with(path.to_owned());
413         all_files
414     }
415 
416     /// Adds a path to the iterator.
417     fn with(&mut self, path: PathBuf) {
418         if path.is_file() {
419             self.files.push_back(path);
420         } else if path.is_dir() {
421             self.dirs.push_back(path);
422         }
423     }
424 }
425 
426 impl Iterator for FSIterator {
427     type Item = PathBuf;
428 
429     /// Get the next file path while ignoring errors.
430     fn next(&mut self) -> Option<Self::Item> {
431         loop {
432             if let Some(file) = self.files.pop_front() {
433                 return Some(file);
434             } else if let Some(dir) = self.dirs.pop_front() {
435                 // TODO detect and skip circles
436                 if let Ok(iter) = dir.read_dir() {
437                     for entry in iter {
438                         if let Ok(path) = entry.map(|entry| entry.path()) {
439                             self.with(path);
440                         }
441                     }
442                 }
443             } else {
444                 // we're done
445                 return None;
446             }
447         }
448     }
449 }
450 
451 /// Returns the lowercase extension.
452 fn lowercase_extension(path: &Path) -> Nfc {
453     path.extension()
454         .and_then(|x| x.to_str())
455         .map(|x| x.to_lowercase())
456         .unwrap_or_default()
457         .into()
458 }
459 
460 /// Converts an OS path to a resource path.
461 fn resource_path(base: &Path, path: &Path) -> Result<Nfc, ResourceError> {
462     if let Ok(resource_path) = path.strip_prefix(base) {
463         return match resource_path.to_str() {
464             Some(utf8) => Ok(Nfc::path(utf8)),
465             None => Err(format!("{:?} contains invalid utf8", resource_path).into()),
466         };
467     }
468     Err(format!("{:?} is not a prefix of {:?}", base, path).into())
469 }
470 
471 /// Trait the adds shortcuts for properties.
472 pub trait ResourcePropertiesExt {
473     /// Gets a reference to the properties container.
474     fn properties(&self) -> &Map<String, Value>;
475 
476     /// Gets a mutable reference to the properties container.
477     fn properties_mut(&mut self) -> &mut Map<String, Value>;
478 
479     /// Removes a property and returns the old value.
480     fn remove_property(&mut self, name: &str) -> Option<Value> {
481         self.properties_mut().remove(name)
482     }
483 
484     /// Sets the value of a property and returns the old value.
485     fn set_property<T: Serialize>(&mut self, name: &str, value: T) -> Option<Value> {
486         self.properties_mut().insert(name.to_owned(), json!(value))
487     }
488 
489     /// Gets the value of a property.
490     fn get_property(&self, name: &str) -> Option<&Value> {
491         self.properties().get(name)
492     }
493 
494     /// Gets the value of a bool property.
495     fn get_bool(&self, name: &str) -> Option<bool> {
496         self.get_property(name).and_then(|v| v.as_bool())
497     }
498 
499     /// Gets the value of a string property.
500     fn get_str(&self, name: &str) -> Option<&str> {
501         self.get_property(name).and_then(|v| v.as_str())
502     }
503 
504     /// Gets the value of an array of strings property.
505     fn get_vec_of_str(&self, name: &str) -> Option<Vec<&str>> {
506         if let Some(array) = self.get_property(name).and_then(|v| v.as_array()) {
507             let mut strings = Vec::new();
508             for value in array {
509                 if !value.is_string() {
510                     return None;
511                 }
512                 strings.push(value.as_str().unwrap());
513             }
514             return Some(strings);
515         }
516         None
517     }
518 
519     /// Gets the signed integer value of a number property.
520     fn get_i64(&self, name: &str) -> Option<i64> {
521         self.get_property(name).and_then(|v| v.as_i64())
522     }
523 
524     /// Gets the unsigned integer value of a number property.
525     fn get_u64(&self, name: &str) -> Option<u64> {
526         self.get_property(name).and_then(|v| v.as_u64())
527     }
528 
529     /// Gets the floating-point value of a number property.
530     fn get_f64(&self, name: &str) -> Option<f64> {
531         self.get_property(name).and_then(|v| v.as_f64())
532     }
533 }
534 
535 impl ResourcePropertiesExt for ResourcePack {
536     fn properties(&self) -> &Map<String, Value> {
537         &self.properties
538     }
539 
540     fn properties_mut(&mut self) -> &mut Map<String, Value> {
541         &mut self.properties
542     }
543 }
544 
545 impl ResourcePropertiesExt for Resource {
546     fn properties(&self) -> &Map<String, Value> {
547         &self.properties
548     }
549 
550     fn properties_mut(&mut self) -> &mut Map<String, Value> {
551         &mut self.properties
552     }
553 }
554 
555 #[cfg(test)]
556 mod tests {
557     use crate::res::{Resource, ResourcePropertiesExt};
558 
559     #[test]
560     fn property_value_compatibility_boolean() {
561         let mut resource = Resource::default();
562         resource.set_property("p", true);
563         assert!(resource.get_bool("p").is_some());
564         assert!(resource.get_str("p").is_none());
565         assert!(resource.get_i64("p").is_none());
566         assert!(resource.get_u64("p").is_none());
567         assert!(resource.get_f64("p").is_none());
568         assert_eq!(resource.get_bool("p").unwrap(), true);
569     }
570 
571     #[test]
572     fn property_value_compatibility_string() {
573         let mut resource = Resource::default();
574         resource.set_property("p", "foo");
575         assert!(resource.get_bool("p").is_none());
576         assert!(resource.get_str("p").is_some());
577         assert!(resource.get_i64("p").is_none());
578         assert!(resource.get_u64("p").is_none());
579         assert!(resource.get_f64("p").is_none());
580         assert_eq!(resource.get_str("p").unwrap(), "foo");
581     }
582 
583     #[test]
584     fn property_value_compatibility_universal_number() {
585         let mut resource = Resource::default();
586         resource.set_property("p", 0);
587         assert!(resource.get_bool("p").is_none());
588         assert!(resource.get_str("p").is_none());
589         assert!(resource.get_i64("p").is_some());
590         assert!(resource.get_u64("p").is_some());
591         assert!(resource.get_f64("p").is_some());
592         assert_eq!(resource.get_i64("p").unwrap(), 0);
593     }
594 
595     #[test]
596     #[allow(clippy::float_cmp)]
597     fn property_value_compatibility_floating_point_number() {
598         let mut resource = Resource::default();
599         resource.set_property("p", 0.5);
600         assert!(resource.get_bool("p").is_none());
601         assert!(resource.get_str("p").is_none());
602         assert!(resource.get_i64("p").is_none());
603         assert!(resource.get_u64("p").is_none());
604         assert!(resource.get_f64("p").is_some());
605         assert_eq!(resource.get_f64("p").unwrap(), 0.5);
606     }
607 
608     #[test]
609     fn property_value_compatibility_negative_number() {
610         let mut resource = Resource::default();
611         resource.set_property("p", -1);
612         assert!(resource.get_bool("p").is_none());
613         assert!(resource.get_str("p").is_none());
614         assert!(resource.get_i64("p").is_some());
615         assert!(resource.get_u64("p").is_none());
616         assert!(resource.get_f64("p").is_some());
617         assert_eq!(resource.get_i64("p").unwrap(), -1);
618     }
619 
620     #[test]
621     fn property_value_compatibility_big_number() {
622         let mut resource = Resource::default();
623         resource.set_property("p", u64::max_value());
624         assert!(resource.get_bool("p").is_none());
625         assert!(resource.get_str("p").is_none());
626         assert!(resource.get_i64("p").is_none());
627         assert!(resource.get_u64("p").is_some());
628         assert!(resource.get_f64("p").is_some());
629         assert_eq!(resource.get_u64("p").unwrap(), u64::max_value());
630     }
631 
632     // Since #[bench] isn't stable, these simple timed tests are ignored and print the times by themselves.
633     // Execute them with `cargo test -- --nocapture --ignored`
634     mod timed {
635         use std::time::{Duration, SystemTime};
636 
637         use digest::Digest;
638         use hex;
639         use md5::Md5;
640 
641         fn data_for_hasher() -> Vec<u8> {
642             "A quick brown fox jumps over the lazy dog"
643                 .repeat(1_000_000)
644                 .as_bytes()
645                 .to_vec()
646         }
647 
648         #[allow(clippy::cast_lossless)]
649         fn print_hasher_result(name: &str, time: Duration, size: usize, hash: &[u8]) {
650             let secs = time.as_secs() as f64 + time.subsec_nanos() as f64 / 1_000_000_000f64;
651             let mib = size as f64 / 1_000_000f64;
652             let speed = mib / secs;
653             let hash = hex::encode(hash);
654             println!(
655                 "{}: {} bytes in {:?} {:.3}MiB/s {:?}",
656                 name, size, time, speed, hash
657             );
658         }
659 
660         #[test]
661         #[ignore]
662         fn hasher_md5() {
663             let data = data_for_hasher();
664             let start = SystemTime::now();
665             let hash = Md5::digest(&data);
666             let elapsed = start.elapsed().unwrap();
667             print_hasher_result("hasher_md5", elapsed, data.len(), &hash);
668         }
669     }
670 }
671