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