From a204df5e11445cedb59c947500396c7ae1a25463 Mon Sep 17 00:00:00 2001 From: Daniel Hutzley Date: Wed, 17 Nov 2021 20:34:21 -0800 Subject: [PATCH 1/2] First draft of modpack module --- Cargo.lock | 311 ++++++++++++++++---------------- shell.nix | 7 + theseus/Cargo.toml | 3 +- theseus/src/lib.rs | 1 + theseus/src/modpack/manifest.rs | 230 +++++++++++++++++++++++ theseus/src/modpack/mod.rs | 68 +++++++ 6 files changed, 464 insertions(+), 156 deletions(-) create mode 100644 shell.nix create mode 100644 theseus/src/modpack/manifest.rs create mode 100644 theseus/src/modpack/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 84bfe7104..bb4915eeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,15 +31,15 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" [[package]] name = "byteorder" @@ -49,9 +49,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "bzip2" @@ -76,9 +76,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.68" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" [[package]] name = "cfg-if" @@ -102,9 +102,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a89e2ae426ea83155dccf10c0fa6b1463ef6d5fcb44cee0b224a408fa640a62" +checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" dependencies = [ "core-foundation-sys", "libc", @@ -112,9 +112,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "crc32fast" @@ -127,9 +127,9 @@ dependencies = [ [[package]] name = "daedalus" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667dec20054908ee40916a3fd8ea5bfc6ed8b1bde6f7741dbea18500a1049ea6" +checksum = "3e8ea161abae801020f48a909a7174024945e924c2f16c27fdf9264422dcf1a6" dependencies = [ "bytes", "chrono", @@ -143,18 +143,18 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.28" +version = "0.8.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" +checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" dependencies = [ "cfg-if", ] [[package]] name = "flate2" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" dependencies = [ "cfg-if", "crc32fast", @@ -194,10 +194,16 @@ dependencies = [ ] [[package]] -name = "futures" -version = "0.3.15" +name = "fs_extra" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + +[[package]] +name = "futures" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" dependencies = [ "futures-channel", "futures-core", @@ -210,9 +216,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" +checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" dependencies = [ "futures-core", "futures-sink", @@ -220,15 +226,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" +checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" [[package]] name = "futures-executor" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" +checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" dependencies = [ "futures-core", "futures-task", @@ -237,15 +243,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" +checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" [[package]] name = "futures-macro" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" +checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" dependencies = [ "autocfg", "proc-macro-hack", @@ -256,21 +262,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" +checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" [[package]] name = "futures-task" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" +checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" [[package]] name = "futures-util" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" +checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" dependencies = [ "autocfg", "futures-channel", @@ -300,9 +306,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.3" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726" +checksum = "7fd819562fcebdac5afc5c113c3ec36f902840b70fd4fc458799c8ce4607ae55" dependencies = [ "bytes", "fnv", @@ -319,9 +325,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "hermit-abi" @@ -334,9 +340,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" +checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" dependencies = [ "bytes", "fnv", @@ -345,9 +351,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" dependencies = [ "bytes", "http", @@ -356,21 +362,21 @@ dependencies = [ [[package]] name = "httparse" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" +checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" [[package]] name = "httpdate" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.9" +version = "0.14.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d6baa1b441335f3ce5098ac421fb6547c46dda735ca1bc6d0153c838f9dd83" +checksum = "436ec0091e4f20e655156a30a0df3770fe2900aa301e548e08446ec794b6953c" dependencies = [ "bytes", "futures-channel", @@ -416,9 +422,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.6.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ "autocfg", "hashbrown", @@ -426,9 +432,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.9" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", ] @@ -441,15 +447,15 @@ checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" [[package]] name = "itoa" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "js-sys" -version = "0.3.51" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" dependencies = [ "wasm-bindgen", ] @@ -462,15 +468,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.97" +version = "0.2.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219" [[package]] name = "lock_api" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" dependencies = [ "scopeguard", ] @@ -486,15 +492,15 @@ dependencies = [ [[package]] name = "matches" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "mime" @@ -514,9 +520,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" dependencies = [ "libc", "log", @@ -536,9 +542,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" dependencies = [ "lazy_static", "libc", @@ -598,9 +604,9 @@ checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" [[package]] name = "openssl" -version = "0.10.35" +version = "0.10.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "549430950c79ae24e6d02e0b7404534ecf311d94cc9f861e9e4020187d13d885" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" dependencies = [ "bitflags", "cfg-if", @@ -618,9 +624,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.65" +version = "0.9.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7907e3bfa08bb85105209cdfcb6c63d109f8f6c1ed6ca318fff5c1853fbc1d" +checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73" dependencies = [ "autocfg", "cc", @@ -631,9 +637,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", @@ -642,9 +648,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ "cfg-if", "instant", @@ -668,9 +674,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pin-project-lite" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" [[package]] name = "pin-utils" @@ -680,15 +686,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" [[package]] name = "proc-macro-hack" @@ -704,18 +710,18 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "ba508cc11742c0dc5c1659771673afbab7a0efab23aa17e854cbab0837ed0b43" dependencies = [ "unicode-xid", ] [[package]] name = "quote" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ "proc-macro2", ] @@ -762,9 +768,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] @@ -797,9 +803,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.4" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246e9f61b9bb77df069a947682be06e31ac43ea37862e244a69f177694ea6d22" +checksum = "66d2927ca2f685faf0fc620ac4834690d29e7abb153add10f5812eef20b5e280" dependencies = [ "base64", "bytes", @@ -854,9 +860,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "security-framework" -version = "2.3.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" dependencies = [ "bitflags", "core-foundation", @@ -867,9 +873,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.3.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284" +checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" dependencies = [ "core-foundation-sys", "libc", @@ -877,18 +883,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.126" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.126" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", @@ -897,9 +903,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.64" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +checksum = "063bf466a64011ac24040a49009724ee60a57da1b437617ceb32e53ad61bfb19" dependencies = [ "itoa", "ryu", @@ -935,21 +941,21 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "smallvec" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "socket2" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" dependencies = [ "libc", "winapi", @@ -957,9 +963,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.73" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +checksum = "f2afee18b8beb5a596ecb4a2dce128c719b4ba399d34126b9e4396e3f9860966" dependencies = [ "proc-macro2", "quote", @@ -968,9 +974,9 @@ dependencies = [ [[package]] name = "sys-info" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fcecee49339531cf6bd84ecf3ed94f9c8ef4a7e700f2a1cac9cc1ca485383a" +checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c" dependencies = [ "cc", "libc", @@ -997,6 +1003,7 @@ dependencies = [ "bytes", "chrono", "daedalus", + "fs_extra", "futures", "lazy_static", "path-clean", @@ -1023,18 +1030,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.25" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.25" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", @@ -1043,20 +1050,19 @@ dependencies = [ [[package]] name = "time" -version = "0.1.44" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "wasi", "winapi", ] [[package]] name = "tinyvec" -version = "1.2.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" dependencies = [ "tinyvec_macros", ] @@ -1069,9 +1075,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.7.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb2ed024293bb19f7a5dc54fe83bf86532a44c12a2bb8ba40d64a4509395ca2" +checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" dependencies = [ "autocfg", "bytes", @@ -1089,9 +1095,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.2.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" +checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" dependencies = [ "proc-macro2", "quote", @@ -1110,9 +1116,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.7" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" dependencies = [ "bytes", "futures-core", @@ -1130,9 +1136,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.26" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" dependencies = [ "cfg-if", "pin-project-lite", @@ -1141,9 +1147,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.18" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" dependencies = [ "lazy_static", ] @@ -1156,12 +1162,9 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "unicode-bidi" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" -dependencies = [ - "matches", -] +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] name = "unicode-normalization" @@ -1218,27 +1221,25 @@ dependencies = [ [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ "cfg-if", - "serde", - "serde_json", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" dependencies = [ "bumpalo", "lazy_static", @@ -1251,9 +1252,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.24" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fba7978c679d53ce2d0ac80c8c175840feb849a161664365d1287b41f2e67f1" +checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" dependencies = [ "cfg-if", "js-sys", @@ -1263,9 +1264,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1273,9 +1274,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" dependencies = [ "proc-macro2", "quote", @@ -1286,15 +1287,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.74" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" [[package]] name = "web-sys" -version = "0.3.51" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/shell.nix b/shell.nix new file mode 100644 index 000000000..a6e893830 --- /dev/null +++ b/shell.nix @@ -0,0 +1,7 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + buildInputs = with pkgs; [ + rustc cargo openssl pkg-config + ]; +} diff --git a/theseus/Cargo.toml b/theseus/Cargo.toml index a1750dcfe..d89703d58 100644 --- a/theseus/Cargo.toml +++ b/theseus/Cargo.toml @@ -20,6 +20,7 @@ bytes = "1" zip = "0.5" sha1 = { version = "0.6.0", features = ["std"]} path-clean = "0.1.0" +fs_extra = "1.2.0" regex = "1.5" lazy_static = "1.4" @@ -27,4 +28,4 @@ lazy_static = "1.4" tokio = { version = "1", features = ["full"] } futures = "0.3" -sys-info = "0.9.0" \ No newline at end of file +sys-info = "0.9.0" diff --git a/theseus/src/lib.rs b/theseus/src/lib.rs index dbf0e2036..1226d2fc1 100644 --- a/theseus/src/lib.rs +++ b/theseus/src/lib.rs @@ -6,4 +6,5 @@ #![warn(missing_docs, unused_import_braces, missing_debug_implementations)] pub mod launcher; +pub mod modpack; mod util; diff --git a/theseus/src/modpack/manifest.rs b/theseus/src/modpack/manifest.rs new file mode 100644 index 000000000..0d7312fda --- /dev/null +++ b/theseus/src/modpack/manifest.rs @@ -0,0 +1,230 @@ +use std::{convert::TryFrom, path::{Path, PathBuf}, str::FromStr}; + +use daedalus::download_file_mirrors; +use tokio::fs; +use futures::future; + +use crate::launcher::ModLoader; + +use super::ModpackError; + +pub struct Manifest { + format_version: u64, + game: ModpackGame, + version_id: String, + + name: String, + summary: Option, + + files: Vec, +} + +impl Manifest { + /// Download a modpack's files for a given side to a given destination + /// Assumes the destination exists and is a directory + pub async fn download_files(&self, dest: &Path, side: &ModpackSide) -> Result<(), ModpackError> { + let handles = self.files.clone().into_iter() + .map(move |file| { + let (dest, side) = (dest.to_owned(), *side); + tokio::spawn(async move { + file.fetch(&dest, &side).await + }) + }); + future::try_join_all(handles).await?.into_iter().collect::>()?; + + // TODO Integrate instance format to save other metadata + Ok(()) + } +} + +fn try_get<'r, F, T>(manifest: &'r serde_json::Map, field: &str, caster: F) -> Result + where + F: Fn(&'r serde_json::Value) -> Option { + manifest.get(field) + .and_then(caster) + .ok_or(ModpackError::ManifestError(format!("Invalid or missing field: {}", field))) +} + +impl TryFrom for Manifest { + type Error = ModpackError; + + fn try_from(value: serde_json::Value) -> Result { + use ModpackError::ManifestError; + + let value = value.as_object() + .ok_or(ManifestError(String::from("Manifest is not a JSON object!")))?; + + let game = ModpackGame::new( + try_get(value, "game", serde_json::Value::as_str)?, + try_get(value, "dependencies", serde_json::Value::as_object)?, + )?; + + let files = try_get(value, "files", serde_json::Value::as_array)? + .iter().map(|it| -> Result { + let file = it.as_object() + .ok_or(ManifestError(String::from("Malformed file: not an object")))?; + + let path = Path::new(try_get(file, "path", serde_json::Value::as_str)?); + let hashes = ModpackFileHashes::try_from(try_get(file, "hashes", serde_json::Value::as_object)?)?; + let sources = try_get(file, "sources", serde_json::Value::as_array)?.iter() + .map(serde_json::Value::as_str) + .map(|it| it.map(String::from)) + .collect::>>() + .ok_or(ManifestError(format!("Invalid source for path {}", path.to_str().unwrap_or("?"))))?; + let env: Option<[ModpackEnv;2]> = if let Some(env) = file.get("env") { + if !env.is_object() { + return Err(ManifestError(String::from("Env is provided, but is not an object!"))); + } + Some([ModpackEnv::from_str(env.get("client").and_then(serde_json::Value::as_str).unwrap_or_default())?, + ModpackEnv::from_str(env.get("server").and_then(serde_json::Value::as_str).unwrap_or_default())?]) + } else { + None + }; + + ModpackFile::new(path, hashes, env, sources.as_slice()) + } + ).collect::, ModpackError>>()?; + + Ok(Self { + format_version: try_get(value, "formatVersion", serde_json::Value::as_u64)?, + game, + version_id: String::from(try_get(value, "versionId", serde_json::Value::as_str)?), + name: String::from(try_get(value, "name", serde_json::Value::as_str)?), + summary: value.get("summary").and_then(serde_json::Value::as_str).map(String::from), + files + }) + } +} + +pub enum ModpackGame { + // TODO: Currently, the launcher does not support specifying mod loader versions, so I just + // store the loader here. + Minecraft(String, ModLoader), +} + +impl ModpackGame { + pub fn new(game: &str, deps: &serde_json::Map) -> Result { + match game { + "minecraft" => { + let game_version = String::from( + deps.get("minecraft") + .ok_or(ModpackError::ManifestError(String::from( + "No version of minecraft given", + )))? + .as_str() + .unwrap(), + ); + + // TODO: See comment in ModpackGame, this code was designed specifically to be + // easily adapted for versioned modloaders + let loader = if let Some(_) = deps.get("fabric-loader") { + ModLoader::Fabric + } else if let Some(_) = deps.get("forge") { + ModLoader::Forge + } else { + ModLoader::Vanilla + }; + + Ok(ModpackGame::Minecraft(game_version, loader)) + } + _ => Err(ModpackError::ManifestError(format!( + "Invalid game: {}", + game + ))), + } + } +} + +#[derive(Clone)] +pub struct ModpackFile { + path: PathBuf, + hashes: ModpackFileHashes, + envs: Option<[ModpackEnv; 2]>, + sources: Vec, +} + +#[derive(Clone, Copy)] +pub enum ModpackSide { + Client = 0, + Server = 1, +} + +impl ModpackFile { + pub fn new( + path: &Path, + hashes: ModpackFileHashes, + envs: Option<[ModpackEnv;2]>, + sources: &[String], + ) -> Result { + if !path.is_dir() { + return Err(ModpackError::ManifestError(format!("Modpack file {} is a directory!", path.to_str().unwrap_or("?")))); + } + + Ok(Self { + path: PathBuf::from(path), + hashes, + envs, + sources: Vec::from(sources), + }) + } + + pub async fn fetch(&self, dest: &Path, side: &ModpackSide) -> Result<(), ModpackError> { + if let Some(envs) = &self.envs { + if envs[*side as usize] == ModpackEnv::Unsupported + || envs[(*side as usize + 1) % 2] == ModpackEnv::Required + { + return Ok(()); + } + } + + let output = dest.join(&self.path); + + // HACK: Since Daedalus appends a file name to all mirrors and the manifest supplies full + // URLs, I'm supplying it with an empty string to avoid reinventing the wheel. + let bytes = download_file_mirrors("", &self.sources.iter().map(|it| it.as_str()).collect::>().as_slice(), Some(&self.hashes.sha1)).await?; + fs::create_dir_all(output.parent().unwrap()).await?; + fs::write(output, bytes).await?; + Ok(()) + } +} + +#[derive(Clone)] +pub struct ModpackFileHashes { + sha1: String, +} + +impl TryFrom<&serde_json::Map> for ModpackFileHashes { + type Error = ModpackError; + + fn try_from(value: &serde_json::Map) -> Result { + let sha1 = String::from(try_get(&value, "sha1", serde_json::Value::as_str)?); + Ok(Self { sha1 }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ModpackEnv { + Required, + Optional, + Unsupported, +} + +impl FromStr for ModpackEnv { + type Err = ModpackError; + + fn from_str(s: &str) -> Result { + use ModpackEnv::*; + match s { + "required" => Ok(Required), + "optional" => Ok(Optional), + "unsupported" => Ok(Unsupported), + _ => Err(ModpackError::ManifestError(format!("Invalid environment support: {}", s))), + } + } +} + +impl Default for ModpackEnv { + fn default() -> Self { + Self::Optional + } +} diff --git a/theseus/src/modpack/mod.rs b/theseus/src/modpack/mod.rs new file mode 100644 index 000000000..a31881755 --- /dev/null +++ b/theseus/src/modpack/mod.rs @@ -0,0 +1,68 @@ +//! Provides utilties for downloading and parsing modpacks + +use std::{convert::TryFrom, io, path::Path}; +use fs_extra::dir::CopyOptions; +use tokio::fs; + +use self::manifest::Manifest; + +mod manifest; + +pub const MANIFEST_PATH: &'static str = "index.json"; +pub const OVERRIDES_PATH: &'static str = "overrides/"; + +#[derive(thiserror::Error, Debug)] +pub enum ModpackError { + #[error("I/O error while reading modpack: {0}")] + IOError(#[from] io::Error), + + #[error("I/O error while reading modpack: {0}")] + FSExtraError(#[from] fs_extra::error::Error), + + #[error("Invalid output directory: {0}")] + InvalidDirectory(String), + + #[error("Error parsing manifest: {0}")] + ManifestError(String), + + #[error("Daedalus error: {0}")] + DaedalusError(#[from] daedalus::Error), + + #[error("Error parsing json: {0}")] + JsonError(#[from] serde_json::Error), + + #[error("Error joining futures: {0}")] + JoinError(#[from] tokio::task::JoinError), +} + +/// Realise a given modpack into an instance +pub async fn realise_modpack(dir: &Path, dest: &Path, side: &manifest::ModpackSide) -> Result<(), ModpackError> { + if dest.is_file() { + return Err(ModpackError::InvalidDirectory(String::from("Output is not a directory"))); + } + if dest.exists() && std::fs::read_dir(dest).map_or(false, |it| it.count() != 0) { + return Err(ModpackError::InvalidDirectory(String::from("Output directory is non-empty"))); + } + if !dest.exists() { + fs::create_dir_all(dest).await?; + } + + // Copy overrides + let overrides = Some(dir.join(OVERRIDES_PATH)).filter(|it| it.exists() && it.is_dir()); + if let Some(overrides) = overrides { + fs_extra::dir::copy(overrides, dest, &CopyOptions::new())?; + } + + // Parse manifest + // NOTE: I'm using standard files here, since Serde does not support async readers + let manifest_path = Some(dir.join(MANIFEST_PATH)) + .filter(|it| it.exists() && it.is_file()) + .ok_or(ModpackError::ManifestError(String::from("Manifest missing or is not a file")))?; + let manifest_file = std::fs::File::open(manifest_path)?; + let manifest_json: serde_json::Value = serde_json::from_reader(io::BufReader::new(manifest_file))?; + let manifest = Manifest::try_from(manifest_json)?; + + // Realise manifest + manifest.download_files(dest, side).await?; + Ok(()) +} From 52ed070b5b14b994f54103fb1348fd7b1b3ae313 Mon Sep 17 00:00:00 2001 From: Daniel Hutzley Date: Sat, 20 Nov 2021 11:56:44 -0800 Subject: [PATCH 2/2] Add tests and example for modpack support --- Cargo.lock | 45 ++++ theseus/Cargo.toml | 6 + theseus/examples/download-pack.rs | 53 +++++ theseus/src/modpack/manifest.rs | 358 +++++++++++++++++++++++++----- theseus/src/modpack/mod.rs | 57 ++++- 5 files changed, 459 insertions(+), 60 deletions(-) create mode 100644 theseus/examples/download-pack.rs diff --git a/Cargo.lock b/Cargo.lock index bb4915eeb..ff5ce47f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,35 @@ dependencies = [ "memchr", ] +[[package]] +name = "argh" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f023c76cd7975f9969f8e29f0e461decbdc7f51048ce43427107a3d192f1c9bf" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ad219abc0c06ca788aface2e3a1970587e3413ab70acd20e54b6ec524c1f8f" +dependencies = [ + "argh_shared", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38de00daab4eac7d753e97697066238d67ce9d7e2d823ab4f72fe14af29f3f33" + [[package]] name = "autocfg" version = "1.0.1" @@ -329,6 +358,15 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1000,6 +1038,7 @@ dependencies = [ name = "theseus" version = "0.1.0" dependencies = [ + "argh", "bytes", "chrono", "daedalus", @@ -1175,6 +1214,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" + [[package]] name = "unicode-xid" version = "0.2.2" diff --git a/theseus/Cargo.toml b/theseus/Cargo.toml index d89703d58..b6a71ea89 100644 --- a/theseus/Cargo.toml +++ b/theseus/Cargo.toml @@ -29,3 +29,9 @@ tokio = { version = "1", features = ["full"] } futures = "0.3" sys-info = "0.9.0" + +[dev-dependencies] +argh = "0.1.6" + +[[example]] +name = "download-pack" diff --git a/theseus/examples/download-pack.rs b/theseus/examples/download-pack.rs new file mode 100644 index 000000000..e41f15fe7 --- /dev/null +++ b/theseus/examples/download-pack.rs @@ -0,0 +1,53 @@ +use std::{path::PathBuf, time::Instant}; + +use argh::FromArgs; +use theseus::modpack::{fetch_modpack, manifest::ModpackSide}; + +#[derive(FromArgs)] +/// Simple modpack download +struct ModpackDownloader { + /// where to download to + #[argh(positional)] + url: String, + + /// where to put the resulting pack + #[argh(option, short = 'o')] + output: Option, + + /// the sha1 hash, if you want it checked + #[argh(option, short = 'c')] + hash: Option, + + /// use verbose logging + #[argh(switch, short = 'v')] + verbose: bool, +} + +// Simple logging helper +fn debug(msg: &str, verbose: bool) { + if verbose { + println!("{}", msg); + } +} + +#[tokio::main] +pub async fn main() { + let args = argh::from_env::(); + let dest = args.output.unwrap_or(PathBuf::from("./pack-download/")); + + debug( + &format!( + "Downloading pack {} to {}", + args.url, + dest.to_str().unwrap_or("?") + ), + args.verbose, + ); + + let start = Instant::now(); + fetch_modpack(&args.url, args.hash.as_deref(), &dest, ModpackSide::Client).await; + let end = start.elapsed(); + + println!("Download completed in {} seconds", end.as_secs_f32()); + debug("Done!", args.verbose); +} diff --git a/theseus/src/modpack/manifest.rs b/theseus/src/modpack/manifest.rs index 0d7312fda..448311bfa 100644 --- a/theseus/src/modpack/manifest.rs +++ b/theseus/src/modpack/manifest.rs @@ -1,13 +1,18 @@ -use std::{convert::TryFrom, path::{Path, PathBuf}, str::FromStr}; +use std::{ + convert::TryFrom, + path::{Path, PathBuf}, + str::FromStr, +}; use daedalus::download_file_mirrors; -use tokio::fs; use futures::future; +use tokio::fs; use crate::launcher::ModLoader; use super::ModpackError; +#[derive(Debug, Clone, PartialEq)] pub struct Manifest { format_version: u64, game: ModpackGame, @@ -22,27 +27,36 @@ pub struct Manifest { impl Manifest { /// Download a modpack's files for a given side to a given destination /// Assumes the destination exists and is a directory - pub async fn download_files(&self, dest: &Path, side: &ModpackSide) -> Result<(), ModpackError> { - let handles = self.files.clone().into_iter() - .map(move |file| { - let (dest, side) = (dest.to_owned(), *side); - tokio::spawn(async move { - file.fetch(&dest, &side).await - }) - }); - future::try_join_all(handles).await?.into_iter().collect::>()?; + pub async fn download_files(&self, dest: &Path, side: ModpackSide) -> Result<(), ModpackError> { + let handles = self.files.clone().into_iter().map(move |file| { + let (dest, side) = (dest.to_owned(), side); + tokio::spawn(async move { file.fetch(&dest, side).await }) + }); + future::try_join_all(handles) + .await? + .into_iter() + .collect::>()?; // TODO Integrate instance format to save other metadata Ok(()) } } -fn try_get<'r, F, T>(manifest: &'r serde_json::Map, field: &str, caster: F) -> Result - where - F: Fn(&'r serde_json::Value) -> Option { - manifest.get(field) +fn try_get<'r, F, T>( + manifest: &'r serde_json::Map, + field: &str, + caster: F, +) -> Result +where + F: Fn(&'r serde_json::Value) -> Option, +{ + manifest + .get(field) .and_then(caster) - .ok_or(ModpackError::ManifestError(format!("Invalid or missing field: {}", field))) + .ok_or(ModpackError::ManifestError(format!( + "Invalid or missing field: {}", + field + ))) } impl TryFrom for Manifest { @@ -51,8 +65,9 @@ impl TryFrom for Manifest { fn try_from(value: serde_json::Value) -> Result { use ModpackError::ManifestError; - let value = value.as_object() - .ok_or(ManifestError(String::from("Manifest is not a JSON object!")))?; + let value = value.as_object().ok_or(ManifestError(String::from( + "Manifest is not a JSON object!", + )))?; let game = ModpackGame::new( try_get(value, "game", serde_json::Value::as_str)?, @@ -60,42 +75,68 @@ impl TryFrom for Manifest { )?; let files = try_get(value, "files", serde_json::Value::as_array)? - .iter().map(|it| -> Result { - let file = it.as_object() + .iter() + .map(|it| -> Result { + let file = it + .as_object() .ok_or(ManifestError(String::from("Malformed file: not an object")))?; let path = Path::new(try_get(file, "path", serde_json::Value::as_str)?); - let hashes = ModpackFileHashes::try_from(try_get(file, "hashes", serde_json::Value::as_object)?)?; - let sources = try_get(file, "sources", serde_json::Value::as_array)?.iter() + let hashes = ModpackFileHashes::try_from(try_get( + file, + "hashes", + serde_json::Value::as_object, + )?)?; + let downloads = try_get(file, "downloads", serde_json::Value::as_array)? + .iter() .map(serde_json::Value::as_str) .map(|it| it.map(String::from)) .collect::>>() - .ok_or(ManifestError(format!("Invalid source for path {}", path.to_str().unwrap_or("?"))))?; - let env: Option<[ModpackEnv;2]> = if let Some(env) = file.get("env") { - if !env.is_object() { - return Err(ManifestError(String::from("Env is provided, but is not an object!"))); - } - Some([ModpackEnv::from_str(env.get("client").and_then(serde_json::Value::as_str).unwrap_or_default())?, - ModpackEnv::from_str(env.get("server").and_then(serde_json::Value::as_str).unwrap_or_default())?]) - } else { - None - }; + .ok_or(ManifestError(format!( + "Invalid source for path {}", + path.to_str().unwrap_or("?") + )))?; + let env: Option<[ModpackEnv; 2]> = if let Some(env) = file.get("env") { + if !env.is_object() { + return Err(ManifestError(String::from( + "Env is provided, but is not an object!", + ))); + } + Some([ + ModpackEnv::from_str( + env.get("client") + .and_then(serde_json::Value::as_str) + .unwrap_or_default(), + )?, + ModpackEnv::from_str( + env.get("server") + .and_then(serde_json::Value::as_str) + .unwrap_or_default(), + )?, + ]) + } else { + None + }; - ModpackFile::new(path, hashes, env, sources.as_slice()) - } - ).collect::, ModpackError>>()?; + ModpackFile::new(path, hashes, env, &downloads) + }) + .collect::, ModpackError>>()?; Ok(Self { format_version: try_get(value, "formatVersion", serde_json::Value::as_u64)?, game, version_id: String::from(try_get(value, "versionId", serde_json::Value::as_str)?), name: String::from(try_get(value, "name", serde_json::Value::as_str)?), - summary: value.get("summary").and_then(serde_json::Value::as_str).map(String::from), - files + summary: value + .get("summary") + .and_then(serde_json::Value::as_str) + .map(String::from), + files, }) } } +#[derive(Debug, Clone, PartialEq)] pub enum ModpackGame { // TODO: Currently, the launcher does not support specifying mod loader versions, so I just // store the loader here. @@ -103,7 +144,10 @@ pub enum ModpackGame { } impl ModpackGame { - pub fn new(game: &str, deps: &serde_json::Map) -> Result { + pub fn new( + game: &str, + deps: &serde_json::Map, + ) -> Result { match game { "minecraft" => { let game_version = String::from( @@ -135,15 +179,15 @@ impl ModpackGame { } } -#[derive(Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct ModpackFile { path: PathBuf, hashes: ModpackFileHashes, envs: Option<[ModpackEnv; 2]>, - sources: Vec, + downloads: Vec, } -#[derive(Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum ModpackSide { Client = 0, Server = 1, @@ -153,25 +197,28 @@ impl ModpackFile { pub fn new( path: &Path, hashes: ModpackFileHashes, - envs: Option<[ModpackEnv;2]>, - sources: &[String], + envs: Option<[ModpackEnv; 2]>, + downloads: &[String], ) -> Result { - if !path.is_dir() { - return Err(ModpackError::ManifestError(format!("Modpack file {} is a directory!", path.to_str().unwrap_or("?")))); + if path.is_dir() { + return Err(ModpackError::ManifestError(format!( + "Modpack file {} is a directory!", + path.to_str().unwrap_or("?") + ))); } Ok(Self { path: PathBuf::from(path), hashes, envs, - sources: Vec::from(sources), + downloads: Vec::from(downloads), }) } - pub async fn fetch(&self, dest: &Path, side: &ModpackSide) -> Result<(), ModpackError> { + pub async fn fetch(&self, dest: &Path, side: ModpackSide) -> Result<(), ModpackError> { if let Some(envs) = &self.envs { - if envs[*side as usize] == ModpackEnv::Unsupported - || envs[(*side as usize + 1) % 2] == ModpackEnv::Required + if envs[side as usize] == ModpackEnv::Unsupported + || envs[(side as usize + 1) % 2] == ModpackEnv::Required { return Ok(()); } @@ -181,14 +228,24 @@ impl ModpackFile { // HACK: Since Daedalus appends a file name to all mirrors and the manifest supplies full // URLs, I'm supplying it with an empty string to avoid reinventing the wheel. - let bytes = download_file_mirrors("", &self.sources.iter().map(|it| it.as_str()).collect::>().as_slice(), Some(&self.hashes.sha1)).await?; + let bytes = download_file_mirrors( + "", + &self + .downloads + .iter() + .map(|it| it.as_str()) + .collect::>() + .as_slice(), + Some(&self.hashes.sha1), + ) + .await?; fs::create_dir_all(output.parent().unwrap()).await?; fs::write(output, bytes).await?; Ok(()) } } -#[derive(Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct ModpackFileHashes { sha1: String, } @@ -202,7 +259,7 @@ impl TryFrom<&serde_json::Map> for ModpackFileHashes } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum ModpackEnv { Required, Optional, @@ -218,7 +275,10 @@ impl FromStr for ModpackEnv { "required" => Ok(Required), "optional" => Ok(Optional), "unsupported" => Ok(Unsupported), - _ => Err(ModpackError::ManifestError(format!("Invalid environment support: {}", s))), + _ => Err(ModpackError::ManifestError(format!( + "Invalid environment support: {}", + s + ))), } } } @@ -228,3 +288,195 @@ impl Default for ModpackEnv { Self::Optional } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_simple() -> Result<(), ModpackError> { + const PACK_JSON: &'static str = r#" + { + "formatVersion": 1, + "game": "minecraft", + "versionId": "deadbeef", + "name": "Example Pack", + "files": [], + "dependencies": { + "minecraft": "1.17.1" + } + } + "#; + let expected_manifest = Manifest { + format_version: 1, + game: ModpackGame::Minecraft(String::from("1.17.1"), ModLoader::Vanilla), + version_id: String::from("deadbeef"), + name: String::from("Example Pack"), + summary: None, + files: Vec::new(), + }; + let manifest_json: serde_json::Value = + serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON"); + let manifest = Manifest::try_from(manifest_json)?; + + assert_eq!(expected_manifest, manifest); + Ok(()) + } + + #[test] + fn parse_forge() -> Result<(), ModpackError> { + const PACK_JSON: &'static str = r#" + { + "formatVersion": 1, + "game": "minecraft", + "versionId": "deadbeef", + "name": "Example Pack", + "files": [ + { + "path": "mods/testmod.jar", + "hashes": { + "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "downloads": [ + "https://example.com/testmod.jar" + ] + } + ], + "dependencies": { + "minecraft": "1.17.1", + "forge": "37.0.110" + } + } + "#; + + let expected_manifest = Manifest { + format_version: 1, + game: ModpackGame::Minecraft(String::from("1.17.1"), ModLoader::Forge), + version_id: String::from("deadbeef"), + name: String::from("Example Pack"), + summary: None, + files: vec![ModpackFile::new( + Path::new("mods/testmod.jar"), + ModpackFileHashes { + sha1: String::from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + }, + None, + &[String::from("https://example.com/testmod.jar")], + )?], + }; + + let manifest_json: serde_json::Value = + serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON"); + let manifest = Manifest::try_from(manifest_json)?; + + assert_eq!(expected_manifest, manifest); + Ok(()) + } + + #[test] + fn parse_fabric() -> Result<(), ModpackError> { + const PACK_JSON: &'static str = r#" + { + "formatVersion": 1, + "game": "minecraft", + "versionId": "deadbeef", + "name": "Example Pack", + "files": [ + { + "path": "mods/testmod.jar", + "hashes": { + "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "downloads": [ + "https://example.com/testmod.jar" + ] + } + ], + "dependencies": { + "minecraft": "1.17.1", + "fabric-loader": "0.9.0" + } + } + "#; + + let expected_manifest = Manifest { + format_version: 1, + game: ModpackGame::Minecraft(String::from("1.17.1"), ModLoader::Fabric), + version_id: String::from("deadbeef"), + name: String::from("Example Pack"), + summary: None, + files: vec![ModpackFile::new( + Path::new("mods/testmod.jar"), + ModpackFileHashes { + sha1: String::from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + }, + None, + &[String::from("https://example.com/testmod.jar")], + )?], + }; + + let manifest_json: serde_json::Value = + serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON"); + let manifest = Manifest::try_from(manifest_json)?; + + assert_eq!(expected_manifest, manifest); + Ok(()) + } + + #[test] + fn parse_complete() -> Result<(), ModpackError> { + const PACK_JSON: &'static str = r#" + { + "formatVersion": 1, + "game": "minecraft", + "versionId": "deadbeef", + "name": "Example Pack", + "summary": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + "files": [ + { + "path": "mods/testmod.jar", + "hashes": { + "sha1": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "env": { + "client": "required", + "server": "unsupported" + }, + "downloads": [ + "https://example.com/testmod.jar" + ] + } + ], + "dependencies": { + "minecraft": "1.17.1", + "forge": "37.0.110" + } + } + "#; + + let expected_manifest = Manifest { + format_version: 1, + game: ModpackGame::Minecraft(String::from("1.17.1"), ModLoader::Forge), + version_id: String::from("deadbeef"), + name: String::from("Example Pack"), + summary: Some(String::from("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")), + files: vec![ + ModpackFile::new( + Path::new("mods/testmod.jar"), + ModpackFileHashes { + sha1: String::from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + }, + Some([ ModpackEnv::Required, ModpackEnv::Unsupported ]), + &[ String::from("https://example.com/testmod.jar") ], + )? + ], + }; + + let manifest_json: serde_json::Value = + serde_json::from_str(PACK_JSON).expect("Error parsing pack JSON"); + let manifest = Manifest::try_from(manifest_json)?; + + assert_eq!(expected_manifest, manifest); + Ok(()) + } +} diff --git a/theseus/src/modpack/mod.rs b/theseus/src/modpack/mod.rs index a31881755..f463fc921 100644 --- a/theseus/src/modpack/mod.rs +++ b/theseus/src/modpack/mod.rs @@ -1,12 +1,15 @@ //! Provides utilties for downloading and parsing modpacks -use std::{convert::TryFrom, io, path::Path}; +use daedalus::download_file; use fs_extra::dir::CopyOptions; +use std::{borrow::Borrow, convert::TryFrom, env, io, path::Path}; use tokio::fs; +use uuid::Uuid; +use zip::ZipArchive; use self::manifest::Manifest; -mod manifest; +pub mod manifest; pub const MANIFEST_PATH: &'static str = "index.json"; pub const OVERRIDES_PATH: &'static str = "overrides/"; @@ -19,6 +22,12 @@ pub enum ModpackError { #[error("I/O error while reading modpack: {0}")] FSExtraError(#[from] fs_extra::error::Error), + #[error("Error extracting archive: {0}")] + ZipError(#[from] zip::result::ZipError), + + #[error("Invalid modpack format: {0}")] + FormatError(String), + #[error("Invalid output directory: {0}")] InvalidDirectory(String), @@ -35,13 +44,44 @@ pub enum ModpackError { JoinError(#[from] tokio::task::JoinError), } +/// Realise a modpack from a given URL +pub async fn fetch_modpack( + url: &str, + sha1: Option<&str>, + dest: &Path, + side: manifest::ModpackSide, +) -> Result<(), ModpackError> { + let bytes = download_file(url, sha1).await?; + let mut archive = ZipArchive::new(io::Cursor::new(&bytes as &[u8]))?; + realise_modpack_zip(&mut archive, dest, side).await +} + +/// Realise a given modpack from a zip archive +pub async fn realise_modpack_zip( + archive: &mut ZipArchive, + dest: &Path, + side: manifest::ModpackSide, +) -> Result<(), ModpackError> { + let tmp = env::temp_dir().join(format!("theseus-{}/", Uuid::new_v4())); + archive.extract(&tmp)?; + realise_modpack(&tmp, dest, side).await +} + /// Realise a given modpack into an instance -pub async fn realise_modpack(dir: &Path, dest: &Path, side: &manifest::ModpackSide) -> Result<(), ModpackError> { +pub async fn realise_modpack( + dir: &Path, + dest: &Path, + side: manifest::ModpackSide, +) -> Result<(), ModpackError> { if dest.is_file() { - return Err(ModpackError::InvalidDirectory(String::from("Output is not a directory"))); + return Err(ModpackError::InvalidDirectory(String::from( + "Output is not a directory", + ))); } if dest.exists() && std::fs::read_dir(dest).map_or(false, |it| it.count() != 0) { - return Err(ModpackError::InvalidDirectory(String::from("Output directory is non-empty"))); + return Err(ModpackError::InvalidDirectory(String::from( + "Output directory is non-empty", + ))); } if !dest.exists() { fs::create_dir_all(dest).await?; @@ -57,9 +97,12 @@ pub async fn realise_modpack(dir: &Path, dest: &Path, side: &manifest::ModpackSi // NOTE: I'm using standard files here, since Serde does not support async readers let manifest_path = Some(dir.join(MANIFEST_PATH)) .filter(|it| it.exists() && it.is_file()) - .ok_or(ModpackError::ManifestError(String::from("Manifest missing or is not a file")))?; + .ok_or(ModpackError::ManifestError(String::from( + "Manifest missing or is not a file", + )))?; let manifest_file = std::fs::File::open(manifest_path)?; - let manifest_json: serde_json::Value = serde_json::from_reader(io::BufReader::new(manifest_file))?; + let manifest_json: serde_json::Value = + serde_json::from_reader(io::BufReader::new(manifest_file))?; let manifest = Manifest::try_from(manifest_json)?; // Realise manifest