From 389c3fe00b5ce563fea6c7b435b4744979be8d0e Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Tue, 19 Jul 2022 19:29:41 -0400 Subject: [PATCH] Add initial files --- .gitignore | 14 + Cargo.lock | 733 ++++++++++++++++++ Cargo.toml | 26 + LICENSE | 674 ++++++++++++++++ README.md | 31 + benches/physis_benchmark.rs | 32 + deny.toml | 28 + .../invalid_sqpack/game/sqpack/invalid.txt | 0 resources/tests/test.exl | 3 + .../tests/valid_sqpack/game/ffxivgame.ver | 1 + .../valid_sqpack/game/sqpack/ex1/ex1.ver | 1 + .../game/sqpack/ex1/test.win32.dat0 | 0 .../valid_sqpack/game/sqpack/ex2/ex2.ver | 1 + .../game/sqpack/ex2/test.win32.dat0 | 0 .../game/sqpack/ffxiv/0c0100.win32.dat0 | 0 src/blowfish.rs | 145 ++++ src/blowfish_constants.rs | 188 +++++ src/bootdata.rs | 43 + src/common.rs | 24 + src/compression.rs | 102 +++ src/dat.rs | 333 ++++++++ src/equipment.rs | 61 ++ src/exl.rs | 86 ++ src/gamedata.rs | 212 +++++ src/index.rs | 82 ++ src/lib.rs | 34 + src/macros.rs | 33 + src/model.rs | 33 + src/patch.rs | 444 +++++++++++ src/race.rs | 99 +++ src/repository.rs | 194 +++++ src/sqpack.rs | 78 ++ tests/integration_test.rs | 19 + 33 files changed, 3754 insertions(+) create mode 100644 .gitignore create mode 100755 Cargo.lock create mode 100755 Cargo.toml create mode 100644 LICENSE create mode 100755 README.md create mode 100755 benches/physis_benchmark.rs create mode 100755 deny.toml create mode 100755 resources/tests/invalid_sqpack/game/sqpack/invalid.txt create mode 100755 resources/tests/test.exl create mode 100755 resources/tests/valid_sqpack/game/ffxivgame.ver create mode 100755 resources/tests/valid_sqpack/game/sqpack/ex1/ex1.ver create mode 100755 resources/tests/valid_sqpack/game/sqpack/ex1/test.win32.dat0 create mode 100755 resources/tests/valid_sqpack/game/sqpack/ex2/ex2.ver create mode 100755 resources/tests/valid_sqpack/game/sqpack/ex2/test.win32.dat0 create mode 100755 resources/tests/valid_sqpack/game/sqpack/ffxiv/0c0100.win32.dat0 create mode 100755 src/blowfish.rs create mode 100755 src/blowfish_constants.rs create mode 100755 src/bootdata.rs create mode 100755 src/common.rs create mode 100755 src/compression.rs create mode 100755 src/dat.rs create mode 100755 src/equipment.rs create mode 100755 src/exl.rs create mode 100755 src/gamedata.rs create mode 100755 src/index.rs create mode 100755 src/lib.rs create mode 100755 src/macros.rs create mode 100755 src/model.rs create mode 100755 src/patch.rs create mode 100755 src/race.rs create mode 100755 src/repository.rs create mode 100755 src/sqpack.rs create mode 100755 tests/integration_test.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b90c22e --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +.idea/ +.DS_Store +.directory \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100755 index 0000000..d407d22 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,733 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "array-init" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb6d71005dc22a708c7496eee5c8dc0300ee47355de6256c3b35b12b5fef596" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "binrw" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4abb4fd60add897b9e8827e0d5fa6c2ca129ece2432d9aa13454b21ac2ecc18f" +dependencies = [ + "array-init", + "binrw_derive", +] + +[[package]] +name = "binrw_derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a36ff195a3a1a82d5eeb98e0069bdf3ea076042d28591396d9020fac763bf66f" +dependencies = [ + "owo-colors", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitfield-struct" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0baacb7d5b6e94369201d5807e086d3f36e2bd35057ca2c5a680af3737e35fa" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + +[[package]] +name = "cast" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "ciborium" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369" + +[[package]] +name = "ciborium-ll" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "3.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2bd7a1eb07da9ac757c923f69373deb7bc2ba5efc951b873bcb5e693992dca" +dependencies = [ + "bitflags", + "clap_lex", + "indexmap", + "textwrap", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "crc" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" + +[[package]] +name = "criterion" +version = "0.3.5" +source = "git+https://github.com/bheisler/criterion.rs?branch=version-0.4#2934163518a75bf037c7467c0a1d28443203eabf" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap", + "criterion-plot", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tabwriter", + "termcolor", + "tinytemplate", + "unicode-width", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.4.4" +source = "git+https://github.com/bheisler/criterion.rs?branch=version-0.4#2934163518a75bf037c7467c0a1d28443203eabf" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "once_cell", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "either" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" + +[[package]] +name = "js-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "libz-sys" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "os_str_bytes" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" + +[[package]] +name = "owo-colors" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b" + +[[package]] +name = "paste" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" + +[[package]] +name = "physis" +version = "0.1.0" +dependencies = [ + "binrw", + "bitfield-struct", + "crc", + "criterion", + "libz-sys", + "paste", +] + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "plotters" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9428003b84df1496fb9d6eeee9c5f8145cb41ca375eb0dad204328888832811f" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" + +[[package]] +name = "plotters-svg" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0918736323d1baff32ee0eade54984f6f201ad7e97d5cfb5d6ab4a358529615" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "proc-macro2" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" + +[[package]] +name = "serde" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tabwriter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36205cfc997faadcc4b0b87aaef3fbedafe20d38d4959a7ca6ff803564051111" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" + +[[package]] +name = "web-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100755 index 0000000..debd2d9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "physis" +version = "0.1.0" +authors = ["Joshua Goins "] +edition = "2021" +description = "Interact with XIV game data." +license = "GPL-3.0" +homepage = "https://xiv.zone/physis" +repository = "https://git.sr.ht/~redstrate/physis" +keywords = ["ffxiv", "modding"] +categories = ["parsing"] + +[[bench]] +name = "physis_benchmark" +harness = false + +[dev-dependencies] +# criterion is not updated to 0.4 yet, which removes an unmaintained package +criterion = { git = "https://github.com/bheisler/criterion.rs", branch="version-0.4", version="0.3.5" } + +[dependencies] +crc = "3.0.0" +binrw = "0.9.2" +libz-sys = { version = "1.1.8", default-features = false } +bitfield-struct = "0.1.7" +paste = "1.0.7" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100755 index 0000000..7db9863 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Physis + +Framework for interacting with FFXIV data, and successor to [libxiv](https://git.sr.ht/~redstrate/libxiv). This intended for +developers writing modding tools, launchers and other programs. + +**Note:** This library is still experimental, and no releases are planned. I'm currently busy with bringing all of libxiv's +features over. + +## Goals +* Make it extremely easy for people to tinker around with game data. +* Parsing data should be safe, and unit tested vigorously. +* Minimal dependencies ;-) All dependencies are also checked by `cargo deny`. + +## Features + +* Apply game patches, enabling custom launchers to patch the game. +* Blockfish ciphers for encrypting and decrypting SqexArg. +* Parse various game formats: + * INDEX + * DAT + * ZiPatch + +## Development + +If you're interested to see how these formats work in more detail, see [xiv.dev](https://xiv.dev/) and [docs.xiv.zone](https://docs.xiv.zone)! + +Some tests and benchmarks require the environment variable `FFXIV_GAME_DIR` to be set. + +## Contributing + +The best way you can help is by [monetarily supporting me](https://redstrate.com/about/) or by submitting patches to help fix bugs or add functionality! diff --git a/benches/physis_benchmark.rs b/benches/physis_benchmark.rs new file mode 100755 index 0000000..4aa6e99 --- /dev/null +++ b/benches/physis_benchmark.rs @@ -0,0 +1,32 @@ +use std::env; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use physis::sqpack::calculate_hash; + +fn reload_repos() { + let game_dir = env::var("FFXIV_GAME_DIR").unwrap(); + let mut gamedata = physis::gamedata::GameData::from_existing(format!("{}/game", game_dir).as_str()).unwrap(); + + gamedata.reload_repositories(); +} + +fn bench_calculate_hash() { + calculate_hash("exd/root.exl"); +} + +fn fetch_data() { + let game_dir = env::var("FFXIV_GAME_DIR").unwrap(); + let mut gamedata = physis::gamedata::GameData::from_existing(format!("{}/game", game_dir).as_str()).unwrap(); + + gamedata.reload_repositories(); + + gamedata.extract("exd/root.exl"); +} + +fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("hash calc", |b| b.iter(|| bench_calculate_hash())); + c.bench_function("gamedata reloading repositories", |b| b.iter(|| reload_repos())); + c.bench_function("gamedata extract", |b| b.iter(|| fetch_data())); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); \ No newline at end of file diff --git a/deny.toml b/deny.toml new file mode 100755 index 0000000..a949e5f --- /dev/null +++ b/deny.toml @@ -0,0 +1,28 @@ +[advisories] +db-path = "~/.cargo/advisory-db" +db-urls = ["https://github.com/rustsec/advisory-db"] +vulnerability = "deny" +unmaintained = "deny" +yanked = "deny" +notice = "warn" + +[licenses] +unlicensed = "deny" +allow = [ + "Unicode-DFS-2016" # used for unicode-ident +] +copyleft = "allow" +allow-osi-fsf-free = "both" +default = "deny" +confidence-threshold = 0.8 + +[bans] +multiple-versions = "deny" +wildcards = "warn" +highlight = "all" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = ["https://github.com/bheisler/criterion.rs"] \ No newline at end of file diff --git a/resources/tests/invalid_sqpack/game/sqpack/invalid.txt b/resources/tests/invalid_sqpack/game/sqpack/invalid.txt new file mode 100755 index 0000000..e69de29 diff --git a/resources/tests/test.exl b/resources/tests/test.exl new file mode 100755 index 0000000..41370b4 --- /dev/null +++ b/resources/tests/test.exl @@ -0,0 +1,3 @@ +EXLT,2 +Foo,4 +Bar,-1 \ No newline at end of file diff --git a/resources/tests/valid_sqpack/game/ffxivgame.ver b/resources/tests/valid_sqpack/game/ffxivgame.ver new file mode 100755 index 0000000..7f20734 --- /dev/null +++ b/resources/tests/valid_sqpack/game/ffxivgame.ver @@ -0,0 +1 @@ +1.0.1 \ No newline at end of file diff --git a/resources/tests/valid_sqpack/game/sqpack/ex1/ex1.ver b/resources/tests/valid_sqpack/game/sqpack/ex1/ex1.ver new file mode 100755 index 0000000..e6d5cb8 --- /dev/null +++ b/resources/tests/valid_sqpack/game/sqpack/ex1/ex1.ver @@ -0,0 +1 @@ +1.0.2 \ No newline at end of file diff --git a/resources/tests/valid_sqpack/game/sqpack/ex1/test.win32.dat0 b/resources/tests/valid_sqpack/game/sqpack/ex1/test.win32.dat0 new file mode 100755 index 0000000..e69de29 diff --git a/resources/tests/valid_sqpack/game/sqpack/ex2/ex2.ver b/resources/tests/valid_sqpack/game/sqpack/ex2/ex2.ver new file mode 100755 index 0000000..e4c0d46 --- /dev/null +++ b/resources/tests/valid_sqpack/game/sqpack/ex2/ex2.ver @@ -0,0 +1 @@ +1.0.3 \ No newline at end of file diff --git a/resources/tests/valid_sqpack/game/sqpack/ex2/test.win32.dat0 b/resources/tests/valid_sqpack/game/sqpack/ex2/test.win32.dat0 new file mode 100755 index 0000000..e69de29 diff --git a/resources/tests/valid_sqpack/game/sqpack/ffxiv/0c0100.win32.dat0 b/resources/tests/valid_sqpack/game/sqpack/ffxiv/0c0100.win32.dat0 new file mode 100755 index 0000000..e69de29 diff --git a/src/blowfish.rs b/src/blowfish.rs new file mode 100755 index 0000000..d629df4 --- /dev/null +++ b/src/blowfish.rs @@ -0,0 +1,145 @@ +use std::io::{Cursor, Write}; +use crate::blowfish_constants::{BLOWFISH_P, BLOWFISH_S}; + +const ROUNDS: usize = 16; +const KEYBITS: u32 = 64u32 >> 3; + +/// Implementation of the Blowfish block cipher, specialized for encrypting and decrypting SqexArg. +pub struct Blowfish { + p: [u32; 18], + s: [[u32; 256]; 4], +} + +impl Blowfish { + pub fn new(key: &[u8]) -> Blowfish { + let mut s = Self { + p: BLOWFISH_P, + s: BLOWFISH_S, + }; + + let mut j = 0usize; + for i in 0..ROUNDS + 2 { + let mut data = 0u32; + for _ in 0..4 { + data = (data << 8) | (key[j as usize] as u32); + j += 1; + + if j >= (KEYBITS as usize) { + j = 0; + } + } + + s.p[i] ^= data; + } + + let mut l = 0u32; + let mut r = 0u32; + + for i in (0..18).step_by(2) { + let (l_new, r_new) = s.encrypt_pair(l, r); + s.p[i] = l_new; + s.p[i + 1] = r_new; + + l = l_new; + r = r_new; + } + + for i in 0..4 { + for j in (0..256).step_by(2) { + let (l_new, r_new) = s.encrypt_pair(l, r); + s.s[i][j] = l_new; + s.s[i][j + 1] = r_new; + + l = l_new; + r = r_new; + } + } + + s + } + + /// Encrypts a block of data. If the encryption for any reason fails, returns None. + pub fn encrypt(&self, data: &[u8]) -> Option> { + let padded_data = Blowfish::pad_buffer(data); + + let mut cursor = Cursor::new(Vec::with_capacity(padded_data.len())); + + for i in (0..padded_data.len()).step_by(8) { + let l_bytes: [u8; 4] = padded_data[i..i + 4].try_into().ok()?; + let r_bytes: [u8; 4] = padded_data[i + 4..i + 8].try_into().ok()?; + + let (l, r) = self.encrypt_pair(u32::from_le_bytes(l_bytes), u32::from_le_bytes(r_bytes)); + + cursor.write(u32::to_le_bytes(l).as_slice()).ok()?; + cursor.write(u32::to_le_bytes(r).as_slice()).ok()?; + } + + Some(cursor.into_inner()) + } + + fn pad_buffer(data: &[u8]) -> Vec { + let mut padded_length = data.len(); + if data.len() % 8 != 0 { + padded_length = data.len() + (8 - (data.len() % 8)); + } + + let mut vec = Vec::with_capacity(padded_length); + vec.resize(padded_length, 0); + vec[..data.len()].clone_from_slice(data); + + vec + } + + /// Decrypts a block of data. If the decryption fails due to buffer overflow issues, will return None - but it + /// does not indicate that the wrong key was used. + pub fn decrypt(&self, data: &[u8]) -> Option> { + let padded_data = Blowfish::pad_buffer(data); + + let mut buffer = Vec::with_capacity(padded_data.len()); + let mut cursor = Cursor::new(&mut buffer); + + for i in (0..padded_data.len()).step_by(8) { + let l_bytes: [u8; 4] = padded_data[i..i + 4].try_into().ok()?; + let r_bytes: [u8; 4] = padded_data[i + 4..i + 8].try_into().ok()?; + + let (l, r) = self.decrypt_pair(u32::from_le_bytes(l_bytes), u32::from_le_bytes(r_bytes)); + + cursor.write(u32::to_le_bytes(l).as_slice()).ok()?; + cursor.write(u32::to_le_bytes(r).as_slice()).ok()?; + } + + Some(buffer) + } + + /// Calculates the F-function for `x`. + fn f(&self, x: u32) -> u32 { + let a = self.s[0][(x >> 24) as usize]; + let b = self.s[1][((x >> 16) & 0xFF) as usize]; + let c = self.s[2][((x >> 8) & 0xFF) as usize]; + let d = self.s[3][(x & 0xFF) as usize]; + + (a.wrapping_add(b) ^ c).wrapping_add(d) + } + + fn encrypt_pair(&self, mut l: u32, mut r: u32) -> (u32, u32) { + for i in (0..ROUNDS).step_by(2) { + l ^= self.p[i]; + r ^= self.f(l); + r ^= self.p[i + 1]; + l ^= self.f(r); + } + + return (r ^ self.p[17], l ^ self.p[16]); + } + + fn decrypt_pair(&self, mut l: u32, mut r: u32) -> (u32, u32) { + for i in (2..ROUNDS + 1).step_by(2).rev() { + l ^= self.p[i + 1]; + r ^= self.f(l); + r ^= self.p[i]; + l ^= self.f(r); + } + + return (r ^ self.p[0], l ^ self.p[1]); + } +} \ No newline at end of file diff --git a/src/blowfish_constants.rs b/src/blowfish_constants.rs new file mode 100755 index 0000000..1b1e66b --- /dev/null +++ b/src/blowfish_constants.rs @@ -0,0 +1,188 @@ +pub const BLOWFISH_P: [u32; 18] = [ + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, + 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b +]; + +pub const BLOWFISH_S: [[u32; 256]; 4] = + [[ + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, + 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, + 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, + 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, + 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, + 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, + 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, + 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176, + 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, + 0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, + 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, + 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, + 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, + 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, + 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, + 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, + 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, + 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705, + 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, + 0x226800bb, 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, + 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, + 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a + ], + [ + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, + 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, + 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, + 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, + 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, + 0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, + 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, + 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908, + 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, + 0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, + 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, + 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, + 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, + 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, + 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, + 0x11ed935f, 0x16681281, 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, + 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, + 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 0xcf62a1f2, 0x5b8d2646, + 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, + 0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, + 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, + 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7 + ], + [ + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, + 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, + 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, + 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec, + 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, + 0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58, + 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, + 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, + 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, + 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, + 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, + 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, + 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, + 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, 0xf0f7c086, + 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, + 0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, + 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, + 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe, + 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, + 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, + 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, + 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0 + ], + [ + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, + 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, + 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, + 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1, + 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, + 0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, + 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, + 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, + 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, + 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, + 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, + 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, + 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, + 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a, + 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, + 0x0f91fc71, 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, + 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, + 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, + 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, + 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, + 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, + 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 + ] + ]; \ No newline at end of file diff --git a/src/bootdata.rs b/src/bootdata.rs new file mode 100755 index 0000000..34307eb --- /dev/null +++ b/src/bootdata.rs @@ -0,0 +1,43 @@ +use std::fs; +use std::path::PathBuf; + +/// Boot data for FFXIV. +pub struct BootData { + version: String, +} + +fn is_valid(path: &str) -> bool { + let d = PathBuf::from(path); + + if fs::metadata(d.as_path()).is_err() { + return false; + } + + true +} + +impl BootData { + /// Reads from an existing boot data location. + /// + /// This will return _None_ if the boot directory is not valid, but it does not check the validity + /// of each individual file. + /// + /// # Example + /// + /// ``` + /// # use physis::bootdata::BootData; + /// let boot = BootData::from_existing("SquareEnix/Final Fantasy XIV - A Realm Reborn/boot"); + /// # assert!(boot.is_none()) + /// ``` + pub fn from_existing(directory: &str) -> Option { + match is_valid(directory) { + true => Some(BootData { + version: String::new() + }), + false => { + println!("Boot data is not valid!"); + None + } + } + } +} \ No newline at end of file diff --git a/src/common.rs b/src/common.rs new file mode 100755 index 0000000..916f046 --- /dev/null +++ b/src/common.rs @@ -0,0 +1,24 @@ +#[repr(u8)] +pub enum Language { + None, + Japanese, + English, + German, + French, + ChineseSimplified, + ChineseTraditional, + Korean, +} + +pub fn get_language_code(lang: Language) -> &'static str { + match lang { + Language::None => "", + Language::Japanese => "ja", + Language::English => "en", + Language::German => "de", + Language::French => "fr", + Language::ChineseSimplified => "chs", + Language::ChineseTraditional => "cht", + Language::Korean => "ko" + } +} \ No newline at end of file diff --git a/src/compression.rs b/src/compression.rs new file mode 100755 index 0000000..a6541ef --- /dev/null +++ b/src/compression.rs @@ -0,0 +1,102 @@ +use std::ptr::null_mut; +use libz_sys::*; + +// This module's functions are licensed under MIT from https://github.com/rust-lang/flate2-rs +mod flate2_zallocation { + use std::ffi::c_void; + use std::ptr::null_mut; + use std::alloc::{self, Layout}; + + const ALIGN: usize = std::mem::align_of::(); + + fn align_up(size: usize, align: usize) -> usize { + (size + align - 1) & !(align - 1) + } + + pub extern "C" fn zalloc(_ptr: *mut c_void, items: u32, item_size: u32) -> *mut c_void { + // We need to multiply `items` and `item_size` to get the actual desired + // allocation size. Since `zfree` doesn't receive a size argument we + // also need to allocate space for a `usize` as a header so we can store + // how large the allocation is to deallocate later. + let size = match items + .checked_mul(item_size) + .and_then(|i| usize::try_from(i).ok()) + .map(|size| align_up(size, ALIGN)) + .and_then(|i| i.checked_add(std::mem::size_of::())) + { + Some(i) => i, + None => return null_mut(), + }; + + // Make sure the `size` isn't too big to fail `Layout`'s restrictions + let layout = match Layout::from_size_align(size, ALIGN) { + Ok(layout) => layout, + Err(_) => return null_mut(), + }; + + unsafe { + // Allocate the data, and if successful store the size we allocated + // at the beginning and then return an offset pointer. + let ptr = alloc::alloc(layout) as *mut usize; + if ptr.is_null() { + return ptr as *mut c_void; + } + *ptr = size; + ptr.add(1) as *mut c_void + } + } + + pub extern "C" fn zfree(_ptr: *mut c_void, address: *mut c_void) { + unsafe { + // Move our address being freed back one pointer, read the size we + // stored in `zalloc`, and then free it using the standard Rust + // allocator. + let ptr = (address as *mut usize).offset(-1); + let size = *ptr; + let layout = Layout::from_size_align_unchecked(size, ALIGN); + alloc::dealloc(ptr as *mut u8, layout) + } + } +} + +pub fn no_header_decompress(in_data: &mut [u8], out_data: &mut [u8]) -> bool { + use crate::compression::flate2_zallocation::zalloc; + use crate::compression::flate2_zallocation::zfree; + + unsafe { + let mut strm = z_stream { + next_in: null_mut(), + avail_in: in_data.len() as u32, + total_in: 0, + next_out: null_mut(), + avail_out: 0, + total_out: 0, + msg: null_mut(), + state: null_mut(), + zalloc, + zfree, + opaque: null_mut(), + data_type: 0, + adler: 0, + reserved: 0, + }; + + let ret = inflateInit2_(&mut strm, -15, zlibVersion(), core::mem::size_of::() as i32); + if ret != Z_OK { + return false; + } + + strm.next_in = in_data.as_mut_ptr(); + strm.avail_out = out_data.len() as u32; + strm.next_out = out_data.as_mut_ptr(); + + let ret = inflate(&mut strm, Z_NO_FLUSH); + if ret != Z_STREAM_END { + return false; + } + + inflateEnd(&mut strm); + + true + } +} \ No newline at end of file diff --git a/src/dat.rs b/src/dat.rs new file mode 100755 index 0000000..130b42a --- /dev/null +++ b/src/dat.rs @@ -0,0 +1,333 @@ +use std::io::{Cursor, Read, Seek, SeekFrom}; +use binrw::BinRead; +use binrw::binrw; +use crate::compression::no_header_decompress; +use crate::gamedata::MemoryBuffer; +use crate::model::ModelHeader; +use std::io::Write; +use binrw::BinWrite; +use crate::sqpack::read_data_block; + +#[binrw] +#[brw(repr = i32)] +#[derive(Debug, PartialEq)] +pub enum FileType { + Empty = 1, + Standard, + Model, + Texture, +} + +#[derive(BinRead)] +struct StandardFileBlock { + #[br(pad_before = 8)] + num_blocks: u32, +} + +#[derive(BinRead)] +struct TextureLodBlock { + compressed_offset: u32, + compressed_size: u32, + decompressed_size: u32, + + block_offset: u32, + block_count: u32, +} + +#[binrw] +pub struct ModelMemorySizes + binrw::BinWrite + Default + std::ops::AddAssign + Copy> { + pub stack_size: T, + pub runtime_size: T, + + pub vertex_buffer_size: [T; 3], + pub edge_geometry_vertex_buffer_size: [T; 3], + pub index_buffer_size: [T; 3], +} + +impl + binrw::BinWrite + Default + std::ops::AddAssign + Copy> ModelMemorySizes { + pub fn total(&self) -> T { + let mut total: T = T::default(); + + total += self.stack_size; + total += self.runtime_size; + + for i in 0..3 { + total += self.vertex_buffer_size[i]; + total += self.edge_geometry_vertex_buffer_size[i]; + total += self.index_buffer_size[i]; + } + + total + } +} + +#[derive(BinRead)] +pub struct ModelFileBlock { + pub num_blocks: u32, + pub num_used_blocks: u32, + pub version: u32, + + pub uncompressed_size: ModelMemorySizes, + pub compressed_size: ModelMemorySizes, + pub offset: ModelMemorySizes, + pub index: ModelMemorySizes, + pub num: ModelMemorySizes, + + pub vertex_declaration_num: u16, + pub material_num: u16, + pub num_lods: u8, + + #[br(map = | x: u8 | x != 0)] + pub index_buffer_streaming_enabled: bool, + #[brw(pad_after = 1)] + #[br(map = | x: u8 | x != 0)] + pub edge_geometry_enabled: bool, +} + +#[derive(BinRead)] +struct TextureBlock { + #[br(pad_before = 8)] + num_blocks: u32, + + #[br(count = num_blocks)] + lods: Vec, +} + +/// A SqPack file info header. It can optionally contain extra information, such as texture or +/// model data depending on the file type. +#[derive(BinRead)] +struct FileInfo { + size: u32, + file_type: FileType, + file_size: i32, + + #[br(if (file_type == FileType::Standard))] + standard_info: Option, + + #[br(if (file_type == FileType::Model))] + model_info: Option, + + #[br(if (file_type == FileType::Texture))] + texture_info: Option, +} + +#[binrw] +pub struct Block { + #[br(pad_after = 4)] + offset: i32, +} + +#[binrw] +#[derive(Debug)] +#[br(import { x : i32, y : i32 })] +#[br(map = | _ : i32 | if x < 32000 { CompressionMode::Compressed{ compressed_length : x, decompressed_length : y} } else { CompressionMode::Uncompressed { file_size : y } } )] +pub enum CompressionMode { + // we manually map here, because for this case the enum value is also a raw value we want to extract :-) + Compressed { compressed_length: i32, decompressed_length: i32 }, + Uncompressed { file_size: i32 }, +} + +#[binrw::binread] +#[derive(Debug)] +pub struct BlockHeader { + #[br(pad_after = 4)] + pub size : u32, + + #[br(temp)] + x : i32, + + #[br(temp)] + y : i32, + + #[br(args { x, y })] + #[br(restore_position)] + pub compression: CompressionMode, +} + +pub struct DatFile { + file: std::fs::File, +} + +// from https://users.rust-lang.org/t/how-best-to-convert-u8-to-u16/57551/4 +fn to_u8_slice(slice: &mut [u16]) -> &mut [u8] { + let byte_len = 2 * slice.len(); + unsafe { + std::slice::from_raw_parts_mut( + slice.as_mut_ptr().cast::(), + byte_len, + ) + } +} + +impl DatFile { + /// Creates a new reference to an existing dat file. + pub fn from_existing(path: &str) -> Option { + Some(DatFile { + file: std::fs::File::open(path).ok()? + }) + } + + + pub fn read_from_offset(&mut self, offset: u32) -> Option { + let offset: u64 = (offset * 0x80) as u64; + + self.file.seek(SeekFrom::Start(offset)) + .expect("TODO: panic message"); + + let file_info = FileInfo::read(&mut self.file) + .expect("Failed to parse file info."); + + match file_info.file_type { + FileType::Empty => None, + FileType::Standard => { + let standard_file_info = file_info.standard_info.unwrap(); + + let mut blocks: Vec = Vec::with_capacity(standard_file_info.num_blocks as usize); + + for _ in 0..standard_file_info.num_blocks { + blocks.push(Block::read(&mut self.file).unwrap()); + } + + let mut data: Vec = Vec::with_capacity(file_info.file_size as usize); + + let starting_position = offset + (file_info.size as u64); + + for i in 0..standard_file_info.num_blocks { + data.append(&mut read_data_block(&mut self.file, starting_position + (blocks[i as usize].offset as u64)) + .expect("Failed to read data block.")); + } + + Some(data) + } + FileType::Model => { + let mut buffer = Cursor::new(Vec::new()); + + let model_file_info = file_info.model_info.unwrap(); + + let base_offset = offset + (file_info.size as u64); + + let total_blocks = model_file_info.num.total(); + + let mut compressed_block_sizes: Vec = vec![0; total_blocks as usize]; + let slice: &mut [u8] = to_u8_slice(&mut compressed_block_sizes); + + self.file.read_exact(slice).ok()?; + + let mut current_block = 0; + + let mut vertex_data_offsets: [u32; 3] = [0; 3]; + let mut vertex_data_sizes: [u32; 3] = [0; 3]; + + let mut index_data_offsets: [u32; 3] = [0; 3]; + let mut index_data_sizes: [u32; 3] = [0; 3]; + + // start writing at 0x44 + buffer.seek(SeekFrom::Start(0x44)).ok()?; + + self.file.seek(SeekFrom::Start(base_offset + (model_file_info.offset.stack_size as u64))).ok()?; + + // read from stack blocks + let mut read_model_blocks = |offset: u64, size: usize| -> Option { + self.file.seek(SeekFrom::Start(base_offset + offset)).ok()?; + let stack_start = buffer.position(); + for _ in 0..size { + let last_pos = &self.file.stream_position().unwrap(); + + let data = read_data_block(&self.file, *last_pos) + .expect("Unable to read block data."); + // write to buffer + buffer.write(data.as_slice()).ok()?; + + self.file.seek(SeekFrom::Start(last_pos + (compressed_block_sizes[current_block as usize] as u64))).ok()?; + current_block += 1; + } + + Some(buffer.position() - stack_start) + }; + + let stack_size = read_model_blocks(model_file_info.offset.stack_size as u64, model_file_info.num.stack_size as usize).unwrap() as u32; + let runtime_size = read_model_blocks(model_file_info.offset.runtime_size as u64, model_file_info.num.runtime_size as usize).unwrap() as u32; + + let mut process_model_data = |i: usize, size: u32, offset: u32, offsets: &mut [u32; 3], data_sizes: &mut [u32; 3]| { + if size != 0 { + let current_vertex_offset = buffer.position() as u32; + if i == 0 || current_vertex_offset != offsets[i - 1] { + offsets[i] = current_vertex_offset; + } else { + offsets[i] = 0; + } + + self.file.seek(SeekFrom::Start(base_offset + (offset as u64))).ok(); + + for _ in 0..size { + let last_pos = self.file.stream_position().unwrap(); + + let data = read_data_block(&self.file, last_pos) + .expect("Unable to read raw model block!"); + + buffer.write(data.as_slice()).expect("Unable to write to memory buffer!"); + + data_sizes[i] += data.len() as u32; + self.file.seek(SeekFrom::Start(last_pos + (compressed_block_sizes[current_block] as u64))) + .expect("Unable to seek properly."); + current_block += 1; + } + } + }; + + // process all 3 lods + for i in 0..3 { + // process vertices + process_model_data(i, model_file_info.num.vertex_buffer_size[i] as u32, model_file_info.offset.vertex_buffer_size[i], &mut vertex_data_offsets, &mut vertex_data_sizes); + + // TODO: process edges + + // process indices + process_model_data(i, model_file_info.num.index_buffer_size[i] as u32, model_file_info.offset.index_buffer_size[i], &mut index_data_offsets, &mut index_data_sizes); + } + + let header = ModelHeader { + version: model_file_info.version, + stack_size, + runtime_size, + vertex_declaration_count: model_file_info.vertex_declaration_num, + material_count: model_file_info.material_num, + vertex_offsets: vertex_data_offsets, + index_offsets: index_data_offsets, + vertex_buffer_size: vertex_data_sizes, + index_buffer_size: index_data_sizes, + lod_count: model_file_info.num_lods, + index_buffer_streaming_enabled: model_file_info.index_buffer_streaming_enabled, + has_edge_geometry: model_file_info.edge_geometry_enabled, + }; + + buffer.seek(SeekFrom::Start(0)).ok()?; + + header.write_to(&mut buffer).ok()?; + + Some(buffer.into_inner()) + } + FileType::Texture => { + let mut data: Vec = Vec::with_capacity(file_info.file_size as usize); + + let texture_file_info = file_info.texture_info.unwrap(); + + for i in 0..texture_file_info.num_blocks { + let mut running_block_total = (texture_file_info.lods[i as usize].compressed_offset as u64) + offset + (file_info.size as u64); + + data.append(&mut read_data_block(&self.file, running_block_total).unwrap()); + + for _ in 1..texture_file_info.lods[i as usize].block_count { + running_block_total += i16::read(&mut self.file).unwrap() as u64; + data.append(&mut read_data_block(&self.file, running_block_total).unwrap()); + } + + // dummy? + i16::read(&mut self.file).unwrap(); + } + + Some(data) + } + } + } +} \ No newline at end of file diff --git a/src/equipment.rs b/src/equipment.rs new file mode 100755 index 0000000..cb8976d --- /dev/null +++ b/src/equipment.rs @@ -0,0 +1,61 @@ +use crate::race::{Gender, get_race_id, Race, Subrace}; + +#[repr(u8)] +pub enum Slot { + Head, + Hands, + Legs, + Feet, + Body, + Earring, + Neck, + Rings, + Wrists, +} + +pub fn get_slot_abbreviation(slot: Slot) -> &'static str { + match slot { + Slot::Head => "met", + Slot::Hands => "glv", + Slot::Legs => "dwn", + Slot::Feet => "sho", + Slot::Body => "top", + Slot::Earring => "ear", + Slot::Neck => "nek", + Slot::Rings => "rir", + Slot::Wrists => "wrs" + } +} + +pub fn get_slot_from_id(id: i32) -> Option { + match id { + 3 => Some(Slot::Head), + 5 => Some(Slot::Hands), + 7 => Some(Slot::Legs), + 8 => Some(Slot::Feet), + 4 => Some(Slot::Body), + 9 => Some(Slot::Earring), + 10 => Some(Slot::Neck), + 12 => Some(Slot::Rings), + 11 => Some(Slot::Wrists), + _ => None + } +} + +pub fn build_equipment_path(model_id: i32, race: Race, subrace: Subrace, gender: Gender, slot: Slot) -> String { + format!("chara/equipment/e{:04}/model/c{:04}e{:04}_{}.mdl", + model_id, + get_race_id(race, subrace, gender).unwrap(), + model_id, + get_slot_abbreviation(slot)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_equipment_path() { + assert_eq!(build_equipment_path(0, Race::Hyur, Subrace::Midlander, Gender::Male, Slot::Body), "chara/equipment/e0000/model/c0101e0000_top.mdl"); + } +} \ No newline at end of file diff --git a/src/exl.rs b/src/exl.rs new file mode 100755 index 0000000..d322b4a --- /dev/null +++ b/src/exl.rs @@ -0,0 +1,86 @@ +use std::collections::HashMap; +use std::io::{BufRead, BufReader}; + +/// Represents an Excel List. +pub struct EXL { + /// The version of the list. + pub version: i32, + + /// The entries of the list. + pub entries: HashMap, +} + +impl EXL { + /// Initializes `EXL` from an existing list. + pub fn from_existing(path: &str) -> Option { + let mut exl = Self { + version: 0, + entries: HashMap::new(), + }; + + let file = std::fs::File::open(path).unwrap(); + + let reader = BufReader::new(file); + + for (_, line) in reader.lines().enumerate() { + // now parse the line! + + let unwrap = line.unwrap(); + let (name, value) = unwrap.split_once(',').unwrap(); + + let parsed_value: i32 = value.parse().unwrap(); + + if name == "EXLT" { + exl.version = parsed_value; + } else { + exl.entries.insert(name.parse().unwrap(), parsed_value); + } + } + + Some(exl) + } + + /// Checks whether or not the list contains a key. + /// + /// # Example + /// + /// ```should_panic + /// # use physis::exl::EXL; + /// let exl = EXL::from_existing("root.exl").unwrap(); + /// exl.contains("Achivements"); + /// ``` + pub fn contains(&self, key: &str) -> bool { + self.entries.contains_key(key) + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + use super::*; + + fn common_setup() -> EXL { + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.push("resources/tests"); + d.push("test.exl"); + + EXL::from_existing(d.to_str().unwrap()).unwrap() + } + + #[test] + fn version_parsing() { + let exl = common_setup(); + + assert_eq!(exl.version, 2); + } + + #[test] + fn contains() { + let exl = common_setup(); + + assert!(exl.contains("Foo")); + + // should be case-sensitive + assert!(!exl.contains("foo")); + } +} \ No newline at end of file diff --git a/src/gamedata.rs b/src/gamedata.rs new file mode 100755 index 0000000..30f9171 --- /dev/null +++ b/src/gamedata.rs @@ -0,0 +1,212 @@ +use std::ffi::OsStr; +use std::fs; +use std::fs::DirEntry; +use std::path::PathBuf; +use crate::dat::DatFile; +use crate::index::IndexFile; +use crate::repository::{Category, Repository, string_to_category}; +use crate::sqpack::calculate_hash; + +/// Framework for operating on game data. +pub struct GameData { + /// The game directory to operate on. + pub game_directory: String, + + /// Repositories in the game directory. + pub repositories: Vec, +} + +fn is_valid(path: &str) -> bool { + let mut d = PathBuf::from(path); + + if fs::metadata(d.as_path()).is_err() { + println!("Failed game directory."); + return false; + } + + true +} + +pub type MemoryBuffer = Vec; + +impl GameData { + /// Read game data from an existing game installation. + /// + /// This will return _None_ if the game directory is not valid, but it does not check the validity + /// of each individual file. + /// + /// **Note**: None of the repositories are searched, and it's required to call `reload_repositories()`. + /// + /// # Example + /// + /// ``` + /// # use physis::gamedata::GameData; + /// GameData::from_existing("$FFXIV/game"); + /// ``` + pub fn from_existing(directory: &str) -> Option { + match is_valid(directory) { + true => Some(Self { + game_directory: String::from(directory), + repositories: vec![], + }), + false => { + println!("Game data is not valid!"); + None + } + } + } + + /// Reloads all repository information from disk. This is a fast operation, as it's not actually + /// reading any dat files yet. + /// + /// # Example + /// + /// ```should_panic + /// # use physis::gamedata::GameData; + /// let mut game = GameData::from_existing("$FFXIV/game").unwrap(); + /// game.reload_repositories(); + /// ``` + pub fn reload_repositories(&mut self) { + self.repositories.clear(); + + let mut d = PathBuf::from(self.game_directory.as_str()); + d.push("sqpack"); + + let repository_paths: Vec = fs::read_dir(d.as_path()) + .unwrap() + .filter_map(Result::ok) + .filter(|s| s.file_type().unwrap().is_dir()) + .collect(); + + for repository_path in repository_paths { + self.repositories.push(Repository::from_existing(repository_path.path().to_str().unwrap()).unwrap()); + } + + self.repositories.sort(); + } + + fn get_index_file(&self, path: &str) -> Option { + let (repository, category) = self.parse_repository_category(path).unwrap(); + + let index_path = format!("{}/sqpack/{}/{}", + self.game_directory, repository.name, repository.index_filename(category)); + + IndexFile::from_existing(index_path.as_str()) + } + + /// Checks if a file located at `path` exists. + /// + /// # Example + /// + /// ```should_panic + /// # use physis::gamedata::GameData; + /// # let mut game = GameData::from_existing("SquareEnix/Final Fantasy XIV - A Realm Reborn/game").unwrap(); + /// if game.exists("exd/cid.exl") { + /// println!("Cid really does exist!"); + /// } else { + /// println!("Oh noes!"); + /// } + /// ``` + pub fn exists(&self, path: &str) -> bool { + let hash = calculate_hash(path); + + let index_file = self.get_index_file(path) + .expect("Failed to find index file."); + + index_file.entries.iter().any(|s| s.hash == hash) + } + + /// Extracts the file lcoated at `path`. This is returned as an in-memory buffer, and will usually + /// have to be further parsed. + /// + /// # Example + /// + /// ```should_panic + /// # use physis::gamedata::GameData; + /// # use std::io::Write; + /// # let mut game = GameData::from_existing("SquareEnix/Final Fantasy XIV - A Realm Reborn/game").unwrap(); + /// let data = game.extract("exd/root.exl").unwrap(); + /// + /// let mut file = std::fs::File::create("root.exl").unwrap(); + /// file.write(data.as_slice()); + /// ``` + pub fn extract(&self, path: &str) -> Option { + let hash = calculate_hash(path); + + let index_file = self.get_index_file(path) + .expect("Failed to find index file."); + + let slice = index_file.entries.iter().find(|s| s.hash == hash); + match slice { + Some(entry) => { + let (repository, category) = self.parse_repository_category(path).unwrap(); + + let dat_filename = repository.dat_filename(category, entry.bitfield.data_file_id().into()); + + let dat_path = format!("{}/sqpack/{}/{}", + self.game_directory, repository.name, dat_filename); + + let mut dat_file = DatFile::from_existing(dat_path.as_str()).unwrap(); + + dat_file.read_from_offset(entry.bitfield.offset()) + } + None => { + println!("Entry not found."); + None + } + } + } + + /// Parses a path structure and spits out the corresponding category and repository. + fn parse_repository_category(&self, path: &str) -> Option<(&Repository, Category)> { + let tokens: Vec<&str> = path.split('/').collect(); // TODO: use split_once here + let repository_token = tokens[0]; + + if tokens.len() < 2 { + return None; + } + + for repository in &self.repositories { + if repository.name == repository_token { + return Some((repository, string_to_category(tokens[1])?)); + } + } + + Some((&self.repositories[0], string_to_category(tokens[0])?)) + } +} + +#[cfg(test)] +mod tests { + use crate::repository::Category::EXD; + use super::*; + + fn common_setup_data() -> GameData { + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.push("resources/tests"); + d.push("valid_sqpack"); + d.push("game"); + + GameData::from_existing(d.to_str().unwrap()).unwrap() + } + + #[test] + fn repository_ordering() { + let mut data = common_setup_data(); + data.reload_repositories(); + + assert_eq!(data.repositories[0].name, "ffxiv"); + assert_eq!(data.repositories[1].name, "ex1"); + assert_eq!(data.repositories[2].name, "ex2"); + } + + #[test] + fn repository_and_category_parsing() { + let mut data = common_setup_data(); + data.reload_repositories(); + + assert_eq!(data.parse_repository_category("exd/root.exl").unwrap(), + (&data.repositories[0], EXD)); + assert!(data.parse_repository_category("what/some_font.dat").is_none()); + } +} \ No newline at end of file diff --git a/src/index.rs b/src/index.rs new file mode 100755 index 0000000..00a857f --- /dev/null +++ b/src/index.rs @@ -0,0 +1,82 @@ +use std::io::SeekFrom; +use binrw::binrw; +use binrw::BinRead; +use bitfield_struct::bitfield; + +#[binrw] +#[brw(repr = u8)] +#[derive(Debug)] +pub enum PlatformId { + Win32, + PS3, + PS4, +} + +#[binrw] +#[br(magic = b"SqPack")] +pub struct SqPackHeader { + #[br(pad_before = 2)] + platform_id: PlatformId, + #[br(pad_before = 3)] + size: u32, + version: u32, + file_type: u32, +} + +#[binrw] +pub struct SqPackIndexHeader { + size: u32, + file_type: u32, + index_data_offset: u32, + index_data_size: u32, +} + +#[bitfield(u32)] +#[binrw] +#[br(map = | x: u32 | Self::from(x))] +pub struct IndexHashBitfield { + #[bits(1)] + pub size: u32, + + #[bits(3)] + pub data_file_id: u32, + + #[bits(28)] + pub offset: u32, +} + +#[binrw] +pub struct IndexHashTableEntry { + pub(crate) hash: u64, + #[br(pad_after = 4)] + pub(crate) bitfield: IndexHashBitfield, +} + +#[derive(Debug)] +pub struct IndexEntry { + pub hash: u64, + pub data_file_id: u8, + pub offset: u32, +} + +#[binrw] +pub struct IndexFile { + sqpack_header: SqPackHeader, + + #[br(seek_before = SeekFrom::Start(sqpack_header.size.into()))] + index_header: SqPackIndexHeader, + + #[br(seek_before = SeekFrom::Start(index_header.index_data_offset.into()))] + #[br(count = index_header.index_data_size / (core::mem::size_of::< IndexHashTableEntry > () as u32))] + pub entries: Vec, +} + +impl IndexFile { + /// Creates a new reference to an existing index file, this reads both an index and index2 file. + pub fn from_existing(path: &str) -> Option { + let mut index_file = std::fs::File::open(path) + .expect("Failed to read index file."); + + Some(IndexFile::read(&mut index_file).ok()?) + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100755 index 0000000..daa9068 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,34 @@ +extern crate core; + +use std::fs; +use std::path::PathBuf; +use crate::bootdata::BootData; +use crate::gamedata::GameData; + +pub mod gamedata; + +/// Reading game data repositories, such as "ffxiv" and "ex1", and so on. +pub mod repository; + +pub mod bootdata; + +/// Everything to do with reading SqPack files. +pub mod sqpack; + +pub mod index; +pub mod dat; +mod compression; +mod model; +pub mod race; + +/// Reading Excel lists (EXL). +pub mod exl; +pub mod equipment; +pub mod common; +pub mod patch; + +#[macro_use] +mod macros; + +pub mod blowfish; +mod blowfish_constants; \ No newline at end of file diff --git a/src/macros.rs b/src/macros.rs new file mode 100755 index 0000000..c4a1744 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,33 @@ +/// Creates a enum list of combined race identifiers. +#[macro_export] macro_rules! define_race_enum { + ( + pub enum $name:ident { + $( + $([$id:expr]($race:ident, $gender:ident $(, $subrace:ident)?))* + ),+$(,)? + } + ) => { + paste! { + #[derive(PartialEq, Debug)] + + pub enum $name { + $( + $([<$race $($subrace)? $gender>] = $id)* + , + )+ + } + } + + paste! { + pub fn convert_to_internal(race : Race, subrace : Subrace, gender : Gender) -> Option<$name> { + $( + $(if race == $race $(&& subrace == $subrace)? && gender == $gender { + return Some($name::[<$race $($subrace)? $gender>]) + })* + )+ + + None + } + } + }; +} diff --git a/src/model.rs b/src/model.rs new file mode 100755 index 0000000..9771959 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,33 @@ +use binrw::binrw; + +#[binrw] +pub struct ModelHeader { + pub(crate) version: u32, + + pub stack_size: u32, + pub runtime_size: u32, + + pub vertex_declaration_count: u16, + pub material_count: u16, + + pub vertex_offsets: [u32; 3], + pub index_offsets: [u32; 3], + pub vertex_buffer_size: [u32; 3], + pub index_buffer_size: [u32; 3], + + pub lod_count: u8, + + #[br(map = | x: u8 | x != 0)] + #[bw(map = | x: & bool | -> u8 { if * x { 1 } else { 0 } })] + pub index_buffer_streaming_enabled: bool, + #[br(map = | x: u8 | x != 0)] + #[bw(map = | x: & bool | -> u8 { if * x { 1 } else { 0 } })] + #[brw(pad_after = 1)] + pub has_edge_geometry: bool, +} + +pub struct Model {} + +impl Model { + pub fn from_existing() {} +} \ No newline at end of file diff --git a/src/patch.rs b/src/patch.rs new file mode 100755 index 0000000..325d0b4 --- /dev/null +++ b/src/patch.rs @@ -0,0 +1,444 @@ +use std::fs; +use std::fs::{File, OpenOptions}; +use std::io::{Cursor, Seek, SeekFrom, Write}; +use binrw::BinRead; +use binrw::binread; +use crate::{BootData, GameData}; +use crate::sqpack::{read_data_block, read_data_block_patch}; +use core::cmp::min; +use crate::patch::TargetHeaderKind::Version; + +#[derive(BinRead, Debug)] +struct PatchChunk { + #[br(big)] + size: u32, + chunk_type: ChunkType, + #[br(if(chunk_type != ChunkType::EndOfFile))] + crc32: u32 +} + +#[derive(BinRead, PartialEq, Debug)] +enum ChunkType { + #[br(magic = b"FHDR")] FileHeader(FileHeaderChunk), + #[br(magic = b"APLY")] ApplyOption(ApplyOptionChunk), + #[br(magic = b"ADIR")] AddDirectory(AddDirectoryChunk), + #[br(magic = b"DELD")] DeleteDirectory(DeleteDirectoryChunk), + #[br(magic = b"SQPK")] Sqpk(SqpkChunk), + #[br(magic = b"EOF_")] EndOfFile, +} + +#[derive(BinRead, PartialEq, Debug)] +#[br(big)] +struct FileHeaderChunk { + #[br(pad_before = 2)] + #[br(pad_after = 1)] + version: u8, + + #[br(count = 4)] + #[br(map = | x: Vec < u8 > | String::from_utf8(x).unwrap())] + name: String, + + #[br(big)] + entry_files: u32, + + #[br(big)] + add_directories : u32, + + #[br(big)] + delete_directories : u32, + + #[br(big)] + delete_data_size : u32, + + #[br(big)] + delete_data_size_2 : u32, + + #[br(big)] + minor_version : u32, + + #[br(big)] + repository_name : u32, + + #[br(big)] + commands : u32, + + #[br(big)] + sqpk_add_commands: u32, + + #[br(big)] + sqpk_delete_commands: u32, + + #[br(big)] + sqpk_expand_commands: u32, + + #[br(big)] + sqpk_header_commands: u32, + + #[br(big)] + #[br(pad_after = 0xB8)] + sqpk_file_commands: u32 +} + +#[binrw::binread] +#[derive(PartialEq, Debug)] +struct ApplyOptionChunk { + #[br(pad_after = 4)] + option: u32, + value: u32, +} + +#[binrw::binread] +#[derive(PartialEq, Debug)] +struct AddDirectoryChunk { + #[br(temp)] + path_length: u32, + + #[br(count = path_length)] + #[br(map = | x: Vec < u8 > | String::from_utf8(x).unwrap())] + name: String, +} + +#[binread] +#[br(big)] +#[derive(PartialEq, Debug)] +struct DeleteDirectoryChunk { + #[br(temp)] + path_length: u32, + + #[br(count = path_length)] + #[br(map = | x: Vec < u8 > | String::from_utf8(x).unwrap())] + name: String, +} + +#[binread] +#[derive(PartialEq, Debug)] +enum SqpkOperation { + #[br(magic = b'A')] AddData(SqpkAddData), + #[br(magic = b'D')] DeleteData(SqpkDeleteData), + #[br(magic = b'E')] ExpandData(SqpkDeleteData), + #[br(magic = b'F')] FileOperation(SqpkFileOperationData), + #[br(magic = b'H')] HeaderUpdate(SqpkHeaderUpdateData), + #[br(magic = b'I')] IndexAddDelete(SqpkIndexData), + #[br(magic = b'X')] PatchInfo(SqpkPatchInfo), + #[br(magic = b'T')] TargetInfo(SqpkTargetInfo), +} + +#[derive(BinRead, PartialEq, Debug)] +enum SqpkIndexCommand { + #[br(magic = b'A')] Add, + #[br(magic = b'D')] Delete +} + +#[derive(BinRead, PartialEq, Debug)] +#[br(big)] +struct SqpkIndexData { + command : SqpkIndexCommand, + + #[br(map = | x: u8 | x != 0)] + #[br(pad_after = 1)] + is_synonym : bool, + + main_id : u16, + sub_id : u16, + file_id : u32, + + file_hash : u64, + + block_offset : u32, + block_number : u32 +} + +#[derive(BinRead, PartialEq, Debug)] +struct SqpkPatchInfo { + status: u8, + #[br(pad_after = 1)] + version: u8, + + #[br(big)] + install_size : u64 +} + +#[binread] +#[derive(PartialEq, Debug)] +enum SqpkFileOperation { + #[br(magic = b'A')] AddFile, + #[br(magic = b'R')] RemoveAll +} + +#[derive(BinRead, PartialEq, Debug)] +#[br(big)] +struct SqpkAddData { + #[br(pad_before = 3)] + main_id : u16, + sub_id : u16, + file_id : u32, + + #[br(map = | x : i32 | x << 7 )] + block_offset : i32, + #[br(map = | x : i32 | x << 7 )] + block_number : i32, + #[br(map = | x : i32 | x << 7 )] + block_delete_number : i32, + + #[br(count = block_number)] + block_data : Vec +} + +#[derive(BinRead, PartialEq, Debug)] +#[br(big)] +struct SqpkDeleteData { + #[br(pad_before = 3)] + main_id : u16, + sub_id : u16, + file_id : u32, + + #[br(map = | x : i32 | x << 7 )] + block_offset : i32, + #[br(pad_after = 4)] + block_number : i32 +} + +#[binread] +#[derive(PartialEq, Debug)] +enum TargetFileKind { + #[br(magic=b'D')] Dat, + #[br(magic=b'I')] Index +} + +#[binread] +#[derive(PartialEq, Debug)] +enum TargetHeaderKind { + #[br(magic=b'V')] Version, + #[br(magic=b'I')] Index, + #[br(magic=b'D')] Data +} + +#[derive(BinRead, PartialEq, Debug)] +#[br(big)] +struct SqpkHeaderUpdateData { + file_kind : TargetFileKind, + header_kind : TargetHeaderKind, + + #[br(pad_before = 1)] + main_id : u16, + sub_id : u16, + file_id : u32, + + #[br(count = 1024)] + header_data : Vec +} + +#[binread] +#[derive(PartialEq, Debug)] +#[br(big)] +struct SqpkFileOperationData { + #[br(pad_after = 2)] + operation: SqpkFileOperation, + + offset : i64, + file_size : u64, + #[br(temp)] + path_length : u32, + expansion_id : u32, + + #[br(count = path_length)] + #[br(map = | x: Vec < u8 > | String::from_utf8(x[..x.len() - 1].to_vec()).unwrap())] + path: String, +} + +#[derive(BinRead, PartialEq, Debug)] +#[br(big)] +struct SqpkTargetInfo { + #[br(pad_before = 3)] + platform : u16, + region : i16, + is_debug : i16, + version : u16, + #[br(little)] + deleted_data_size : u64, + #[br(little)] + #[br(pad_after = 96)] + seek_count : u64 +} + +#[derive(BinRead, PartialEq, Debug)] +#[br(big)] +struct SqpkChunk { + size: u32, + operation : SqpkOperation +} + +pub fn process_patch(data_dir : &str, path : &str) { + let mut file = File::open(path).unwrap(); + + file.seek(SeekFrom::Start(12)); + + loop { + let chunk = PatchChunk::read(&mut file).unwrap(); + + match chunk.chunk_type { + ChunkType::Sqpk(pchunk) => { + match pchunk.operation { + SqpkOperation::AddData(add) => { + let filename = format!("{}/sqpack/ffxiv/{:02x}{:04x}.win32.dat{}", data_dir, add.main_id, add.sub_id, add.file_id); + + let mut new_file = OpenOptions::new() + .write(true) + .create(true) + .open(filename).unwrap(); + + new_file.seek(SeekFrom::Start(add.block_offset as u64)); + + new_file.write(&*add.block_data); + + let wipe_buffer : [u8; 1 << 16] = [0; 1 << 16]; + + let mut length = add.block_delete_number; + let mut num_bytes = 0; + while length > 0 { + num_bytes = min(wipe_buffer.len() as i32, add.block_delete_number); + new_file.write(&wipe_buffer[0..num_bytes as usize]); + length -= num_bytes; + } + } + SqpkOperation::DeleteData(delete) => { + let filename = format!("{}/sqpack/ffxiv/{:02x}{:04x}.win32.dat{}", data_dir, delete.main_id, delete.sub_id, delete.file_id); + + let mut new_file = OpenOptions::new() + .write(true) + .create(true) + .open(filename).unwrap(); + + new_file.seek(SeekFrom::Start(delete.block_offset as u64)); + + let wipe_buffer : [u8; 1 << 16] = [0; 1 << 16]; + + let mut length = delete.block_number << 7; + let mut num_bytes = 0; + while length > 0 { + num_bytes = min(wipe_buffer.len() as i32, delete.block_number << 7); + new_file.write(&wipe_buffer[0..num_bytes as usize]); + length -= num_bytes; + } + + let block_size : i32 = 1 >> 7; + new_file.write(block_size.to_le_bytes().as_slice()); + + let unknown : i32 = 0; + new_file.write(unknown.to_le_bytes().as_slice()); + + let file_size : i32 = 0; + new_file.write(file_size.to_le_bytes().as_slice()); + + let num_blocks : i32 = delete.block_number - 1; + new_file.write(num_blocks.to_le_bytes().as_slice()); + + let used_blocks : i32 = 0; + new_file.write(used_blocks.to_le_bytes().as_slice()); + } + SqpkOperation::ExpandData(expand) => { + let filename = format!("{}/sqpack/ffxiv/{:02x}{:04x}.win32.dat{}", data_dir, expand.main_id, expand.sub_id, expand.file_id); + + let mut new_file = OpenOptions::new() + .write(true) + .create(true) + .open(filename).unwrap(); + + new_file.seek(SeekFrom::Start(expand.block_offset as u64)); + + let wipe_buffer : [u8; 1 << 16] = [0; 1 << 16]; + + let mut length = expand.block_number << 7; + let mut num_bytes = 0; + while length > 0 { + num_bytes = min(wipe_buffer.len() as i32, expand.block_number << 7); + new_file.write(&wipe_buffer[0..num_bytes as usize]); + length -= num_bytes; + } + + let block_size : i32 = 1 >> 7; + new_file.write(block_size.to_le_bytes().as_slice()); + + let unknown : i32 = 0; + new_file.write(unknown.to_le_bytes().as_slice()); + + let file_size : i32 = 0; + new_file.write(file_size.to_le_bytes().as_slice()); + + let num_blocks : i32 = expand.block_number - 1; + new_file.write(num_blocks.to_le_bytes().as_slice()); + + let used_blocks : i32 = 0; + new_file.write(used_blocks.to_le_bytes().as_slice()); + } + SqpkOperation::HeaderUpdate(header) => { + let mut file_path : String; + + match header.file_kind { + TargetFileKind::Dat => { + file_path = format!("{}/sqpack/ffxiv/{:02x}{:04x}.win32.dat{}", data_dir, header.main_id, header.sub_id, header.file_id); + } + TargetFileKind::Index => { + file_path = format!("{}/sqpack/ffxiv/{:02x}{:04x}.win32.index", data_dir, header.main_id, header.sub_id); + + // index files have no special ending if it's file_id == 0 + if header.file_id != 0 { + file_path += &*format!("{}", header.file_id); + } + } + } + + let mut new_file = OpenOptions::new() + .write(true) + .create(true) + .open(file_path.as_str()).unwrap(); + + if header.header_kind != Version { + new_file.seek(SeekFrom::Start(1024)); + } + + new_file.write(&*header.header_data); + } + SqpkOperation::FileOperation(fop) => { + match fop.operation { + SqpkFileOperation::AddFile => { + let new_path = data_dir.to_owned() + "/" + &fop.path; + + let (left, right) = new_path.rsplit_once('/').unwrap(); + + fs::create_dir_all(left); + + // reverse reading crc32 + file.seek(SeekFrom::Current(-4)); + + let mut data: Vec = Vec::with_capacity(fop.file_size as usize); + + while data.len() < fop.file_size as usize { + data.append(&mut read_data_block_patch(&mut file).unwrap()); + } + + // re-apply crc32 + file.seek(SeekFrom::Current(4)); + + // now apply the file! + let mut new_file = OpenOptions::new() + .write(true) + .create(true) + .open(new_path).unwrap(); + new_file.seek(SeekFrom::Start(fop.offset as u64)); + + new_file.write(&mut data); + } + _ => {} + } + } + _ => {} + } + } + ChunkType::EndOfFile => { + return; + } + _ => {} + } + } +} \ No newline at end of file diff --git a/src/race.rs b/src/race.rs new file mode 100755 index 0000000..98062b2 --- /dev/null +++ b/src/race.rs @@ -0,0 +1,99 @@ +#[derive(PartialEq)] +#[repr(u8)] +pub enum Gender { + Male, + Female, +} + +#[derive(PartialEq)] +#[repr(u8)] +pub enum Subrace { + Midlander, + Highlander, + Wildwood, + Duskwight, + Plainsfolk, + Dunesfolk, + Seeker, + Keeper, + SeaWolf, + Hellsguard, + Raen, + Xaela, + Hellion, + Lost, + Rava, + Veena, +} + +#[derive(PartialEq)] +#[repr(u8)] +pub enum Race { + Hyur, + Elezen, + Lalafell, + Miqote, + Roegadyn, + AuRa, + Hrothgar, + Viera, +} + +mod internal_race { + use crate::race::Race; + use crate::race::Subrace; + use crate::race::Gender; + use crate::race::Race::*; + use crate::race::Subrace::*; + use crate::race::Gender::*; + use paste::paste; + use crate::define_race_enum; + + define_race_enum! { + pub enum RaceTest { + [101](Hyur, Male, Midlander), + [201](Hyur, Female, Midlander), + [301](Hyur, Male, Highlander), + [401](Hyur, Female, Highlander), + + [501](Elezen, Male), + [601](Elezen, Female), + + [701](Miqote, Male), + [801](Miqote, Female), + + [901](Roegadyn, Male), + [1001](Roegadyn, Female), + + [1101](Lalafell, Male), + [1201](Lalafell, Female), + + [1301](AuRa, Male), + [1401](AuRa, Female), + + [1501](Hrothgar, Male), + [1601](Hrothgar, Female), + + [1701](Viera, Male), + [1801](Viera, Female) + } + } +} + +/// Gets a proper race identifier (such as 101, for Hyur-Midlander-Males) given a race, a subrace, +/// and a gender. +pub fn get_race_id(race: Race, subrace: Subrace, gender: Gender) -> Option { + Some(internal_race::convert_to_internal(race, subrace, gender).unwrap() as i32) +} + +#[cfg(test)] +mod tests { + use crate::race::internal_race::{convert_to_internal, RaceTest}; + use super::*; + + #[test] + fn test_convert_to_internal() { + assert_eq!(convert_to_internal(Race::Hyur, Subrace::Midlander, Gender::Female).unwrap(), + RaceTest::HyurMidlanderFemale); + } +} \ No newline at end of file diff --git a/src/repository.rs b/src/repository.rs new file mode 100755 index 0000000..f0efa7d --- /dev/null +++ b/src/repository.rs @@ -0,0 +1,194 @@ +use std::cmp::Ordering; +use std::cmp::Ordering::{Greater, Less}; +use std::fs; +use std::path::{Path, PathBuf}; +use crate::repository::RepositoryType::{Base, Expansion}; + +/// The type of repository, discerning game data from expansion data. +#[derive(Debug, PartialEq)] +pub enum RepositoryType { + /// The base game directory, like "ffxiv". + Base, + /// An expansion directory, like "ex1". + Expansion { + /// The expansion number starting at 1. + number: i32 + }, +} + +/// Encapsulates a directory of game data, such as "ex1". This data is also versioned. +/// This handles calculating the correct dat and index filenames, mainly for `GameData`. +#[derive(Debug)] +pub struct Repository { + /// The folder name, such as "ex1". + pub name: String, + /// The type of repository, such as "base game" or "expansion". + pub repo_type: RepositoryType, + /// The version of the game data. + pub version: String, +} + +impl Eq for Repository {} + +impl PartialEq for Repository { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +impl Ord for Repository { + fn cmp(&self, other: &Self) -> Ordering { + // This ensures that the ordering of the repositories is always ffxiv, ex1, ex2 and so on. + match self.repo_type { + Base => Less, + Expansion { number } => { + let super_number = number; + match other.repo_type { + Base => Greater, + Expansion { number } => super_number.cmp(&number) + } + } + } + } +} + +impl PartialOrd for Repository { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +fn read_version(p: &Path) -> Option { + fs::read_to_string(p).ok() +} + +/// This refers to the specific root directory a file is located in. +/// This is a fixed list of directories, and all of them are known. +#[derive(Debug, PartialEq)] +pub enum Category { + /// Common files such as game fonts, and other data that doesn't really fit anywhere else. + Common, + /// Shared data between game maps. + BgCommon, + /// Game map data such as models, textures, and so on. + Bg, + /// Cutscene content such as animations. + Cut, + /// Character model files and more. + Chara, + /// Compiled shaders used by the retail client. + Shader, + /// UI layouts and textures. + UI, + /// Sound effects, basically anything not under `Music`. + Sound, + /// This "VFX" means "visual effects", and contains textures and definitions for stuff like battle effects. + VFX, + /// A leftover from 1.0, where the UI was primarily driven by LUA scripts. + UIScript, + /// Excel data. + EXD, + /// Many game events are driven by LUA scripts, such as cutscenes. + GameScript, + /// Music! + Music, + /// Unknown purpose, most likely to test SqPack functionality. + SqPackTest, + /// Unknown purpose, msot likely debug files. + Debug, +} + +pub fn string_to_category(string: &str) -> Option { + use crate::repository::Category::*; + + match string { + "common" => Some(Common), + "bgcommon" => Some(BgCommon), + "bg" => Some(Bg), + "cut" => Some(Cut), + "chara" => Some(Chara), + "shader" => Some(Shader), + "ui" => Some(UI), + "sound" => Some(Sound), + "vfx" => Some(VFX), + "ui_script" => Some(UIScript), + "exd" => Some(EXD), + "game_script" => Some(GameScript), + "music" => Some(Music), + "sqpack_test" => Some(SqPackTest), + "debug" => Some(Debug), + _ => None + } +} + +impl Repository { + /// Creates a new `Repository`, from an existing repository directory. This may return `None` if + /// the directory is invalid, e.g. a version file is missing. + pub fn from_existing(dir: &str) -> Option { + let path = Path::new(dir); + if path.metadata().is_err() { + return None; + } + + let name = String::from(path.file_stem().unwrap().to_str().unwrap()); + + let repo_type: RepositoryType; + + if name == "ffxiv" { + repo_type = Base; + } else { + repo_type = Expansion { + number: 0 + } + } + + let version: Option; + + if repo_type == Base { + let mut d = PathBuf::from(dir); + d.pop(); + d.pop(); + d.push("ffxivgame.ver"); + + version = read_version(d.as_path()); + } else { + let mut d = PathBuf::from(dir); + d.push(format!("{}.ver", name)); + + version = read_version(d.as_path()); + } + + if version == None { + return None; + } + + Some(Repository { + name, + repo_type, + version: version.unwrap(), + }) + } + + fn expansion(&self) -> i32 { + match self.repo_type { + Base => 0, + Expansion { number } => number + } + } + + /// Calculate an index filename for a specific category, like _"0a0000.win32.index"_. + pub fn index_filename(&self, category: Category) -> String { + format!("{:02x}{:02}{:02}.{}.index", + category as i32, self.expansion(), 0, "win32") + } + + /// Calculate a dat filename given a category and a data file id, returns something like _"0a0000.win32.dat0"_. + pub fn dat_filename(&self, category: Category, data_file_id: u32) -> String { + let expansion = self.expansion(); + let chunk = 0; + let platform = "win32"; + + format!("{:02x}{expansion:02}{chunk:02}.{platform}.dat{data_file_id}", + category as u32) + } +} \ No newline at end of file diff --git a/src/sqpack.rs b/src/sqpack.rs new file mode 100755 index 0000000..1f12a3b --- /dev/null +++ b/src/sqpack.rs @@ -0,0 +1,78 @@ +use std::io::{Read, Seek, SeekFrom}; +use crc::{Crc, CRC_32_JAMCRC}; +use crate::compression::no_header_decompress; +use crate::dat::{BlockHeader, CompressionMode}; +use binrw::BinRead; + +const JAMCRC: Crc = Crc::::new(&CRC_32_JAMCRC); + +/// Calculates a hash for `index` files from a game path. +pub fn calculate_hash(path: &str) -> u64 { + let lowercase = path.to_lowercase(); + + let pos = lowercase.rfind('/').unwrap(); + + let (directory, filename) = lowercase.split_at(pos); + + let directory_crc = JAMCRC.checksum(directory.as_bytes()); + let filename_crc = JAMCRC.checksum(filename[1..filename.len()].as_bytes()); + + (directory_crc as u64) << 32 | (filename_crc as u64) +} + +pub fn read_data_block(mut buf : T, starting_position: u64) -> Option> { + buf.seek(SeekFrom::Start(starting_position)).ok()?; + + let block_header = BlockHeader::read(&mut buf).unwrap(); + + match block_header.compression { + CompressionMode::Compressed { compressed_length, decompressed_length } => { + let mut compressed_data: Vec = vec![0; compressed_length as usize]; + buf.read_exact(&mut compressed_data).ok()?; + + let mut decompressed_data: Vec = vec![0; decompressed_length as usize]; + if !no_header_decompress(&mut compressed_data, &mut decompressed_data) { + return None; + } + + Some(decompressed_data) + } + CompressionMode::Uncompressed { file_size } => { + let mut local_data: Vec = vec![0; file_size as usize]; + buf.read_exact(&mut local_data).ok()?; + + Some(local_data) + } + } +} + +/// A fixed version of read_data_block accounting for differing compressed block sizes in ZiPatch files. +pub fn read_data_block_patch(mut buf : T) -> Option> { + let block_header = BlockHeader::read(&mut buf).unwrap(); + + match block_header.compression { + CompressionMode::Compressed { compressed_length, decompressed_length } => { + let compressed_length : usize = ((compressed_length as usize + 143) & 0xFFFFFF80) - (block_header.size as usize); + + let mut compressed_data: Vec = vec![0; compressed_length as usize]; + buf.read_exact(&mut compressed_data).ok()?; + + let mut decompressed_data: Vec = vec![0; decompressed_length as usize]; + if !no_header_decompress(&mut compressed_data, &mut decompressed_data) { + return None; + } + + Some(decompressed_data) + } + CompressionMode::Uncompressed { file_size } => { + let new_file_size : usize = ((file_size as usize + 143) & 0xFFFFFF80); + + let mut local_data: Vec = vec![0; file_size as usize]; + buf.read_exact(&mut local_data).ok()?; + + buf.seek(SeekFrom::Current((new_file_size as usize - block_header.size as usize - file_size as usize) as i64)).ok()?; + + Some(local_data) + } + } +} diff --git a/tests/integration_test.rs b/tests/integration_test.rs new file mode 100755 index 0000000..3e0710e --- /dev/null +++ b/tests/integration_test.rs @@ -0,0 +1,19 @@ +use std::env; + +#[test] +fn test_index_read() { + let game_dir = env::var("FFXIV_GAME_DIR").unwrap(); + + physis::index::IndexFile::from_existing(format!("{}/game/sqpack/ffxiv/000000.win32.index", game_dir).as_str()); +} + +#[test] +fn test_gamedata_extract() { + let game_dir = env::var("FFXIV_GAME_DIR").unwrap(); + + let mut gamedata = physis::gamedata::GameData::from_existing(format!("{}/game", game_dir).as_str()).unwrap(); + + gamedata.reload_repositories(); + + assert!(gamedata.extract("exd/root.exl").is_some()); +} \ No newline at end of file