├── .cargo └── config.toml ├── .github ├── dependabot.yml └── workflows │ └── docker-build-push.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── HACKING.md ├── LICENSE ├── README.md ├── README.tpl ├── USAGE ├── USAGE-fdroid.md ├── USAGE-google-play.md ├── build-remote.sh ├── build.rs ├── logo.png └── src ├── cli.rs ├── config.rs ├── consts.rs ├── download_sources ├── apkpure.rs ├── fdroid.rs ├── fdroid │ └── error.rs ├── google_play.rs ├── huawei_app_gallery.rs └── mod.rs ├── main.rs └── util ├── mod.rs └── progress_bar.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.armv7-unknown-linux-gnueabihf] 2 | linker = "arm-linux-gnueabihf-gcc" 3 | 4 | [target.i686-unknown-linux-gnu] 5 | linker = "i686-linux-gnu-gcc" 6 | 7 | [target.aarch64-unknown-linux-gnu] 8 | linker = "aarch64-linux-gnu-gcc" 9 | 10 | [target.aarch64-linux-android] 11 | linker = "aarch64-linux-android26-clang" 12 | rustflags = ["-C", "link-args=-Wl,-rpath,/data/data/com.termux/files/usr/lib"] 13 | 14 | [target.armv7-linux-androideabi] 15 | linker = "armv7a-linux-androideabi26-clang" 16 | rustflags = ["-C", "link-args=-Wl,-rpath,/data/data/com.termux/files/usr/lib"] 17 | 18 | [target.i686-linux-android] 19 | linker = "i686-linux-android26-clang" 20 | rustflags = ["-C", "link-args=-Wl,-rpath,/data/data/com.termux/files/usr/lib"] 21 | 22 | [target.x86_64-linux-android] 23 | linker = "x86_64-linux-android26-clang" 24 | rustflags = ["-C", "link-args=-Wl,-rpath,/data/data/com.termux/files/usr/lib"] 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for cargo 4 | - package-ecosystem: "cargo" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | assignees: 9 | - "hainish" 10 | commit-message: 11 | prefix: "cargo-update" 12 | labels: 13 | - "dependencies" 14 | pull-request-branch-name: 15 | separator: "-" 16 | reviewers: 17 | - "hainish" 18 | versioning-strategy: auto 19 | -------------------------------------------------------------------------------- /.github/workflows/docker-build-push.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: docker-build-push 3 | 4 | on: 5 | push: 6 | branches: 7 | - 'master' 8 | release: 9 | types: 10 | - 'published' 11 | 12 | env: 13 | REGISTRY: ghcr.io 14 | IMAGE_NAME: ${{ github.repository }} 15 | 16 | jobs: 17 | build-and-push-image: 18 | runs-on: ubuntu-latest 19 | permissions: 20 | contents: read 21 | packages: write 22 | 23 | steps: 24 | - name: Checkout repository 25 | uses: actions/checkout@v2 26 | 27 | - name: Log in to the Container registry 28 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 29 | with: 30 | registry: ${{ env.REGISTRY }} 31 | username: ${{ github.actor }} 32 | password: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Prepare lowercase variable 35 | run: | 36 | echo IMAGE_REPOSITORY=$(echo ${{ github.repository }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV 37 | 38 | - name: Build and push Docker image (development) 39 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 40 | with: 41 | context: . 42 | push: true 43 | tags: | 44 | ${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}:edge 45 | 46 | - name: Build and push Docker image (latest + tag) 47 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 48 | if: ${{ github.event_name == 'release' }} 49 | with: 50 | context: . 51 | push: true 52 | tags: | 53 | ${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}:latest 54 | ${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}:${{ github.event.release.tag_name }} 55 | 56 | - name: Build and push Docker image (stable) 57 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 58 | if: ${{ github.event_name == 'release' && github.event.release.prerelease == false }} 59 | with: 60 | context: . 61 | push: true 62 | tags: | 63 | ${{ env.REGISTRY }}/${{ env.IMAGE_REPOSITORY }}:stable 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | /target 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | - Adding the ability to specify json as an output format when listing versions of an app available 9 | - Using latest `gpapi` dependency, with more device configuration options for Google Play 10 | - Update dependencies 11 | 12 | ## [0.17.0] - 2024-10-11 13 | - Added support for F-Droid entry point specification and new index versions 14 | - Update dependencies 15 | 16 | ## [0.16.0] - 2024-04-04 17 | - Support Google Play API v3 and document workflow for downloading via Google Play 18 | 19 | ## [0.15.0] - 2023-01-04 20 | - Add progress bars to all download sources 21 | - Update dependencies 22 | 23 | ## [0.14.1] - 2022-11-23 24 | - Bugfix release: updating `zstd-sys` dependency, which fixes cross-compilation for Windows 25 | 26 | ## [0.14.0] - 2022-11-21 27 | - Downloading split APKs downloads the base APK as well 28 | - Switch to OpenSSL 3.0.7 29 | - Update dependencies 30 | 31 | ## [0.13.0] - 2022-05-26 32 | - Add support for downloading split APKs with `google-play` 33 | - Add support for downloading additional files with `google-play` 34 | - Use the appropriate filename extensions (`xapk` or `apk`) for `apkpure` 35 | 36 | ## [0.12.2] - 2022-05-18 37 | - Android-only release: switch to OpenSSL 3.0.3 for `termux` releases 38 | 39 | ## [0.12.1] - 2022-05-11 40 | - Android-only release: fix dependencies to ensure `openssl-1.1` is used 41 | 42 | ## [0.12.0] - 2022-05-05 43 | - Add a default config file which allows users to store Google credentials 44 | - Allow specifying a custom path to the above config file 45 | - Prompt users for a Google username and password if none is found 46 | 47 | ## [0.11.0] - 2022-04-22 48 | - Adding `huawei-app-gallery` as a download source 49 | 50 | ## [0.10.0] - 2022-03-17 51 | ### Added 52 | - `options` command-line option to specify options specific to a download source 53 | - With `options`, adding ability to download from a specific F-Droid repo or mirror 54 | - With `options`, adding ability to specify a device configuration and additional options for Google Play 55 | - Documenting `options` 56 | 57 | ## [0.9.0] - 2022-02-22 58 | - Fix bug where another package is fetched for certain ids in APKPure 59 | - Updated usage for download sources 60 | - Dependency updates 61 | 62 | ## [0.8.0] - 2021-12-07 63 | ### Added 64 | - Cacheing of F-Droid package index to local config directory 65 | 66 | ## [0.7.0] - 2021-11-29 67 | ### Added 68 | - Ability to download versioned apps on APKPure and F-Droid, as well as making it possible in a separate field if you're using a CSV 69 | - Ability to look up what versions are available on these sources using the -l flag 70 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | This project is governed by [EFF's Public Projects Code of Conduct](https://www.eff.org/pages/eppcode). 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler2" 16 | version = "2.0.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 19 | 20 | [[package]] 21 | name = "aes" 22 | version = "0.8.4" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" 25 | dependencies = [ 26 | "cfg-if", 27 | "cipher", 28 | "cpufeatures", 29 | ] 30 | 31 | [[package]] 32 | name = "aho-corasick" 33 | version = "1.1.3" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 36 | dependencies = [ 37 | "memchr", 38 | ] 39 | 40 | [[package]] 41 | name = "android-tzdata" 42 | version = "0.1.1" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 45 | 46 | [[package]] 47 | name = "android_system_properties" 48 | version = "0.1.5" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 51 | dependencies = [ 52 | "libc", 53 | ] 54 | 55 | [[package]] 56 | name = "anstream" 57 | version = "0.6.18" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 60 | dependencies = [ 61 | "anstyle", 62 | "anstyle-parse", 63 | "anstyle-query", 64 | "anstyle-wincon", 65 | "colorchoice", 66 | "is_terminal_polyfill", 67 | "utf8parse", 68 | ] 69 | 70 | [[package]] 71 | name = "anstyle" 72 | version = "1.0.10" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 75 | 76 | [[package]] 77 | name = "anstyle-parse" 78 | version = "0.2.6" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 81 | dependencies = [ 82 | "utf8parse", 83 | ] 84 | 85 | [[package]] 86 | name = "anstyle-query" 87 | version = "1.1.2" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 90 | dependencies = [ 91 | "windows-sys 0.59.0", 92 | ] 93 | 94 | [[package]] 95 | name = "anstyle-wincon" 96 | version = "3.0.7" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" 99 | dependencies = [ 100 | "anstyle", 101 | "once_cell", 102 | "windows-sys 0.59.0", 103 | ] 104 | 105 | [[package]] 106 | name = "anyhow" 107 | version = "1.0.95" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 110 | 111 | [[package]] 112 | name = "apkeep" 113 | version = "0.17.0" 114 | dependencies = [ 115 | "base64", 116 | "clap", 117 | "configparser", 118 | "cryptographic-message-syntax", 119 | "dirs", 120 | "futures-util", 121 | "gpapi", 122 | "hex", 123 | "indicatif", 124 | "regex", 125 | "reqwest", 126 | "ring", 127 | "serde", 128 | "serde_json", 129 | "sha1", 130 | "sha2", 131 | "simple-error", 132 | "tempfile", 133 | "tokio", 134 | "tokio-dl-stream-to-disk", 135 | "x509-certificate", 136 | "zip", 137 | ] 138 | 139 | [[package]] 140 | name = "arbitrary" 141 | version = "1.4.1" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" 144 | dependencies = [ 145 | "derive_arbitrary", 146 | ] 147 | 148 | [[package]] 149 | name = "atomic-waker" 150 | version = "1.1.2" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" 153 | 154 | [[package]] 155 | name = "autocfg" 156 | version = "1.4.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 159 | 160 | [[package]] 161 | name = "backtrace" 162 | version = "0.3.74" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" 165 | dependencies = [ 166 | "addr2line", 167 | "cfg-if", 168 | "libc", 169 | "miniz_oxide", 170 | "object", 171 | "rustc-demangle", 172 | "windows-targets", 173 | ] 174 | 175 | [[package]] 176 | name = "base64" 177 | version = "0.22.1" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 180 | 181 | [[package]] 182 | name = "base64ct" 183 | version = "1.6.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" 186 | 187 | [[package]] 188 | name = "bcder" 189 | version = "0.7.5" 190 | source = "registry+https://github.com/rust-lang/crates.io-index" 191 | checksum = "89ffdaa8c6398acd07176317eb6c1f9082869dd1cc3fee7c72c6354866b928cc" 192 | dependencies = [ 193 | "bytes", 194 | "smallvec", 195 | ] 196 | 197 | [[package]] 198 | name = "bincode" 199 | version = "1.3.3" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 202 | dependencies = [ 203 | "serde", 204 | ] 205 | 206 | [[package]] 207 | name = "bitflags" 208 | version = "2.8.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" 211 | 212 | [[package]] 213 | name = "block-buffer" 214 | version = "0.10.4" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 217 | dependencies = [ 218 | "generic-array", 219 | ] 220 | 221 | [[package]] 222 | name = "bumpalo" 223 | version = "3.17.0" 224 | source = "registry+https://github.com/rust-lang/crates.io-index" 225 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 226 | 227 | [[package]] 228 | name = "byteorder" 229 | version = "1.5.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 232 | 233 | [[package]] 234 | name = "bytes" 235 | version = "1.10.0" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" 238 | 239 | [[package]] 240 | name = "bzip2" 241 | version = "0.4.4" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" 244 | dependencies = [ 245 | "bzip2-sys", 246 | "libc", 247 | ] 248 | 249 | [[package]] 250 | name = "bzip2-sys" 251 | version = "0.1.12+1.0.8" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "72ebc2f1a417f01e1da30ef264ee86ae31d2dcd2d603ea283d3c244a883ca2a9" 254 | dependencies = [ 255 | "cc", 256 | "libc", 257 | "pkg-config", 258 | ] 259 | 260 | [[package]] 261 | name = "cc" 262 | version = "1.2.13" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" 265 | dependencies = [ 266 | "jobserver", 267 | "libc", 268 | "shlex", 269 | ] 270 | 271 | [[package]] 272 | name = "cfg-if" 273 | version = "1.0.0" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 276 | 277 | [[package]] 278 | name = "cfg_aliases" 279 | version = "0.2.1" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 282 | 283 | [[package]] 284 | name = "chrono" 285 | version = "0.4.39" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 288 | dependencies = [ 289 | "android-tzdata", 290 | "iana-time-zone", 291 | "num-traits", 292 | "windows-targets", 293 | ] 294 | 295 | [[package]] 296 | name = "cipher" 297 | version = "0.4.4" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 300 | dependencies = [ 301 | "crypto-common", 302 | "inout", 303 | ] 304 | 305 | [[package]] 306 | name = "clap" 307 | version = "4.5.29" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" 310 | dependencies = [ 311 | "clap_builder", 312 | "clap_derive", 313 | ] 314 | 315 | [[package]] 316 | name = "clap_builder" 317 | version = "4.5.29" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" 320 | dependencies = [ 321 | "anstream", 322 | "anstyle", 323 | "clap_lex", 324 | "strsim", 325 | ] 326 | 327 | [[package]] 328 | name = "clap_derive" 329 | version = "4.5.28" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" 332 | dependencies = [ 333 | "heck", 334 | "proc-macro2", 335 | "quote", 336 | "syn", 337 | ] 338 | 339 | [[package]] 340 | name = "clap_lex" 341 | version = "0.7.4" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 344 | 345 | [[package]] 346 | name = "colorchoice" 347 | version = "1.0.3" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 350 | 351 | [[package]] 352 | name = "configparser" 353 | version = "3.1.0" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b" 356 | 357 | [[package]] 358 | name = "console" 359 | version = "0.15.10" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" 362 | dependencies = [ 363 | "encode_unicode", 364 | "libc", 365 | "once_cell", 366 | "unicode-width", 367 | "windows-sys 0.59.0", 368 | ] 369 | 370 | [[package]] 371 | name = "const-oid" 372 | version = "0.9.6" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" 375 | 376 | [[package]] 377 | name = "constant_time_eq" 378 | version = "0.3.1" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" 381 | 382 | [[package]] 383 | name = "core-foundation" 384 | version = "0.9.4" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 387 | dependencies = [ 388 | "core-foundation-sys", 389 | "libc", 390 | ] 391 | 392 | [[package]] 393 | name = "core-foundation-sys" 394 | version = "0.8.7" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 397 | 398 | [[package]] 399 | name = "cpufeatures" 400 | version = "0.2.17" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" 403 | dependencies = [ 404 | "libc", 405 | ] 406 | 407 | [[package]] 408 | name = "crc" 409 | version = "3.2.1" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" 412 | dependencies = [ 413 | "crc-catalog", 414 | ] 415 | 416 | [[package]] 417 | name = "crc-catalog" 418 | version = "2.4.0" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 421 | 422 | [[package]] 423 | name = "crc32fast" 424 | version = "1.4.2" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 427 | dependencies = [ 428 | "cfg-if", 429 | ] 430 | 431 | [[package]] 432 | name = "crossbeam-utils" 433 | version = "0.8.21" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 436 | 437 | [[package]] 438 | name = "crypto-common" 439 | version = "0.1.6" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 442 | dependencies = [ 443 | "generic-array", 444 | "typenum", 445 | ] 446 | 447 | [[package]] 448 | name = "cryptographic-message-syntax" 449 | version = "0.27.0" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "97a99e58d7755c646cb3f2a138d99f90da4c495282e1700b82daff8a48759ce0" 452 | dependencies = [ 453 | "bcder", 454 | "bytes", 455 | "chrono", 456 | "hex", 457 | "pem", 458 | "reqwest", 459 | "ring", 460 | "signature", 461 | "x509-certificate", 462 | ] 463 | 464 | [[package]] 465 | name = "deflate64" 466 | version = "0.1.9" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" 469 | 470 | [[package]] 471 | name = "der" 472 | version = "0.7.9" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" 475 | dependencies = [ 476 | "const-oid", 477 | "zeroize", 478 | ] 479 | 480 | [[package]] 481 | name = "deranged" 482 | version = "0.3.11" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 485 | dependencies = [ 486 | "powerfmt", 487 | ] 488 | 489 | [[package]] 490 | name = "derive_arbitrary" 491 | version = "1.4.1" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" 494 | dependencies = [ 495 | "proc-macro2", 496 | "quote", 497 | "syn", 498 | ] 499 | 500 | [[package]] 501 | name = "digest" 502 | version = "0.10.7" 503 | source = "registry+https://github.com/rust-lang/crates.io-index" 504 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 505 | dependencies = [ 506 | "block-buffer", 507 | "crypto-common", 508 | "subtle", 509 | ] 510 | 511 | [[package]] 512 | name = "dirs" 513 | version = "6.0.0" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 516 | dependencies = [ 517 | "dirs-sys", 518 | ] 519 | 520 | [[package]] 521 | name = "dirs-sys" 522 | version = "0.5.0" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 525 | dependencies = [ 526 | "libc", 527 | "option-ext", 528 | "redox_users", 529 | "windows-sys 0.59.0", 530 | ] 531 | 532 | [[package]] 533 | name = "displaydoc" 534 | version = "0.2.5" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 537 | dependencies = [ 538 | "proc-macro2", 539 | "quote", 540 | "syn", 541 | ] 542 | 543 | [[package]] 544 | name = "either" 545 | version = "1.13.0" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 548 | 549 | [[package]] 550 | name = "encode_unicode" 551 | version = "1.0.0" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 554 | 555 | [[package]] 556 | name = "encoding_rs" 557 | version = "0.8.35" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" 560 | dependencies = [ 561 | "cfg-if", 562 | ] 563 | 564 | [[package]] 565 | name = "equivalent" 566 | version = "1.0.1" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 569 | 570 | [[package]] 571 | name = "errno" 572 | version = "0.3.10" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 575 | dependencies = [ 576 | "libc", 577 | "windows-sys 0.59.0", 578 | ] 579 | 580 | [[package]] 581 | name = "fastrand" 582 | version = "2.3.0" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 585 | 586 | [[package]] 587 | name = "fixedbitset" 588 | version = "0.5.7" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" 591 | 592 | [[package]] 593 | name = "flate2" 594 | version = "1.0.35" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" 597 | dependencies = [ 598 | "crc32fast", 599 | "miniz_oxide", 600 | ] 601 | 602 | [[package]] 603 | name = "fnv" 604 | version = "1.0.7" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 607 | 608 | [[package]] 609 | name = "foreign-types" 610 | version = "0.3.2" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 613 | dependencies = [ 614 | "foreign-types-shared", 615 | ] 616 | 617 | [[package]] 618 | name = "foreign-types-shared" 619 | version = "0.1.1" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 622 | 623 | [[package]] 624 | name = "form_urlencoded" 625 | version = "1.2.1" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 628 | dependencies = [ 629 | "percent-encoding", 630 | ] 631 | 632 | [[package]] 633 | name = "futures" 634 | version = "0.3.31" 635 | source = "registry+https://github.com/rust-lang/crates.io-index" 636 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" 637 | dependencies = [ 638 | "futures-channel", 639 | "futures-core", 640 | "futures-executor", 641 | "futures-io", 642 | "futures-sink", 643 | "futures-task", 644 | "futures-util", 645 | ] 646 | 647 | [[package]] 648 | name = "futures-channel" 649 | version = "0.3.31" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" 652 | dependencies = [ 653 | "futures-core", 654 | "futures-sink", 655 | ] 656 | 657 | [[package]] 658 | name = "futures-core" 659 | version = "0.3.31" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 662 | 663 | [[package]] 664 | name = "futures-executor" 665 | version = "0.3.31" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" 668 | dependencies = [ 669 | "futures-core", 670 | "futures-task", 671 | "futures-util", 672 | ] 673 | 674 | [[package]] 675 | name = "futures-io" 676 | version = "0.3.31" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 679 | 680 | [[package]] 681 | name = "futures-macro" 682 | version = "0.3.31" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 685 | dependencies = [ 686 | "proc-macro2", 687 | "quote", 688 | "syn", 689 | ] 690 | 691 | [[package]] 692 | name = "futures-sink" 693 | version = "0.3.31" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" 696 | 697 | [[package]] 698 | name = "futures-task" 699 | version = "0.3.31" 700 | source = "registry+https://github.com/rust-lang/crates.io-index" 701 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 702 | 703 | [[package]] 704 | name = "futures-util" 705 | version = "0.3.31" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 708 | dependencies = [ 709 | "futures-channel", 710 | "futures-core", 711 | "futures-io", 712 | "futures-macro", 713 | "futures-sink", 714 | "futures-task", 715 | "memchr", 716 | "pin-project-lite", 717 | "pin-utils", 718 | "slab", 719 | ] 720 | 721 | [[package]] 722 | name = "generic-array" 723 | version = "0.14.7" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 726 | dependencies = [ 727 | "typenum", 728 | "version_check", 729 | ] 730 | 731 | [[package]] 732 | name = "getrandom" 733 | version = "0.2.15" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 736 | dependencies = [ 737 | "cfg-if", 738 | "js-sys", 739 | "libc", 740 | "wasi 0.11.0+wasi-snapshot-preview1", 741 | "wasm-bindgen", 742 | ] 743 | 744 | [[package]] 745 | name = "getrandom" 746 | version = "0.3.1" 747 | source = "registry+https://github.com/rust-lang/crates.io-index" 748 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" 749 | dependencies = [ 750 | "cfg-if", 751 | "libc", 752 | "wasi 0.13.3+wasi-0.2.2", 753 | "windows-targets", 754 | ] 755 | 756 | [[package]] 757 | name = "gimli" 758 | version = "0.31.1" 759 | source = "registry+https://github.com/rust-lang/crates.io-index" 760 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 761 | 762 | [[package]] 763 | name = "googleplay-protobuf" 764 | version = "2.1.0" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "14f081ef352e2c0ada44c41c590e2df0555c99ff8cfa818244728a46f8aa781c" 767 | dependencies = [ 768 | "prost", 769 | "prost-build", 770 | ] 771 | 772 | [[package]] 773 | name = "gpapi" 774 | version = "5.4.0" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "517030b33e92df94a13855f13d17f05e2b29ff597136ecf3f11524eab13ea3b4" 777 | dependencies = [ 778 | "bincode", 779 | "bytes", 780 | "configparser", 781 | "futures", 782 | "googleplay-protobuf", 783 | "prost", 784 | "reqwest", 785 | "serde", 786 | "tokio-dl-stream-to-disk", 787 | ] 788 | 789 | [[package]] 790 | name = "h2" 791 | version = "0.4.7" 792 | source = "registry+https://github.com/rust-lang/crates.io-index" 793 | checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" 794 | dependencies = [ 795 | "atomic-waker", 796 | "bytes", 797 | "fnv", 798 | "futures-core", 799 | "futures-sink", 800 | "http", 801 | "indexmap", 802 | "slab", 803 | "tokio", 804 | "tokio-util", 805 | "tracing", 806 | ] 807 | 808 | [[package]] 809 | name = "hashbrown" 810 | version = "0.15.2" 811 | source = "registry+https://github.com/rust-lang/crates.io-index" 812 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 813 | 814 | [[package]] 815 | name = "heck" 816 | version = "0.5.0" 817 | source = "registry+https://github.com/rust-lang/crates.io-index" 818 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 819 | 820 | [[package]] 821 | name = "hex" 822 | version = "0.4.3" 823 | source = "registry+https://github.com/rust-lang/crates.io-index" 824 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 825 | 826 | [[package]] 827 | name = "hmac" 828 | version = "0.12.1" 829 | source = "registry+https://github.com/rust-lang/crates.io-index" 830 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 831 | dependencies = [ 832 | "digest", 833 | ] 834 | 835 | [[package]] 836 | name = "http" 837 | version = "1.2.0" 838 | source = "registry+https://github.com/rust-lang/crates.io-index" 839 | checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" 840 | dependencies = [ 841 | "bytes", 842 | "fnv", 843 | "itoa", 844 | ] 845 | 846 | [[package]] 847 | name = "http-body" 848 | version = "1.0.1" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" 851 | dependencies = [ 852 | "bytes", 853 | "http", 854 | ] 855 | 856 | [[package]] 857 | name = "http-body-util" 858 | version = "0.1.2" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" 861 | dependencies = [ 862 | "bytes", 863 | "futures-util", 864 | "http", 865 | "http-body", 866 | "pin-project-lite", 867 | ] 868 | 869 | [[package]] 870 | name = "httparse" 871 | version = "1.10.0" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" 874 | 875 | [[package]] 876 | name = "hyper" 877 | version = "1.6.0" 878 | source = "registry+https://github.com/rust-lang/crates.io-index" 879 | checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" 880 | dependencies = [ 881 | "bytes", 882 | "futures-channel", 883 | "futures-util", 884 | "h2", 885 | "http", 886 | "http-body", 887 | "httparse", 888 | "itoa", 889 | "pin-project-lite", 890 | "smallvec", 891 | "tokio", 892 | "want", 893 | ] 894 | 895 | [[package]] 896 | name = "hyper-rustls" 897 | version = "0.27.5" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" 900 | dependencies = [ 901 | "futures-util", 902 | "http", 903 | "hyper", 904 | "hyper-util", 905 | "rustls", 906 | "rustls-pki-types", 907 | "tokio", 908 | "tokio-rustls", 909 | "tower-service", 910 | "webpki-roots", 911 | ] 912 | 913 | [[package]] 914 | name = "hyper-tls" 915 | version = "0.6.0" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 918 | dependencies = [ 919 | "bytes", 920 | "http-body-util", 921 | "hyper", 922 | "hyper-util", 923 | "native-tls", 924 | "tokio", 925 | "tokio-native-tls", 926 | "tower-service", 927 | ] 928 | 929 | [[package]] 930 | name = "hyper-util" 931 | version = "0.1.10" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" 934 | dependencies = [ 935 | "bytes", 936 | "futures-channel", 937 | "futures-util", 938 | "http", 939 | "http-body", 940 | "hyper", 941 | "pin-project-lite", 942 | "socket2", 943 | "tokio", 944 | "tower-service", 945 | "tracing", 946 | ] 947 | 948 | [[package]] 949 | name = "iana-time-zone" 950 | version = "0.1.61" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 953 | dependencies = [ 954 | "android_system_properties", 955 | "core-foundation-sys", 956 | "iana-time-zone-haiku", 957 | "js-sys", 958 | "wasm-bindgen", 959 | "windows-core", 960 | ] 961 | 962 | [[package]] 963 | name = "iana-time-zone-haiku" 964 | version = "0.1.2" 965 | source = "registry+https://github.com/rust-lang/crates.io-index" 966 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 967 | dependencies = [ 968 | "cc", 969 | ] 970 | 971 | [[package]] 972 | name = "icu_collections" 973 | version = "1.5.0" 974 | source = "registry+https://github.com/rust-lang/crates.io-index" 975 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 976 | dependencies = [ 977 | "displaydoc", 978 | "yoke", 979 | "zerofrom", 980 | "zerovec", 981 | ] 982 | 983 | [[package]] 984 | name = "icu_locid" 985 | version = "1.5.0" 986 | source = "registry+https://github.com/rust-lang/crates.io-index" 987 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 988 | dependencies = [ 989 | "displaydoc", 990 | "litemap", 991 | "tinystr", 992 | "writeable", 993 | "zerovec", 994 | ] 995 | 996 | [[package]] 997 | name = "icu_locid_transform" 998 | version = "1.5.0" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 1001 | dependencies = [ 1002 | "displaydoc", 1003 | "icu_locid", 1004 | "icu_locid_transform_data", 1005 | "icu_provider", 1006 | "tinystr", 1007 | "zerovec", 1008 | ] 1009 | 1010 | [[package]] 1011 | name = "icu_locid_transform_data" 1012 | version = "1.5.0" 1013 | source = "registry+https://github.com/rust-lang/crates.io-index" 1014 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 1015 | 1016 | [[package]] 1017 | name = "icu_normalizer" 1018 | version = "1.5.0" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 1021 | dependencies = [ 1022 | "displaydoc", 1023 | "icu_collections", 1024 | "icu_normalizer_data", 1025 | "icu_properties", 1026 | "icu_provider", 1027 | "smallvec", 1028 | "utf16_iter", 1029 | "utf8_iter", 1030 | "write16", 1031 | "zerovec", 1032 | ] 1033 | 1034 | [[package]] 1035 | name = "icu_normalizer_data" 1036 | version = "1.5.0" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 1039 | 1040 | [[package]] 1041 | name = "icu_properties" 1042 | version = "1.5.1" 1043 | source = "registry+https://github.com/rust-lang/crates.io-index" 1044 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 1045 | dependencies = [ 1046 | "displaydoc", 1047 | "icu_collections", 1048 | "icu_locid_transform", 1049 | "icu_properties_data", 1050 | "icu_provider", 1051 | "tinystr", 1052 | "zerovec", 1053 | ] 1054 | 1055 | [[package]] 1056 | name = "icu_properties_data" 1057 | version = "1.5.0" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 1060 | 1061 | [[package]] 1062 | name = "icu_provider" 1063 | version = "1.5.0" 1064 | source = "registry+https://github.com/rust-lang/crates.io-index" 1065 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 1066 | dependencies = [ 1067 | "displaydoc", 1068 | "icu_locid", 1069 | "icu_provider_macros", 1070 | "stable_deref_trait", 1071 | "tinystr", 1072 | "writeable", 1073 | "yoke", 1074 | "zerofrom", 1075 | "zerovec", 1076 | ] 1077 | 1078 | [[package]] 1079 | name = "icu_provider_macros" 1080 | version = "1.5.0" 1081 | source = "registry+https://github.com/rust-lang/crates.io-index" 1082 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 1083 | dependencies = [ 1084 | "proc-macro2", 1085 | "quote", 1086 | "syn", 1087 | ] 1088 | 1089 | [[package]] 1090 | name = "idna" 1091 | version = "1.0.3" 1092 | source = "registry+https://github.com/rust-lang/crates.io-index" 1093 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 1094 | dependencies = [ 1095 | "idna_adapter", 1096 | "smallvec", 1097 | "utf8_iter", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "idna_adapter" 1102 | version = "1.2.0" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 1105 | dependencies = [ 1106 | "icu_normalizer", 1107 | "icu_properties", 1108 | ] 1109 | 1110 | [[package]] 1111 | name = "indexmap" 1112 | version = "2.7.1" 1113 | source = "registry+https://github.com/rust-lang/crates.io-index" 1114 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 1115 | dependencies = [ 1116 | "equivalent", 1117 | "hashbrown", 1118 | ] 1119 | 1120 | [[package]] 1121 | name = "indicatif" 1122 | version = "0.17.11" 1123 | source = "registry+https://github.com/rust-lang/crates.io-index" 1124 | checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" 1125 | dependencies = [ 1126 | "console", 1127 | "number_prefix", 1128 | "portable-atomic", 1129 | "unicode-width", 1130 | "web-time", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "inout" 1135 | version = "0.1.3" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" 1138 | dependencies = [ 1139 | "generic-array", 1140 | ] 1141 | 1142 | [[package]] 1143 | name = "ipnet" 1144 | version = "2.11.0" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 1147 | 1148 | [[package]] 1149 | name = "is_terminal_polyfill" 1150 | version = "1.70.1" 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" 1152 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 1153 | 1154 | [[package]] 1155 | name = "itertools" 1156 | version = "0.14.0" 1157 | source = "registry+https://github.com/rust-lang/crates.io-index" 1158 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 1159 | dependencies = [ 1160 | "either", 1161 | ] 1162 | 1163 | [[package]] 1164 | name = "itoa" 1165 | version = "1.0.14" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 1168 | 1169 | [[package]] 1170 | name = "jobserver" 1171 | version = "0.1.32" 1172 | source = "registry+https://github.com/rust-lang/crates.io-index" 1173 | checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" 1174 | dependencies = [ 1175 | "libc", 1176 | ] 1177 | 1178 | [[package]] 1179 | name = "js-sys" 1180 | version = "0.3.77" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 1183 | dependencies = [ 1184 | "once_cell", 1185 | "wasm-bindgen", 1186 | ] 1187 | 1188 | [[package]] 1189 | name = "libc" 1190 | version = "0.2.169" 1191 | source = "registry+https://github.com/rust-lang/crates.io-index" 1192 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 1193 | 1194 | [[package]] 1195 | name = "libredox" 1196 | version = "0.1.3" 1197 | source = "registry+https://github.com/rust-lang/crates.io-index" 1198 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 1199 | dependencies = [ 1200 | "bitflags", 1201 | "libc", 1202 | ] 1203 | 1204 | [[package]] 1205 | name = "linux-raw-sys" 1206 | version = "0.4.15" 1207 | source = "registry+https://github.com/rust-lang/crates.io-index" 1208 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 1209 | 1210 | [[package]] 1211 | name = "litemap" 1212 | version = "0.7.4" 1213 | source = "registry+https://github.com/rust-lang/crates.io-index" 1214 | checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" 1215 | 1216 | [[package]] 1217 | name = "lock_api" 1218 | version = "0.4.12" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 1221 | dependencies = [ 1222 | "autocfg", 1223 | "scopeguard", 1224 | ] 1225 | 1226 | [[package]] 1227 | name = "lockfree-object-pool" 1228 | version = "0.1.6" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" 1231 | 1232 | [[package]] 1233 | name = "log" 1234 | version = "0.4.25" 1235 | source = "registry+https://github.com/rust-lang/crates.io-index" 1236 | checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" 1237 | 1238 | [[package]] 1239 | name = "lzma-rs" 1240 | version = "0.3.0" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" 1243 | dependencies = [ 1244 | "byteorder", 1245 | "crc", 1246 | ] 1247 | 1248 | [[package]] 1249 | name = "memchr" 1250 | version = "2.7.4" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 1253 | 1254 | [[package]] 1255 | name = "mime" 1256 | version = "0.3.17" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 1259 | 1260 | [[package]] 1261 | name = "miniz_oxide" 1262 | version = "0.8.4" 1263 | source = "registry+https://github.com/rust-lang/crates.io-index" 1264 | checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" 1265 | dependencies = [ 1266 | "adler2", 1267 | ] 1268 | 1269 | [[package]] 1270 | name = "mio" 1271 | version = "1.0.3" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" 1274 | dependencies = [ 1275 | "libc", 1276 | "wasi 0.11.0+wasi-snapshot-preview1", 1277 | "windows-sys 0.52.0", 1278 | ] 1279 | 1280 | [[package]] 1281 | name = "multimap" 1282 | version = "0.10.0" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" 1285 | 1286 | [[package]] 1287 | name = "native-tls" 1288 | version = "0.2.13" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "0dab59f8e050d5df8e4dd87d9206fb6f65a483e20ac9fda365ade4fab353196c" 1291 | dependencies = [ 1292 | "libc", 1293 | "log", 1294 | "openssl", 1295 | "openssl-probe", 1296 | "openssl-sys", 1297 | "schannel", 1298 | "security-framework", 1299 | "security-framework-sys", 1300 | "tempfile", 1301 | ] 1302 | 1303 | [[package]] 1304 | name = "num-conv" 1305 | version = "0.1.0" 1306 | source = "registry+https://github.com/rust-lang/crates.io-index" 1307 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 1308 | 1309 | [[package]] 1310 | name = "num-traits" 1311 | version = "0.2.19" 1312 | source = "registry+https://github.com/rust-lang/crates.io-index" 1313 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1314 | dependencies = [ 1315 | "autocfg", 1316 | ] 1317 | 1318 | [[package]] 1319 | name = "number_prefix" 1320 | version = "0.4.0" 1321 | source = "registry+https://github.com/rust-lang/crates.io-index" 1322 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 1323 | 1324 | [[package]] 1325 | name = "object" 1326 | version = "0.36.7" 1327 | source = "registry+https://github.com/rust-lang/crates.io-index" 1328 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1329 | dependencies = [ 1330 | "memchr", 1331 | ] 1332 | 1333 | [[package]] 1334 | name = "once_cell" 1335 | version = "1.20.3" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 1338 | 1339 | [[package]] 1340 | name = "openssl" 1341 | version = "0.10.70" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "61cfb4e166a8bb8c9b55c500bc2308550148ece889be90f609377e58140f42c6" 1344 | dependencies = [ 1345 | "bitflags", 1346 | "cfg-if", 1347 | "foreign-types", 1348 | "libc", 1349 | "once_cell", 1350 | "openssl-macros", 1351 | "openssl-sys", 1352 | ] 1353 | 1354 | [[package]] 1355 | name = "openssl-macros" 1356 | version = "0.1.1" 1357 | source = "registry+https://github.com/rust-lang/crates.io-index" 1358 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 1359 | dependencies = [ 1360 | "proc-macro2", 1361 | "quote", 1362 | "syn", 1363 | ] 1364 | 1365 | [[package]] 1366 | name = "openssl-probe" 1367 | version = "0.1.6" 1368 | source = "registry+https://github.com/rust-lang/crates.io-index" 1369 | checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" 1370 | 1371 | [[package]] 1372 | name = "openssl-sys" 1373 | version = "0.9.105" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "8b22d5b84be05a8d6947c7cb71f7c849aa0f112acd4bf51c2a7c1c988ac0a9dc" 1376 | dependencies = [ 1377 | "cc", 1378 | "libc", 1379 | "pkg-config", 1380 | "vcpkg", 1381 | ] 1382 | 1383 | [[package]] 1384 | name = "option-ext" 1385 | version = "0.2.0" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 1388 | 1389 | [[package]] 1390 | name = "parking_lot" 1391 | version = "0.12.3" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 1394 | dependencies = [ 1395 | "lock_api", 1396 | "parking_lot_core", 1397 | ] 1398 | 1399 | [[package]] 1400 | name = "parking_lot_core" 1401 | version = "0.9.10" 1402 | source = "registry+https://github.com/rust-lang/crates.io-index" 1403 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 1404 | dependencies = [ 1405 | "cfg-if", 1406 | "libc", 1407 | "redox_syscall", 1408 | "smallvec", 1409 | "windows-targets", 1410 | ] 1411 | 1412 | [[package]] 1413 | name = "pbkdf2" 1414 | version = "0.12.2" 1415 | source = "registry+https://github.com/rust-lang/crates.io-index" 1416 | checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" 1417 | dependencies = [ 1418 | "digest", 1419 | "hmac", 1420 | ] 1421 | 1422 | [[package]] 1423 | name = "pem" 1424 | version = "3.0.4" 1425 | source = "registry+https://github.com/rust-lang/crates.io-index" 1426 | checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" 1427 | dependencies = [ 1428 | "base64", 1429 | "serde", 1430 | ] 1431 | 1432 | [[package]] 1433 | name = "percent-encoding" 1434 | version = "2.3.1" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1437 | 1438 | [[package]] 1439 | name = "petgraph" 1440 | version = "0.7.1" 1441 | source = "registry+https://github.com/rust-lang/crates.io-index" 1442 | checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" 1443 | dependencies = [ 1444 | "fixedbitset", 1445 | "indexmap", 1446 | ] 1447 | 1448 | [[package]] 1449 | name = "pin-project-lite" 1450 | version = "0.2.16" 1451 | source = "registry+https://github.com/rust-lang/crates.io-index" 1452 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 1453 | 1454 | [[package]] 1455 | name = "pin-utils" 1456 | version = "0.1.0" 1457 | source = "registry+https://github.com/rust-lang/crates.io-index" 1458 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1459 | 1460 | [[package]] 1461 | name = "pkg-config" 1462 | version = "0.3.31" 1463 | source = "registry+https://github.com/rust-lang/crates.io-index" 1464 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 1465 | 1466 | [[package]] 1467 | name = "portable-atomic" 1468 | version = "1.10.0" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" 1471 | 1472 | [[package]] 1473 | name = "powerfmt" 1474 | version = "0.2.0" 1475 | source = "registry+https://github.com/rust-lang/crates.io-index" 1476 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 1477 | 1478 | [[package]] 1479 | name = "ppv-lite86" 1480 | version = "0.2.20" 1481 | source = "registry+https://github.com/rust-lang/crates.io-index" 1482 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 1483 | dependencies = [ 1484 | "zerocopy", 1485 | ] 1486 | 1487 | [[package]] 1488 | name = "prettyplease" 1489 | version = "0.2.29" 1490 | source = "registry+https://github.com/rust-lang/crates.io-index" 1491 | checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" 1492 | dependencies = [ 1493 | "proc-macro2", 1494 | "syn", 1495 | ] 1496 | 1497 | [[package]] 1498 | name = "proc-macro2" 1499 | version = "1.0.93" 1500 | source = "registry+https://github.com/rust-lang/crates.io-index" 1501 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 1502 | dependencies = [ 1503 | "unicode-ident", 1504 | ] 1505 | 1506 | [[package]] 1507 | name = "prost" 1508 | version = "0.13.5" 1509 | source = "registry+https://github.com/rust-lang/crates.io-index" 1510 | checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" 1511 | dependencies = [ 1512 | "bytes", 1513 | "prost-derive", 1514 | ] 1515 | 1516 | [[package]] 1517 | name = "prost-build" 1518 | version = "0.13.5" 1519 | source = "registry+https://github.com/rust-lang/crates.io-index" 1520 | checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" 1521 | dependencies = [ 1522 | "heck", 1523 | "itertools", 1524 | "log", 1525 | "multimap", 1526 | "once_cell", 1527 | "petgraph", 1528 | "prettyplease", 1529 | "prost", 1530 | "prost-types", 1531 | "regex", 1532 | "syn", 1533 | "tempfile", 1534 | ] 1535 | 1536 | [[package]] 1537 | name = "prost-derive" 1538 | version = "0.13.5" 1539 | source = "registry+https://github.com/rust-lang/crates.io-index" 1540 | checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" 1541 | dependencies = [ 1542 | "anyhow", 1543 | "itertools", 1544 | "proc-macro2", 1545 | "quote", 1546 | "syn", 1547 | ] 1548 | 1549 | [[package]] 1550 | name = "prost-types" 1551 | version = "0.13.5" 1552 | source = "registry+https://github.com/rust-lang/crates.io-index" 1553 | checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" 1554 | dependencies = [ 1555 | "prost", 1556 | ] 1557 | 1558 | [[package]] 1559 | name = "quinn" 1560 | version = "0.11.6" 1561 | source = "registry+https://github.com/rust-lang/crates.io-index" 1562 | checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" 1563 | dependencies = [ 1564 | "bytes", 1565 | "pin-project-lite", 1566 | "quinn-proto", 1567 | "quinn-udp", 1568 | "rustc-hash", 1569 | "rustls", 1570 | "socket2", 1571 | "thiserror 2.0.11", 1572 | "tokio", 1573 | "tracing", 1574 | ] 1575 | 1576 | [[package]] 1577 | name = "quinn-proto" 1578 | version = "0.11.9" 1579 | source = "registry+https://github.com/rust-lang/crates.io-index" 1580 | checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" 1581 | dependencies = [ 1582 | "bytes", 1583 | "getrandom 0.2.15", 1584 | "rand", 1585 | "ring", 1586 | "rustc-hash", 1587 | "rustls", 1588 | "rustls-pki-types", 1589 | "slab", 1590 | "thiserror 2.0.11", 1591 | "tinyvec", 1592 | "tracing", 1593 | "web-time", 1594 | ] 1595 | 1596 | [[package]] 1597 | name = "quinn-udp" 1598 | version = "0.5.9" 1599 | source = "registry+https://github.com/rust-lang/crates.io-index" 1600 | checksum = "1c40286217b4ba3a71d644d752e6a0b71f13f1b6a2c5311acfcbe0c2418ed904" 1601 | dependencies = [ 1602 | "cfg_aliases", 1603 | "libc", 1604 | "once_cell", 1605 | "socket2", 1606 | "tracing", 1607 | "windows-sys 0.59.0", 1608 | ] 1609 | 1610 | [[package]] 1611 | name = "quote" 1612 | version = "1.0.38" 1613 | source = "registry+https://github.com/rust-lang/crates.io-index" 1614 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 1615 | dependencies = [ 1616 | "proc-macro2", 1617 | ] 1618 | 1619 | [[package]] 1620 | name = "rand" 1621 | version = "0.8.5" 1622 | source = "registry+https://github.com/rust-lang/crates.io-index" 1623 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1624 | dependencies = [ 1625 | "libc", 1626 | "rand_chacha", 1627 | "rand_core", 1628 | ] 1629 | 1630 | [[package]] 1631 | name = "rand_chacha" 1632 | version = "0.3.1" 1633 | source = "registry+https://github.com/rust-lang/crates.io-index" 1634 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1635 | dependencies = [ 1636 | "ppv-lite86", 1637 | "rand_core", 1638 | ] 1639 | 1640 | [[package]] 1641 | name = "rand_core" 1642 | version = "0.6.4" 1643 | source = "registry+https://github.com/rust-lang/crates.io-index" 1644 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1645 | dependencies = [ 1646 | "getrandom 0.2.15", 1647 | ] 1648 | 1649 | [[package]] 1650 | name = "redox_syscall" 1651 | version = "0.5.8" 1652 | source = "registry+https://github.com/rust-lang/crates.io-index" 1653 | checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 1654 | dependencies = [ 1655 | "bitflags", 1656 | ] 1657 | 1658 | [[package]] 1659 | name = "redox_users" 1660 | version = "0.5.0" 1661 | source = "registry+https://github.com/rust-lang/crates.io-index" 1662 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 1663 | dependencies = [ 1664 | "getrandom 0.2.15", 1665 | "libredox", 1666 | "thiserror 2.0.11", 1667 | ] 1668 | 1669 | [[package]] 1670 | name = "regex" 1671 | version = "1.11.1" 1672 | source = "registry+https://github.com/rust-lang/crates.io-index" 1673 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1674 | dependencies = [ 1675 | "aho-corasick", 1676 | "memchr", 1677 | "regex-automata", 1678 | "regex-syntax", 1679 | ] 1680 | 1681 | [[package]] 1682 | name = "regex-automata" 1683 | version = "0.4.9" 1684 | source = "registry+https://github.com/rust-lang/crates.io-index" 1685 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1686 | dependencies = [ 1687 | "aho-corasick", 1688 | "memchr", 1689 | "regex-syntax", 1690 | ] 1691 | 1692 | [[package]] 1693 | name = "regex-syntax" 1694 | version = "0.8.5" 1695 | source = "registry+https://github.com/rust-lang/crates.io-index" 1696 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1697 | 1698 | [[package]] 1699 | name = "reqwest" 1700 | version = "0.12.12" 1701 | source = "registry+https://github.com/rust-lang/crates.io-index" 1702 | checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" 1703 | dependencies = [ 1704 | "base64", 1705 | "bytes", 1706 | "encoding_rs", 1707 | "futures-channel", 1708 | "futures-core", 1709 | "futures-util", 1710 | "h2", 1711 | "http", 1712 | "http-body", 1713 | "http-body-util", 1714 | "hyper", 1715 | "hyper-rustls", 1716 | "hyper-tls", 1717 | "hyper-util", 1718 | "ipnet", 1719 | "js-sys", 1720 | "log", 1721 | "mime", 1722 | "native-tls", 1723 | "once_cell", 1724 | "percent-encoding", 1725 | "pin-project-lite", 1726 | "quinn", 1727 | "rustls", 1728 | "rustls-pemfile", 1729 | "rustls-pki-types", 1730 | "serde", 1731 | "serde_json", 1732 | "serde_urlencoded", 1733 | "sync_wrapper", 1734 | "system-configuration", 1735 | "tokio", 1736 | "tokio-native-tls", 1737 | "tokio-rustls", 1738 | "tokio-util", 1739 | "tower", 1740 | "tower-service", 1741 | "url", 1742 | "wasm-bindgen", 1743 | "wasm-bindgen-futures", 1744 | "wasm-streams", 1745 | "web-sys", 1746 | "webpki-roots", 1747 | "windows-registry", 1748 | ] 1749 | 1750 | [[package]] 1751 | name = "ring" 1752 | version = "0.17.8" 1753 | source = "registry+https://github.com/rust-lang/crates.io-index" 1754 | checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" 1755 | dependencies = [ 1756 | "cc", 1757 | "cfg-if", 1758 | "getrandom 0.2.15", 1759 | "libc", 1760 | "spin", 1761 | "untrusted", 1762 | "windows-sys 0.52.0", 1763 | ] 1764 | 1765 | [[package]] 1766 | name = "rustc-demangle" 1767 | version = "0.1.24" 1768 | source = "registry+https://github.com/rust-lang/crates.io-index" 1769 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 1770 | 1771 | [[package]] 1772 | name = "rustc-hash" 1773 | version = "2.1.1" 1774 | source = "registry+https://github.com/rust-lang/crates.io-index" 1775 | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 1776 | 1777 | [[package]] 1778 | name = "rustix" 1779 | version = "0.38.44" 1780 | source = "registry+https://github.com/rust-lang/crates.io-index" 1781 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" 1782 | dependencies = [ 1783 | "bitflags", 1784 | "errno", 1785 | "libc", 1786 | "linux-raw-sys", 1787 | "windows-sys 0.59.0", 1788 | ] 1789 | 1790 | [[package]] 1791 | name = "rustls" 1792 | version = "0.23.23" 1793 | source = "registry+https://github.com/rust-lang/crates.io-index" 1794 | checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" 1795 | dependencies = [ 1796 | "once_cell", 1797 | "ring", 1798 | "rustls-pki-types", 1799 | "rustls-webpki", 1800 | "subtle", 1801 | "zeroize", 1802 | ] 1803 | 1804 | [[package]] 1805 | name = "rustls-pemfile" 1806 | version = "2.2.0" 1807 | source = "registry+https://github.com/rust-lang/crates.io-index" 1808 | checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" 1809 | dependencies = [ 1810 | "rustls-pki-types", 1811 | ] 1812 | 1813 | [[package]] 1814 | name = "rustls-pki-types" 1815 | version = "1.11.0" 1816 | source = "registry+https://github.com/rust-lang/crates.io-index" 1817 | checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" 1818 | dependencies = [ 1819 | "web-time", 1820 | ] 1821 | 1822 | [[package]] 1823 | name = "rustls-webpki" 1824 | version = "0.102.8" 1825 | source = "registry+https://github.com/rust-lang/crates.io-index" 1826 | checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" 1827 | dependencies = [ 1828 | "ring", 1829 | "rustls-pki-types", 1830 | "untrusted", 1831 | ] 1832 | 1833 | [[package]] 1834 | name = "rustversion" 1835 | version = "1.0.19" 1836 | source = "registry+https://github.com/rust-lang/crates.io-index" 1837 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 1838 | 1839 | [[package]] 1840 | name = "ryu" 1841 | version = "1.0.19" 1842 | source = "registry+https://github.com/rust-lang/crates.io-index" 1843 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 1844 | 1845 | [[package]] 1846 | name = "schannel" 1847 | version = "0.1.27" 1848 | source = "registry+https://github.com/rust-lang/crates.io-index" 1849 | checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" 1850 | dependencies = [ 1851 | "windows-sys 0.59.0", 1852 | ] 1853 | 1854 | [[package]] 1855 | name = "scopeguard" 1856 | version = "1.2.0" 1857 | source = "registry+https://github.com/rust-lang/crates.io-index" 1858 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1859 | 1860 | [[package]] 1861 | name = "security-framework" 1862 | version = "2.11.1" 1863 | source = "registry+https://github.com/rust-lang/crates.io-index" 1864 | checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" 1865 | dependencies = [ 1866 | "bitflags", 1867 | "core-foundation", 1868 | "core-foundation-sys", 1869 | "libc", 1870 | "security-framework-sys", 1871 | ] 1872 | 1873 | [[package]] 1874 | name = "security-framework-sys" 1875 | version = "2.14.0" 1876 | source = "registry+https://github.com/rust-lang/crates.io-index" 1877 | checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" 1878 | dependencies = [ 1879 | "core-foundation-sys", 1880 | "libc", 1881 | ] 1882 | 1883 | [[package]] 1884 | name = "serde" 1885 | version = "1.0.217" 1886 | source = "registry+https://github.com/rust-lang/crates.io-index" 1887 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 1888 | dependencies = [ 1889 | "serde_derive", 1890 | ] 1891 | 1892 | [[package]] 1893 | name = "serde_derive" 1894 | version = "1.0.217" 1895 | source = "registry+https://github.com/rust-lang/crates.io-index" 1896 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 1897 | dependencies = [ 1898 | "proc-macro2", 1899 | "quote", 1900 | "syn", 1901 | ] 1902 | 1903 | [[package]] 1904 | name = "serde_json" 1905 | version = "1.0.138" 1906 | source = "registry+https://github.com/rust-lang/crates.io-index" 1907 | checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949" 1908 | dependencies = [ 1909 | "itoa", 1910 | "memchr", 1911 | "ryu", 1912 | "serde", 1913 | ] 1914 | 1915 | [[package]] 1916 | name = "serde_urlencoded" 1917 | version = "0.7.1" 1918 | source = "registry+https://github.com/rust-lang/crates.io-index" 1919 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1920 | dependencies = [ 1921 | "form_urlencoded", 1922 | "itoa", 1923 | "ryu", 1924 | "serde", 1925 | ] 1926 | 1927 | [[package]] 1928 | name = "sha1" 1929 | version = "0.10.6" 1930 | source = "registry+https://github.com/rust-lang/crates.io-index" 1931 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 1932 | dependencies = [ 1933 | "cfg-if", 1934 | "cpufeatures", 1935 | "digest", 1936 | ] 1937 | 1938 | [[package]] 1939 | name = "sha2" 1940 | version = "0.10.8" 1941 | source = "registry+https://github.com/rust-lang/crates.io-index" 1942 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 1943 | dependencies = [ 1944 | "cfg-if", 1945 | "cpufeatures", 1946 | "digest", 1947 | ] 1948 | 1949 | [[package]] 1950 | name = "shlex" 1951 | version = "1.3.0" 1952 | source = "registry+https://github.com/rust-lang/crates.io-index" 1953 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1954 | 1955 | [[package]] 1956 | name = "signal-hook-registry" 1957 | version = "1.4.2" 1958 | source = "registry+https://github.com/rust-lang/crates.io-index" 1959 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 1960 | dependencies = [ 1961 | "libc", 1962 | ] 1963 | 1964 | [[package]] 1965 | name = "signature" 1966 | version = "2.2.0" 1967 | source = "registry+https://github.com/rust-lang/crates.io-index" 1968 | checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 1969 | dependencies = [ 1970 | "rand_core", 1971 | ] 1972 | 1973 | [[package]] 1974 | name = "simd-adler32" 1975 | version = "0.3.7" 1976 | source = "registry+https://github.com/rust-lang/crates.io-index" 1977 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 1978 | 1979 | [[package]] 1980 | name = "simple-error" 1981 | version = "0.3.1" 1982 | source = "registry+https://github.com/rust-lang/crates.io-index" 1983 | checksum = "7e2accd2c41a0e920d2abd91b2badcfa1da784662f54fbc47e0e3a51f1e2e1cf" 1984 | 1985 | [[package]] 1986 | name = "slab" 1987 | version = "0.4.9" 1988 | source = "registry+https://github.com/rust-lang/crates.io-index" 1989 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1990 | dependencies = [ 1991 | "autocfg", 1992 | ] 1993 | 1994 | [[package]] 1995 | name = "smallvec" 1996 | version = "1.13.2" 1997 | source = "registry+https://github.com/rust-lang/crates.io-index" 1998 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1999 | 2000 | [[package]] 2001 | name = "socket2" 2002 | version = "0.5.8" 2003 | source = "registry+https://github.com/rust-lang/crates.io-index" 2004 | checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" 2005 | dependencies = [ 2006 | "libc", 2007 | "windows-sys 0.52.0", 2008 | ] 2009 | 2010 | [[package]] 2011 | name = "spin" 2012 | version = "0.9.8" 2013 | source = "registry+https://github.com/rust-lang/crates.io-index" 2014 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 2015 | 2016 | [[package]] 2017 | name = "spki" 2018 | version = "0.7.3" 2019 | source = "registry+https://github.com/rust-lang/crates.io-index" 2020 | checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 2021 | dependencies = [ 2022 | "base64ct", 2023 | "der", 2024 | ] 2025 | 2026 | [[package]] 2027 | name = "stable_deref_trait" 2028 | version = "1.2.0" 2029 | source = "registry+https://github.com/rust-lang/crates.io-index" 2030 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 2031 | 2032 | [[package]] 2033 | name = "strsim" 2034 | version = "0.11.1" 2035 | source = "registry+https://github.com/rust-lang/crates.io-index" 2036 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 2037 | 2038 | [[package]] 2039 | name = "subtle" 2040 | version = "2.6.1" 2041 | source = "registry+https://github.com/rust-lang/crates.io-index" 2042 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 2043 | 2044 | [[package]] 2045 | name = "syn" 2046 | version = "2.0.98" 2047 | source = "registry+https://github.com/rust-lang/crates.io-index" 2048 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 2049 | dependencies = [ 2050 | "proc-macro2", 2051 | "quote", 2052 | "unicode-ident", 2053 | ] 2054 | 2055 | [[package]] 2056 | name = "sync_wrapper" 2057 | version = "1.0.2" 2058 | source = "registry+https://github.com/rust-lang/crates.io-index" 2059 | checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 2060 | dependencies = [ 2061 | "futures-core", 2062 | ] 2063 | 2064 | [[package]] 2065 | name = "synstructure" 2066 | version = "0.13.1" 2067 | source = "registry+https://github.com/rust-lang/crates.io-index" 2068 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 2069 | dependencies = [ 2070 | "proc-macro2", 2071 | "quote", 2072 | "syn", 2073 | ] 2074 | 2075 | [[package]] 2076 | name = "system-configuration" 2077 | version = "0.6.1" 2078 | source = "registry+https://github.com/rust-lang/crates.io-index" 2079 | checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" 2080 | dependencies = [ 2081 | "bitflags", 2082 | "core-foundation", 2083 | "system-configuration-sys", 2084 | ] 2085 | 2086 | [[package]] 2087 | name = "system-configuration-sys" 2088 | version = "0.6.0" 2089 | source = "registry+https://github.com/rust-lang/crates.io-index" 2090 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 2091 | dependencies = [ 2092 | "core-foundation-sys", 2093 | "libc", 2094 | ] 2095 | 2096 | [[package]] 2097 | name = "tempfile" 2098 | version = "3.16.0" 2099 | source = "registry+https://github.com/rust-lang/crates.io-index" 2100 | checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" 2101 | dependencies = [ 2102 | "cfg-if", 2103 | "fastrand", 2104 | "getrandom 0.3.1", 2105 | "once_cell", 2106 | "rustix", 2107 | "windows-sys 0.59.0", 2108 | ] 2109 | 2110 | [[package]] 2111 | name = "thiserror" 2112 | version = "1.0.69" 2113 | source = "registry+https://github.com/rust-lang/crates.io-index" 2114 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 2115 | dependencies = [ 2116 | "thiserror-impl 1.0.69", 2117 | ] 2118 | 2119 | [[package]] 2120 | name = "thiserror" 2121 | version = "2.0.11" 2122 | source = "registry+https://github.com/rust-lang/crates.io-index" 2123 | checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" 2124 | dependencies = [ 2125 | "thiserror-impl 2.0.11", 2126 | ] 2127 | 2128 | [[package]] 2129 | name = "thiserror-impl" 2130 | version = "1.0.69" 2131 | source = "registry+https://github.com/rust-lang/crates.io-index" 2132 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 2133 | dependencies = [ 2134 | "proc-macro2", 2135 | "quote", 2136 | "syn", 2137 | ] 2138 | 2139 | [[package]] 2140 | name = "thiserror-impl" 2141 | version = "2.0.11" 2142 | source = "registry+https://github.com/rust-lang/crates.io-index" 2143 | checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" 2144 | dependencies = [ 2145 | "proc-macro2", 2146 | "quote", 2147 | "syn", 2148 | ] 2149 | 2150 | [[package]] 2151 | name = "time" 2152 | version = "0.3.37" 2153 | source = "registry+https://github.com/rust-lang/crates.io-index" 2154 | checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" 2155 | dependencies = [ 2156 | "deranged", 2157 | "num-conv", 2158 | "powerfmt", 2159 | "serde", 2160 | "time-core", 2161 | ] 2162 | 2163 | [[package]] 2164 | name = "time-core" 2165 | version = "0.1.2" 2166 | source = "registry+https://github.com/rust-lang/crates.io-index" 2167 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 2168 | 2169 | [[package]] 2170 | name = "tinystr" 2171 | version = "0.7.6" 2172 | source = "registry+https://github.com/rust-lang/crates.io-index" 2173 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 2174 | dependencies = [ 2175 | "displaydoc", 2176 | "zerovec", 2177 | ] 2178 | 2179 | [[package]] 2180 | name = "tinyvec" 2181 | version = "1.8.1" 2182 | source = "registry+https://github.com/rust-lang/crates.io-index" 2183 | checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" 2184 | dependencies = [ 2185 | "tinyvec_macros", 2186 | ] 2187 | 2188 | [[package]] 2189 | name = "tinyvec_macros" 2190 | version = "0.1.1" 2191 | source = "registry+https://github.com/rust-lang/crates.io-index" 2192 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 2193 | 2194 | [[package]] 2195 | name = "tokio" 2196 | version = "1.43.0" 2197 | source = "registry+https://github.com/rust-lang/crates.io-index" 2198 | checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" 2199 | dependencies = [ 2200 | "backtrace", 2201 | "bytes", 2202 | "libc", 2203 | "mio", 2204 | "parking_lot", 2205 | "pin-project-lite", 2206 | "signal-hook-registry", 2207 | "socket2", 2208 | "tokio-macros", 2209 | "windows-sys 0.52.0", 2210 | ] 2211 | 2212 | [[package]] 2213 | name = "tokio-dl-stream-to-disk" 2214 | version = "1.0.1" 2215 | source = "registry+https://github.com/rust-lang/crates.io-index" 2216 | checksum = "a1f364cd47675674b7d9dead65b7ef11e46bc918f1b058f37195fc81dfb0ef62" 2217 | dependencies = [ 2218 | "bytes", 2219 | "futures-util", 2220 | "reqwest", 2221 | "sha2", 2222 | "tokio", 2223 | "tokio-util", 2224 | ] 2225 | 2226 | [[package]] 2227 | name = "tokio-macros" 2228 | version = "2.5.0" 2229 | source = "registry+https://github.com/rust-lang/crates.io-index" 2230 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 2231 | dependencies = [ 2232 | "proc-macro2", 2233 | "quote", 2234 | "syn", 2235 | ] 2236 | 2237 | [[package]] 2238 | name = "tokio-native-tls" 2239 | version = "0.3.1" 2240 | source = "registry+https://github.com/rust-lang/crates.io-index" 2241 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 2242 | dependencies = [ 2243 | "native-tls", 2244 | "tokio", 2245 | ] 2246 | 2247 | [[package]] 2248 | name = "tokio-rustls" 2249 | version = "0.26.1" 2250 | source = "registry+https://github.com/rust-lang/crates.io-index" 2251 | checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" 2252 | dependencies = [ 2253 | "rustls", 2254 | "tokio", 2255 | ] 2256 | 2257 | [[package]] 2258 | name = "tokio-util" 2259 | version = "0.7.13" 2260 | source = "registry+https://github.com/rust-lang/crates.io-index" 2261 | checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" 2262 | dependencies = [ 2263 | "bytes", 2264 | "futures-core", 2265 | "futures-sink", 2266 | "pin-project-lite", 2267 | "tokio", 2268 | ] 2269 | 2270 | [[package]] 2271 | name = "tower" 2272 | version = "0.5.2" 2273 | source = "registry+https://github.com/rust-lang/crates.io-index" 2274 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 2275 | dependencies = [ 2276 | "futures-core", 2277 | "futures-util", 2278 | "pin-project-lite", 2279 | "sync_wrapper", 2280 | "tokio", 2281 | "tower-layer", 2282 | "tower-service", 2283 | ] 2284 | 2285 | [[package]] 2286 | name = "tower-layer" 2287 | version = "0.3.3" 2288 | source = "registry+https://github.com/rust-lang/crates.io-index" 2289 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 2290 | 2291 | [[package]] 2292 | name = "tower-service" 2293 | version = "0.3.3" 2294 | source = "registry+https://github.com/rust-lang/crates.io-index" 2295 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 2296 | 2297 | [[package]] 2298 | name = "tracing" 2299 | version = "0.1.41" 2300 | source = "registry+https://github.com/rust-lang/crates.io-index" 2301 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 2302 | dependencies = [ 2303 | "pin-project-lite", 2304 | "tracing-core", 2305 | ] 2306 | 2307 | [[package]] 2308 | name = "tracing-core" 2309 | version = "0.1.33" 2310 | source = "registry+https://github.com/rust-lang/crates.io-index" 2311 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 2312 | dependencies = [ 2313 | "once_cell", 2314 | ] 2315 | 2316 | [[package]] 2317 | name = "try-lock" 2318 | version = "0.2.5" 2319 | source = "registry+https://github.com/rust-lang/crates.io-index" 2320 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2321 | 2322 | [[package]] 2323 | name = "typenum" 2324 | version = "1.17.0" 2325 | source = "registry+https://github.com/rust-lang/crates.io-index" 2326 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 2327 | 2328 | [[package]] 2329 | name = "unicode-ident" 2330 | version = "1.0.16" 2331 | source = "registry+https://github.com/rust-lang/crates.io-index" 2332 | checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" 2333 | 2334 | [[package]] 2335 | name = "unicode-width" 2336 | version = "0.2.0" 2337 | source = "registry+https://github.com/rust-lang/crates.io-index" 2338 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 2339 | 2340 | [[package]] 2341 | name = "untrusted" 2342 | version = "0.9.0" 2343 | source = "registry+https://github.com/rust-lang/crates.io-index" 2344 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 2345 | 2346 | [[package]] 2347 | name = "url" 2348 | version = "2.5.4" 2349 | source = "registry+https://github.com/rust-lang/crates.io-index" 2350 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 2351 | dependencies = [ 2352 | "form_urlencoded", 2353 | "idna", 2354 | "percent-encoding", 2355 | ] 2356 | 2357 | [[package]] 2358 | name = "utf16_iter" 2359 | version = "1.0.5" 2360 | source = "registry+https://github.com/rust-lang/crates.io-index" 2361 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 2362 | 2363 | [[package]] 2364 | name = "utf8_iter" 2365 | version = "1.0.4" 2366 | source = "registry+https://github.com/rust-lang/crates.io-index" 2367 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 2368 | 2369 | [[package]] 2370 | name = "utf8parse" 2371 | version = "0.2.2" 2372 | source = "registry+https://github.com/rust-lang/crates.io-index" 2373 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 2374 | 2375 | [[package]] 2376 | name = "vcpkg" 2377 | version = "0.2.15" 2378 | source = "registry+https://github.com/rust-lang/crates.io-index" 2379 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2380 | 2381 | [[package]] 2382 | name = "version_check" 2383 | version = "0.9.5" 2384 | source = "registry+https://github.com/rust-lang/crates.io-index" 2385 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2386 | 2387 | [[package]] 2388 | name = "want" 2389 | version = "0.3.1" 2390 | source = "registry+https://github.com/rust-lang/crates.io-index" 2391 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 2392 | dependencies = [ 2393 | "try-lock", 2394 | ] 2395 | 2396 | [[package]] 2397 | name = "wasi" 2398 | version = "0.11.0+wasi-snapshot-preview1" 2399 | source = "registry+https://github.com/rust-lang/crates.io-index" 2400 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2401 | 2402 | [[package]] 2403 | name = "wasi" 2404 | version = "0.13.3+wasi-0.2.2" 2405 | source = "registry+https://github.com/rust-lang/crates.io-index" 2406 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" 2407 | dependencies = [ 2408 | "wit-bindgen-rt", 2409 | ] 2410 | 2411 | [[package]] 2412 | name = "wasm-bindgen" 2413 | version = "0.2.100" 2414 | source = "registry+https://github.com/rust-lang/crates.io-index" 2415 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 2416 | dependencies = [ 2417 | "cfg-if", 2418 | "once_cell", 2419 | "rustversion", 2420 | "wasm-bindgen-macro", 2421 | ] 2422 | 2423 | [[package]] 2424 | name = "wasm-bindgen-backend" 2425 | version = "0.2.100" 2426 | source = "registry+https://github.com/rust-lang/crates.io-index" 2427 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 2428 | dependencies = [ 2429 | "bumpalo", 2430 | "log", 2431 | "proc-macro2", 2432 | "quote", 2433 | "syn", 2434 | "wasm-bindgen-shared", 2435 | ] 2436 | 2437 | [[package]] 2438 | name = "wasm-bindgen-futures" 2439 | version = "0.4.50" 2440 | source = "registry+https://github.com/rust-lang/crates.io-index" 2441 | checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" 2442 | dependencies = [ 2443 | "cfg-if", 2444 | "js-sys", 2445 | "once_cell", 2446 | "wasm-bindgen", 2447 | "web-sys", 2448 | ] 2449 | 2450 | [[package]] 2451 | name = "wasm-bindgen-macro" 2452 | version = "0.2.100" 2453 | source = "registry+https://github.com/rust-lang/crates.io-index" 2454 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 2455 | dependencies = [ 2456 | "quote", 2457 | "wasm-bindgen-macro-support", 2458 | ] 2459 | 2460 | [[package]] 2461 | name = "wasm-bindgen-macro-support" 2462 | version = "0.2.100" 2463 | source = "registry+https://github.com/rust-lang/crates.io-index" 2464 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 2465 | dependencies = [ 2466 | "proc-macro2", 2467 | "quote", 2468 | "syn", 2469 | "wasm-bindgen-backend", 2470 | "wasm-bindgen-shared", 2471 | ] 2472 | 2473 | [[package]] 2474 | name = "wasm-bindgen-shared" 2475 | version = "0.2.100" 2476 | source = "registry+https://github.com/rust-lang/crates.io-index" 2477 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 2478 | dependencies = [ 2479 | "unicode-ident", 2480 | ] 2481 | 2482 | [[package]] 2483 | name = "wasm-streams" 2484 | version = "0.4.2" 2485 | source = "registry+https://github.com/rust-lang/crates.io-index" 2486 | checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" 2487 | dependencies = [ 2488 | "futures-util", 2489 | "js-sys", 2490 | "wasm-bindgen", 2491 | "wasm-bindgen-futures", 2492 | "web-sys", 2493 | ] 2494 | 2495 | [[package]] 2496 | name = "web-sys" 2497 | version = "0.3.77" 2498 | source = "registry+https://github.com/rust-lang/crates.io-index" 2499 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 2500 | dependencies = [ 2501 | "js-sys", 2502 | "wasm-bindgen", 2503 | ] 2504 | 2505 | [[package]] 2506 | name = "web-time" 2507 | version = "1.1.0" 2508 | source = "registry+https://github.com/rust-lang/crates.io-index" 2509 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 2510 | dependencies = [ 2511 | "js-sys", 2512 | "wasm-bindgen", 2513 | ] 2514 | 2515 | [[package]] 2516 | name = "webpki-roots" 2517 | version = "0.26.8" 2518 | source = "registry+https://github.com/rust-lang/crates.io-index" 2519 | checksum = "2210b291f7ea53617fbafcc4939f10914214ec15aace5ba62293a668f322c5c9" 2520 | dependencies = [ 2521 | "rustls-pki-types", 2522 | ] 2523 | 2524 | [[package]] 2525 | name = "windows-core" 2526 | version = "0.52.0" 2527 | source = "registry+https://github.com/rust-lang/crates.io-index" 2528 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 2529 | dependencies = [ 2530 | "windows-targets", 2531 | ] 2532 | 2533 | [[package]] 2534 | name = "windows-registry" 2535 | version = "0.2.0" 2536 | source = "registry+https://github.com/rust-lang/crates.io-index" 2537 | checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" 2538 | dependencies = [ 2539 | "windows-result", 2540 | "windows-strings", 2541 | "windows-targets", 2542 | ] 2543 | 2544 | [[package]] 2545 | name = "windows-result" 2546 | version = "0.2.0" 2547 | source = "registry+https://github.com/rust-lang/crates.io-index" 2548 | checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 2549 | dependencies = [ 2550 | "windows-targets", 2551 | ] 2552 | 2553 | [[package]] 2554 | name = "windows-strings" 2555 | version = "0.1.0" 2556 | source = "registry+https://github.com/rust-lang/crates.io-index" 2557 | checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 2558 | dependencies = [ 2559 | "windows-result", 2560 | "windows-targets", 2561 | ] 2562 | 2563 | [[package]] 2564 | name = "windows-sys" 2565 | version = "0.52.0" 2566 | source = "registry+https://github.com/rust-lang/crates.io-index" 2567 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2568 | dependencies = [ 2569 | "windows-targets", 2570 | ] 2571 | 2572 | [[package]] 2573 | name = "windows-sys" 2574 | version = "0.59.0" 2575 | source = "registry+https://github.com/rust-lang/crates.io-index" 2576 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2577 | dependencies = [ 2578 | "windows-targets", 2579 | ] 2580 | 2581 | [[package]] 2582 | name = "windows-targets" 2583 | version = "0.52.6" 2584 | source = "registry+https://github.com/rust-lang/crates.io-index" 2585 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2586 | dependencies = [ 2587 | "windows_aarch64_gnullvm", 2588 | "windows_aarch64_msvc", 2589 | "windows_i686_gnu", 2590 | "windows_i686_gnullvm", 2591 | "windows_i686_msvc", 2592 | "windows_x86_64_gnu", 2593 | "windows_x86_64_gnullvm", 2594 | "windows_x86_64_msvc", 2595 | ] 2596 | 2597 | [[package]] 2598 | name = "windows_aarch64_gnullvm" 2599 | version = "0.52.6" 2600 | source = "registry+https://github.com/rust-lang/crates.io-index" 2601 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2602 | 2603 | [[package]] 2604 | name = "windows_aarch64_msvc" 2605 | version = "0.52.6" 2606 | source = "registry+https://github.com/rust-lang/crates.io-index" 2607 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2608 | 2609 | [[package]] 2610 | name = "windows_i686_gnu" 2611 | version = "0.52.6" 2612 | source = "registry+https://github.com/rust-lang/crates.io-index" 2613 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2614 | 2615 | [[package]] 2616 | name = "windows_i686_gnullvm" 2617 | version = "0.52.6" 2618 | source = "registry+https://github.com/rust-lang/crates.io-index" 2619 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2620 | 2621 | [[package]] 2622 | name = "windows_i686_msvc" 2623 | version = "0.52.6" 2624 | source = "registry+https://github.com/rust-lang/crates.io-index" 2625 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2626 | 2627 | [[package]] 2628 | name = "windows_x86_64_gnu" 2629 | version = "0.52.6" 2630 | source = "registry+https://github.com/rust-lang/crates.io-index" 2631 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2632 | 2633 | [[package]] 2634 | name = "windows_x86_64_gnullvm" 2635 | version = "0.52.6" 2636 | source = "registry+https://github.com/rust-lang/crates.io-index" 2637 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2638 | 2639 | [[package]] 2640 | name = "windows_x86_64_msvc" 2641 | version = "0.52.6" 2642 | source = "registry+https://github.com/rust-lang/crates.io-index" 2643 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2644 | 2645 | [[package]] 2646 | name = "wit-bindgen-rt" 2647 | version = "0.33.0" 2648 | source = "registry+https://github.com/rust-lang/crates.io-index" 2649 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" 2650 | dependencies = [ 2651 | "bitflags", 2652 | ] 2653 | 2654 | [[package]] 2655 | name = "write16" 2656 | version = "1.0.0" 2657 | source = "registry+https://github.com/rust-lang/crates.io-index" 2658 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 2659 | 2660 | [[package]] 2661 | name = "writeable" 2662 | version = "0.5.5" 2663 | source = "registry+https://github.com/rust-lang/crates.io-index" 2664 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 2665 | 2666 | [[package]] 2667 | name = "x509-certificate" 2668 | version = "0.24.0" 2669 | source = "registry+https://github.com/rust-lang/crates.io-index" 2670 | checksum = "e57b9f8bcae7c1f36479821ae826d75050c60ce55146fd86d3553ed2573e2762" 2671 | dependencies = [ 2672 | "bcder", 2673 | "bytes", 2674 | "chrono", 2675 | "der", 2676 | "hex", 2677 | "pem", 2678 | "ring", 2679 | "signature", 2680 | "spki", 2681 | "thiserror 1.0.69", 2682 | "zeroize", 2683 | ] 2684 | 2685 | [[package]] 2686 | name = "yoke" 2687 | version = "0.7.5" 2688 | source = "registry+https://github.com/rust-lang/crates.io-index" 2689 | checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" 2690 | dependencies = [ 2691 | "serde", 2692 | "stable_deref_trait", 2693 | "yoke-derive", 2694 | "zerofrom", 2695 | ] 2696 | 2697 | [[package]] 2698 | name = "yoke-derive" 2699 | version = "0.7.5" 2700 | source = "registry+https://github.com/rust-lang/crates.io-index" 2701 | checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" 2702 | dependencies = [ 2703 | "proc-macro2", 2704 | "quote", 2705 | "syn", 2706 | "synstructure", 2707 | ] 2708 | 2709 | [[package]] 2710 | name = "zerocopy" 2711 | version = "0.7.35" 2712 | source = "registry+https://github.com/rust-lang/crates.io-index" 2713 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 2714 | dependencies = [ 2715 | "byteorder", 2716 | "zerocopy-derive", 2717 | ] 2718 | 2719 | [[package]] 2720 | name = "zerocopy-derive" 2721 | version = "0.7.35" 2722 | source = "registry+https://github.com/rust-lang/crates.io-index" 2723 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 2724 | dependencies = [ 2725 | "proc-macro2", 2726 | "quote", 2727 | "syn", 2728 | ] 2729 | 2730 | [[package]] 2731 | name = "zerofrom" 2732 | version = "0.1.5" 2733 | source = "registry+https://github.com/rust-lang/crates.io-index" 2734 | checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" 2735 | dependencies = [ 2736 | "zerofrom-derive", 2737 | ] 2738 | 2739 | [[package]] 2740 | name = "zerofrom-derive" 2741 | version = "0.1.5" 2742 | source = "registry+https://github.com/rust-lang/crates.io-index" 2743 | checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" 2744 | dependencies = [ 2745 | "proc-macro2", 2746 | "quote", 2747 | "syn", 2748 | "synstructure", 2749 | ] 2750 | 2751 | [[package]] 2752 | name = "zeroize" 2753 | version = "1.8.1" 2754 | source = "registry+https://github.com/rust-lang/crates.io-index" 2755 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 2756 | dependencies = [ 2757 | "zeroize_derive", 2758 | ] 2759 | 2760 | [[package]] 2761 | name = "zeroize_derive" 2762 | version = "1.4.2" 2763 | source = "registry+https://github.com/rust-lang/crates.io-index" 2764 | checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 2765 | dependencies = [ 2766 | "proc-macro2", 2767 | "quote", 2768 | "syn", 2769 | ] 2770 | 2771 | [[package]] 2772 | name = "zerovec" 2773 | version = "0.10.4" 2774 | source = "registry+https://github.com/rust-lang/crates.io-index" 2775 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 2776 | dependencies = [ 2777 | "yoke", 2778 | "zerofrom", 2779 | "zerovec-derive", 2780 | ] 2781 | 2782 | [[package]] 2783 | name = "zerovec-derive" 2784 | version = "0.10.3" 2785 | source = "registry+https://github.com/rust-lang/crates.io-index" 2786 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 2787 | dependencies = [ 2788 | "proc-macro2", 2789 | "quote", 2790 | "syn", 2791 | ] 2792 | 2793 | [[package]] 2794 | name = "zip" 2795 | version = "2.2.2" 2796 | source = "registry+https://github.com/rust-lang/crates.io-index" 2797 | checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" 2798 | dependencies = [ 2799 | "aes", 2800 | "arbitrary", 2801 | "bzip2", 2802 | "constant_time_eq", 2803 | "crc32fast", 2804 | "crossbeam-utils", 2805 | "deflate64", 2806 | "displaydoc", 2807 | "flate2", 2808 | "hmac", 2809 | "indexmap", 2810 | "lzma-rs", 2811 | "memchr", 2812 | "pbkdf2", 2813 | "rand", 2814 | "sha1", 2815 | "thiserror 2.0.11", 2816 | "time", 2817 | "zeroize", 2818 | "zopfli", 2819 | "zstd", 2820 | ] 2821 | 2822 | [[package]] 2823 | name = "zopfli" 2824 | version = "0.8.1" 2825 | source = "registry+https://github.com/rust-lang/crates.io-index" 2826 | checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" 2827 | dependencies = [ 2828 | "bumpalo", 2829 | "crc32fast", 2830 | "lockfree-object-pool", 2831 | "log", 2832 | "once_cell", 2833 | "simd-adler32", 2834 | ] 2835 | 2836 | [[package]] 2837 | name = "zstd" 2838 | version = "0.13.2" 2839 | source = "registry+https://github.com/rust-lang/crates.io-index" 2840 | checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" 2841 | dependencies = [ 2842 | "zstd-safe", 2843 | ] 2844 | 2845 | [[package]] 2846 | name = "zstd-safe" 2847 | version = "7.2.1" 2848 | source = "registry+https://github.com/rust-lang/crates.io-index" 2849 | checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" 2850 | dependencies = [ 2851 | "zstd-sys", 2852 | ] 2853 | 2854 | [[package]] 2855 | name = "zstd-sys" 2856 | version = "2.0.13+zstd.1.5.6" 2857 | source = "registry+https://github.com/rust-lang/crates.io-index" 2858 | checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" 2859 | dependencies = [ 2860 | "cc", 2861 | "pkg-config", 2862 | ] 2863 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "apkeep" 3 | version = "0.17.0" 4 | authors = ["William Budington "] 5 | edition = "2021" 6 | license = "MIT" 7 | description = "A command-line tool for downloading APK files from various sources" 8 | homepage = "https://github.com/EFForg/apkeep" 9 | repository = "https://github.com/EFForg/apkeep" 10 | 11 | [dependencies] 12 | tokio = { version = "1", features = ["full"] } 13 | reqwest = { version = "0.12", features = ["stream"] } 14 | futures-util = { version = "0.3", features = ["io"] } 15 | regex = "1.11" 16 | clap = { version = "4", features = ["derive"] } 17 | gpapi = "5" 18 | tokio-dl-stream-to-disk = { version = "1", features = ["sha256sum"] } 19 | tempfile = "3" 20 | dirs = "6" 21 | zip = "2" 22 | cryptographic-message-syntax = "0.27" 23 | ring = "0.17" 24 | x509-certificate = "0.24" 25 | simple-error = "0.3" 26 | sha1 = "0.10" 27 | sha2 = "0.10" 28 | base64 = "0.22" 29 | serde_json = "1" 30 | hex = "0.4" 31 | configparser = "3" 32 | serde = { version = "1", features = ["derive"] } 33 | indicatif = "0.17" 34 | 35 | [build-dependencies] 36 | clap = { version = "4", features = ["derive"] } 37 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:latest 2 | 3 | COPY . /app 4 | 5 | WORKDIR /app 6 | 7 | RUN cargo install --path . 8 | 9 | ENTRYPOINT ["apkeep"] 10 | -------------------------------------------------------------------------------- /HACKING.md: -------------------------------------------------------------------------------- 1 | # `apkeep` - A command-line tool for downloading APK files from various sources 2 | 3 | To build `apkeep` from source, simply [install rust](https://www.rust-lang.org/tools/install) and in the repository path run 4 | 5 | ```shell 6 | cargo build 7 | ``` 8 | 9 | If you wish to build the release version, run 10 | 11 | ```shell 12 | cargo build --release 13 | ``` 14 | 15 | This will compile the binaries and put them in a new `target/` path. 16 | 17 | To build and run all in one step, run 18 | 19 | ```shell 20 | cargo run -- ARGS 21 | ``` 22 | 23 | or 24 | 25 | ```shell 26 | cargo run --release -- ARGS 27 | ``` 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Electronic Frontier Foundation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # `apkeep` - A command-line tool for downloading APK files from various sources 3 | 4 | [![crates.io](https://img.shields.io/crates/v/apkeep.svg)](https://crates.io/crates/apkeep) 5 | [![MIT licensed](https://img.shields.io/crates/l/apkeep.svg)](./LICENSE) 6 | 7 | ![apkeep logo](logo.png) 8 | 9 | ## Installation 10 | 11 | Precompiled binaries for `apkeep` on various platforms can be downloaded 12 | [here](https://github.com/EFForg/apkeep/releases). 13 | 14 | To install from `crates.io`, simply [install rust](https://www.rust-lang.org/tools/install) and 15 | run 16 | 17 | ```shell 18 | cargo install apkeep 19 | ``` 20 | 21 | Or to install from the latest commit in our repository, run 22 | 23 | ```shell 24 | cargo install --git https://github.com/EFForg/apkeep.git 25 | ``` 26 | 27 | If using on an Android platform, [`termux`](https://termux.org/) must be installed first. 28 | Upgrade to the latest packages with `pkg update`, then install the `apkeep` precompiled binary 29 | as described above or run `pkg install apkeep` to install from the `termux` repository. 30 | 31 | Docker images are also available through the GitHub Container Registry. Aside from using a 32 | specific release version, the following floating tags are available: 33 | 34 | - stable: tracks the latest stable release (recommended) 35 | - latest: tracks the latest release, including pre-releases 36 | - edge: tracks the latest commit 37 | 38 | ## Usage 39 | 40 | See [`USAGE`](https://github.com/EFForg/apkeep/blob/master/USAGE). 41 | 42 | ## Examples 43 | 44 | The simplest example is to download a single APK to the current directory: 45 | 46 | ```shell 47 | apkeep -a com.instagram.android . 48 | ``` 49 | 50 | This downloads from the default source, APKPure, which does not require credentials. To 51 | download directly from the google play store, you will first have to [obtain an AAS token](USAGE-google-play.md). 52 | Then, 53 | 54 | ```shell 55 | apkeep -a com.instagram.android -d google-play -e 'someone@gmail.com' -t aas_token . 56 | ``` 57 | 58 | For more google play usage examples, such as specifying a device configuration, timezone or 59 | locale, refer to the [`USAGE-google-play.md`](USAGE-google-play.md) document. 60 | 61 | To download from the F-Droid open source repository: 62 | 63 | ```shell 64 | apkeep -a org.mozilla.fennec_fdroid -d f-droid . 65 | ``` 66 | 67 | For more F-Droid usage examples, such as downloading from F-Droid mirrors or other F-Droid 68 | repositories, refer to the [`USAGE-fdroid.md`](USAGE-fdroid.md) document. 69 | 70 | Or, to download from the Huawei AppGallery: 71 | 72 | ```shell 73 | apkeep -a com.elysiumlabs.newsbytes -d huawei-app-gallery . 74 | ``` 75 | 76 | To download a specific version of an APK (possible for APKPure or F-Droid), use the `@version` 77 | convention: 78 | 79 | ```shell 80 | apkeep -a com.instagram.android@1.2.3 . 81 | ``` 82 | 83 | Or, to list what versions are available, use `-l`: 84 | 85 | ```shell 86 | apkeep -l -a org.mozilla.fennec_fdroid -d f-droid 87 | ``` 88 | 89 | Refer to [`USAGE`](https://github.com/EFForg/apkeep/blob/master/USAGE) to download multiple 90 | APKs in a single run. 91 | 92 | All the above examples can also be used in Docker with minimal changes. For example, to 93 | download a single APK to your chosen output directory: 94 | 95 | ```shell 96 | docker run --rm -v output_path:/output ghcr.io/efforg/apkeep:stable -a com.instagram.android 97 | /output 98 | ``` 99 | 100 | ## Specify a CSV file or individual app ID 101 | 102 | You can either specify a CSV file which lists the apps to download, or an individual app ID. 103 | If you specify a CSV file and the app ID is not specified by the first column, you'll have to 104 | use the --field option as well. If you have a simple file with one app ID per line, you can 105 | just treat it as a CSV with a single field. 106 | 107 | ## Download Sources 108 | 109 | You can use this tool to download from a few distinct sources. 110 | 111 | * The Google Play Store (`-d google-play`), given an email address and AAS token 112 | * APKPure (`-d apk-pure`), a third-party site hosting APKs available on the Play Store 113 | * F-Droid (`-d f-droid`), a repository for free and open-source Android apps. `apkeep` 114 | verifies that these APKs are signed by the F-Droid maintainers, and alerts the user if an APK 115 | was downloaded but could not be verified 116 | * The Huawei AppGallery (`-d huawei-app-gallery`), an app store popular in China 117 | 118 | ## Usage Note 119 | 120 | Users should not use app lists or choose so many parallel APK fetches as to place unreasonable 121 | or disproportionately large load on the infrastructure of the app distributor. 122 | 123 | When using with the Google Play Store as the download source, a few considerations should be 124 | made: 125 | 126 | * Google may terminate your Google account based on Terms of Service violations. Read their 127 | [Terms of Service](https://play.google.com/about/play-terms/index.html), avoid violating it, 128 | and choose an account where this outcome is acceptable. 129 | * Paid and DRM apps will not be available. 130 | * Using Tor will make it a lot more likely that the download will fail. 131 | 132 | License: MIT 133 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | 2 | # `apkeep` - A command-line tool for downloading APK files from various sources 3 | 4 | [![crates.io](https://img.shields.io/crates/v/apkeep.svg)](https://crates.io/crates/apkeep) 5 | [![MIT licensed](https://img.shields.io/crates/l/apkeep.svg)](./LICENSE) 6 | 7 | ![apkeep logo](logo.png) 8 | 9 | {{readme}} 10 | 11 | License: {{license}} 12 | -------------------------------------------------------------------------------- /USAGE: -------------------------------------------------------------------------------- 1 | Downloads APKs from various sources 2 | 3 | Usage: apkeep <-a app_id[@version] | -c csv [-f field] [-v version_field]> [-d download_source] [-r parallel] OUTPATH 4 | 5 | Arguments: 6 | [OUTPATH] Path to store output files 7 | 8 | Options: 9 | -a, --app 10 | Provide the ID and optionally the version of an app directly (e.g. com.instagram.android) 11 | -c, --csv 12 | CSV file to use 13 | -f, --field 14 | CSV field containing app IDs (used only if CSV is specified) [default: 1] 15 | -v, --version-field 16 | CSV field containing versions (used only if CSV is specified) 17 | -l, --list-versions 18 | List the versions available 19 | -d, --download-source 20 | Where to download the APKs from [default: apk-pure] [possible values: apk-pure, google-play, f-droid, huawei-app-gallery] 21 | -o, --options 22 | A comma-separated list of additional options to pass to the download source 23 | -i, --ini 24 | The path to an ini file which contains configuration data 25 | --oauth-token 26 | Google oauth token, required to retrieve long-lived aas token 27 | -e, --email 28 | Google account email address (required if download source is Google Play) 29 | -t, --aas-token 30 | Google aas token (required if download source is Google Play) 31 | --accept-tos 32 | Accept Google Play Terms of Service 33 | -s, --sleep-duration 34 | Sleep duration (in ms) before download requests [default: 0] 35 | -r, --parallel 36 | The number of parallel APK fetches to run at a time [default: 4] 37 | -h, --help 38 | Print help 39 | -V, --version 40 | Print version 41 | -------------------------------------------------------------------------------- /USAGE-fdroid.md: -------------------------------------------------------------------------------- 1 | To download from the F-Droid open source repository: 2 | 3 | ```shell 4 | apkeep -a org.mozilla.fennec_fdroid -d f-droid . 5 | ``` 6 | 7 | More advanced options for this download source can be passed with the `-o` option. For instance, to use an F-Droid mirror: 8 | 9 | ```shell 10 | apkeep -a org.mozilla.fennec_fdroid -d f-droid -o repo=https://cloudflare.f-droid.org/repo . 11 | ``` 12 | 13 | In addition to specifying a mirror, a wholly separate F-Droid repo can be specified along with its fingerprint: 14 | 15 | ```shell 16 | apkeep -a org.torproject.android -d f-droid -o repo=https://guardianproject.info/fdroid/repo?fingerprint=B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135 . 17 | ``` 18 | 19 | If a repo supports the new [entry point specification](https://f-droid.org/docs/All_our_APIs/#the-repo-index), you can specify that be used instead of the older (v1) package index. This may become the default behavior in the future, but can be specified by use of the `use_entry` option: 20 | 21 | ```shell 22 | apkeep -a org.torproject.android -d f-droid -o repo=https://guardianproject.info/fdroid/repo?fingerprint=B7C2EEFD8DAC7806AF67DFCD92EB18126BC08312A7F2D6F3862E46013C7A6135,use_entry=true . 23 | ``` 24 | 25 | A special option can also be used to skip verification of the repository index. *Only use for debugging purposes*: 26 | 27 | ```shell 28 | apkeep -a org.torproject.android -d f-droid -o repo=https://guardianproject.info/fdroid/repo,verify-index=false . 29 | ``` 30 | 31 | It is also possible to download a specific architecture variant of an app with the `arch=` option: 32 | 33 | ```shell 34 | apkeep -a org.videloan.vlc@3.5.4 -d f-droid -o arch=arm64-v8a . 35 | ``` 36 | 37 | To list what versions are available, use `-l`: 38 | 39 | ```shell 40 | apkeep -l -a org.mozilla.fennec_fdroid -d f-droid 41 | ``` 42 | 43 | To output the list in json, use the `output_format=json` option: 44 | 45 | ```shell 46 | apkeep -l -a org.mozilla.fennec_fdroid -d f-droid -o output_format=json 47 | ``` 48 | -------------------------------------------------------------------------------- /USAGE-google-play.md: -------------------------------------------------------------------------------- 1 | To download directly from the Google Play Store, first you'll have to obtain an OAuth token by visiting the Google [embedded setup page](https://accounts.google.com/EmbeddedSetup) and: 2 | 3 | - Opening the browser debugging console on `Network` tab 4 | - Logging in 5 | - If the "Google Terms of Services" pop up, click `I agree` (it can hang up on this step but it's not important) 6 | - Select the last request from `accounts.google.com` in the `Network` tab 7 | - Select the `Cookies` tab of this request 8 | - One of the response cookie is `oauth_token` 9 | - Copy the `value` field (it must start with `oauth2_4/`) 10 | 11 | It can only be used once, in order to obtain the AAS token which can be used subsequently. To obtain this token: 12 | 13 | ```shell 14 | apkeep -e 'someone@gmail.com' --oauth-token oauth2_4/... 15 | ``` 16 | 17 | An AAS token should be printed. You can use this to download an app: 18 | 19 | ```shell 20 | apkeep -a com.instagram.android -d google-play -e 'someone@gmail.com' -t some_aas_token . 21 | ``` 22 | 23 | This will use a default device configuration of `px_7a`, a timezone of `UTC`, and a locale of `en_US`. To specify a different device profile, use the `-o` option: 24 | 25 | ```shell 26 | apkeep -a com.instagram.android -d google-play -o device=ad_g3_pro -e 'someone@gmail.com' -t some_aas_token . 27 | ``` 28 | 29 | Available devices are specified [here](https://github.com/EFForg/rs-google-play/blob/master/gpapi/device.properties). 30 | 31 | Likewise, a separate timezone or locale can also be specified: 32 | 33 | ```shell 34 | apkeep -a com.instagram.android -d google-play -o device=ad_g3_pro,locale=es_MX -e 'someone@gmail.com' -t some_aas_token . 35 | ``` 36 | 37 | This option attempts to download a split APK if available, and falls back to the full APK: 38 | 39 | ```shell 40 | apkeep -a hk.easyvan.app.client -d google-play -o split_apk=true -e 'someone@gmail.com' -t some_aas_token . 41 | ``` 42 | 43 | A full list of options: 44 | 45 | * `device`: specify a device profile as described above 46 | * `locale`: specify a locale 47 | * `split_apk`: when set to `1` or `true`, attempts to download a [split APK](https://developer.android.com/studio/build/configure-apk-splits) 48 | * `include_additional_files`: when set to `1` or `true`, attempts to download any [additional `obb` expansion files](https://developer.android.com/google/play/expansion-files) for the app 49 | 50 | If you prefer not to provide your credentials on the command line, you can specify them in a config file named `apkeep.ini`. This config file may have to be created, and must be located in the user config directory under the subpath `apkeep`. Usually on Linux systems this will be `~/.config/apkeep/apkeep.ini`. In this file specify your email and/or AAS token: 51 | 52 | ```ini 53 | [google] 54 | email = someone@gmail.com 55 | aas_token = some_aas_token 56 | ``` 57 | 58 | Optionally, the path to this `ini` file can be specified: 59 | 60 | ```shell 61 | apkeep -a com.instagram.android -d google-play -i ~/path/to/some.ini . 62 | ``` 63 | -------------------------------------------------------------------------------- /build-remote.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Build `apkeep` for release from a fresh Debian 12 x64 install 3 | 4 | ssh -o 'StrictHostKeyChecking no' apkeep-compiler << 'EOF' 5 | sudo dpkg --add-architecture armhf 6 | sudo dpkg --add-architecture i386 7 | sudo dpkg --add-architecture arm64 8 | sudo apt-get -y update 9 | sudo apt-get -y dist-upgrade 10 | sudo apt-get -y install git build-essential libssl-dev pkg-config unzip gcc-multilib 11 | sudo apt-get -y install libc6-armhf-cross libc6-dev-armhf-cross gcc-arm-linux-gnueabihf libssl-dev:armhf 12 | sudo apt-get -y install libc6-i386-cross libc6-dev-i386-cross gcc-i686-linux-gnu libssl-dev:i386 13 | sudo apt-get -y install libc6-arm64-cross libc6-dev-arm64-cross gcc-aarch64-linux-gnu libssl-dev:arm64 14 | sudo apt-get -y install clang-16 llvm-16 lld-16 15 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/get_rust.sh 16 | bash /tmp/get_rust.sh -y 17 | source ~/.cargo/env 18 | rustup target install armv7-unknown-linux-gnueabihf i686-unknown-linux-gnu aarch64-unknown-linux-gnu aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android x86_64-pc-windows-msvc 19 | 20 | git clone https://www.github.com/EFForg/apkeep.git 21 | cd apkeep 22 | export PKG_CONFIG_ALLOW_CROSS="true" 23 | cargo build --release 24 | export PKG_CONFIG_PATH="/usr/lib/arm-linux-gnueabihf/pkgconfig" 25 | cargo build --release --target=armv7-unknown-linux-gnueabihf 26 | export PKG_CONFIG_PATH="/usr/lib/i686-linux-gnu-gcc/pkgconfig" 27 | cargo build --release --target=i686-unknown-linux-gnu 28 | export PKG_CONFIG_PATH="/usr/lib/aarch-linux-gnu-gcc/pkgconfig" 29 | cargo build --release --target=aarch64-unknown-linux-gnu 30 | 31 | cd ~ 32 | wget https://www.openssl.org/source/openssl-3.3.2.tar.gz 33 | tar -zxvf openssl-3.3.2.tar.gz 34 | cd openssl-3.3.2 35 | wget https://raw.githubusercontent.com/EFForg/apkeep-files/main/Configurations-15-android.conf.patch 36 | patch -u Configurations/15-android.conf Configurations-15-android.conf.patch 37 | export OPENSSL_DIR=$PWD 38 | export OPENSSL_LIB_DIR=$PWD 39 | 40 | cd ~ 41 | wget https://dl.google.com/android/repository/android-ndk-r26c-linux.zip 42 | unzip android-ndk-r26c-linux.zip 43 | cd android-ndk-r26c 44 | export ANDROID_NDK_ROOT="$PWD" 45 | export OLDPATH="$PATH" 46 | export NEWPATH="$PWD/toolchains/llvm/prebuilt/linux-x86_64/bin" 47 | export PATH="$NEWPATH:$PATH" 48 | export AR="llvm-ar" 49 | cd $NEWPATH 50 | ln -s armv7a-linux-androideabi26-clang arm-linux-androideabi-clang 51 | ln -s i686-linux-android26-clang i686-linux-android-clang 52 | 53 | cd $OPENSSL_DIR 54 | ./Configure android-arm64 -D__ANDROID_MIN_SDK_VERSION__=26 55 | make 56 | cd ../apkeep 57 | cargo build --release --target=aarch64-linux-android 58 | 59 | cd $OPENSSL_DIR 60 | make clean 61 | ./Configure android-arm -D__ANDROID_MIN_SDK_VERSION__=26 62 | make 63 | cd ../apkeep 64 | cargo build --release --target=armv7-linux-androideabi 65 | 66 | cd $OPENSSL_DIR 67 | make clean 68 | ./Configure android-x86 -D__ANDROID_MIN_SDK_VERSION__=26 69 | make 70 | cd ../apkeep 71 | cargo build --release --target=i686-linux-android 72 | 73 | cd $OPENSSL_DIR 74 | make clean 75 | ./Configure android-x86_64 -D__ANDROID_MIN_SDK_VERSION__=26 76 | make 77 | cd ../apkeep 78 | cargo build --release --target=x86_64-linux-android 79 | 80 | export PATH="$OLDPATH" 81 | unset AR 82 | 83 | sudo ln -s clang-16 /usr/bin/clang && sudo ln -s clang /usr/bin/clang++ && sudo ln -s lld-16 /usr/bin/ld.lld 84 | sudo ln -s clang-16 /usr/bin/clang-cl && sudo ln -s llvm-ar-16 /usr/bin/llvm-lib && sudo ln -s lld-link-16 /usr/bin/lld-link && sudo ln -s lld-link /usr/bin/link.exe 85 | 86 | cd ~ 87 | XWIN_VERSION="0.6.5" 88 | XWIN_PREFIX="xwin-$XWIN_VERSION-x86_64-unknown-linux-musl" 89 | curl --fail -L https://github.com/Jake-Shadle/xwin/releases/download/$XWIN_VERSION/$XWIN_PREFIX.tar.gz | tar -xzv -C ~/.cargo/bin --strip-components=1 $XWIN_PREFIX/xwin 90 | cd ~ && mkdir xwin 91 | xwin --accept-license splat --output xwin 92 | 93 | export CC_x86_64_pc_windows_msvc="clang-cl" 94 | export CXX_x86_64_pc_windows_msvc="clang-cl" 95 | export AR_x86_64_pc_windows_msvc="llvm-lib" 96 | export CL_FLAGS="-Wno-unused-command-line-argument -fuse-ld=lld-link /imsvc$HOME/xwin/crt/include /imsvc$HOME/xwin/sdk/include/ucrt /imsvc$HOME/xwin/sdk/include/um /imsvc$HOME/xwin/sdk/include/shared" 97 | export RUSTFLAGS="-Lnative=$HOME/xwin/crt/lib/x86_64 -Lnative=$HOME/xwin/sdk/lib/um/x86_64 -Lnative=$HOME/xwin/sdk/lib/ucrt/x86_64" 98 | export CFLAGS_x86_64_pc_windows_msvc="$CL_FLAGS" 99 | export CXXFLAGS_x86_64_pc_windows_msvc="$CL_FLAGS" 100 | 101 | cd ~/apkeep 102 | cargo build --release --target x86_64-pc-windows-msvc 103 | EOF 104 | 105 | scp apkeep-compiler:~/apkeep/target/release/apkeep ./apkeep-x86_64-unknown-linux-gnu 106 | scp apkeep-compiler:~/apkeep/target/armv7-unknown-linux-gnueabihf/release/apkeep ./apkeep-armv7-unknown-linux-gnueabihf 107 | scp apkeep-compiler:~/apkeep/target/i686-unknown-linux-gnu/release/apkeep ./apkeep-i686-unknown-linux-gnu 108 | scp apkeep-compiler:~/apkeep/target/aarch64-unknown-linux-gnu/release/apkeep ./apkeep-aarch64-unknown-linux-gnu 109 | scp apkeep-compiler:~/apkeep/target/aarch64-linux-android/release/apkeep ./apkeep-aarch64-linux-android 110 | scp apkeep-compiler:~/apkeep/target/armv7-linux-androideabi/release/apkeep ./apkeep-armv7-linux-androideabi 111 | scp apkeep-compiler:~/apkeep/target/i686-linux-android/release/apkeep ./apkeep-i686-linux-android 112 | scp apkeep-compiler:~/apkeep/target/x86_64-linux-android/release/apkeep ./apkeep-x86_64-linux-android 113 | scp apkeep-compiler:~/apkeep/target/x86_64-pc-windows-msvc/release/apkeep.exe ./apkeep-x86_64-pc-windows-msvc.exe 114 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::fs::File; 2 | use std::io::Write; 3 | 4 | include!("src/cli.rs"); 5 | 6 | fn main() { 7 | let mut file = File::create("USAGE").unwrap(); 8 | let help = app().render_help(); 9 | file.write_all(help.to_string().as_bytes()).unwrap(); 10 | } 11 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EFForg/apkeep/2f4a1597939789a8e8cf0ece68c5911a25577a7d/logo.png -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{value_parser, Command, Arg, ArgAction, ValueEnum, {builder::EnumValueParser}}; 2 | 3 | #[derive(Copy, Clone, PartialEq, Eq, ValueEnum)] 4 | pub enum DownloadSource { 5 | APKPure, 6 | GooglePlay, 7 | FDroid, 8 | HuaweiAppGallery, 9 | } 10 | 11 | impl std::fmt::Display for DownloadSource { 12 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 13 | self.to_possible_value() 14 | .expect("no values are skipped") 15 | .get_name() 16 | .fmt(f) 17 | } 18 | } 19 | 20 | impl std::str::FromStr for DownloadSource { 21 | type Err = String; 22 | 23 | fn from_str(s: &str) -> Result { 24 | for variant in Self::value_variants() { 25 | if variant.to_possible_value().unwrap().matches(s, false) { 26 | return Ok(*variant); 27 | } 28 | } 29 | Err(format!("Invalid variant: {}", s)) 30 | } 31 | } 32 | 33 | pub fn app() -> Command { 34 | Command::new("apkeep") 35 | .version(env!("CARGO_PKG_VERSION")) 36 | .author("William Budington ") 37 | .about("Downloads APKs from various sources") 38 | .override_usage("apkeep <-a app_id[@version] | -c csv [-f field] [-v version_field]> [-d download_source] [-r parallel] OUTPATH") 39 | .arg( 40 | Arg::new("app") 41 | .help("Provide the ID and optionally the version of an app directly (e.g. com.instagram.android)") 42 | .short('a') 43 | .long("app") 44 | .action(ArgAction::Set) 45 | .conflicts_with("csv") 46 | .required_unless_present_any(["csv", "google_oauth_token"]), 47 | ) 48 | .arg( 49 | Arg::new("csv") 50 | .help("CSV file to use") 51 | .short('c') 52 | .long("csv") 53 | .action(ArgAction::Set), 54 | ) 55 | .arg( 56 | Arg::new("field") 57 | .help("CSV field containing app IDs (used only if CSV is specified)") 58 | .short('f') 59 | .long("field") 60 | .action(ArgAction::Set) 61 | .value_parser(value_parser!(usize)) 62 | .default_value("1"), 63 | ) 64 | .arg( 65 | Arg::new("version_field") 66 | .help("CSV field containing versions (used only if CSV is specified)") 67 | .short('v') 68 | .long("version-field") 69 | .action(ArgAction::Set) 70 | .value_parser(value_parser!(usize)) 71 | .required(false), 72 | ) 73 | .arg( 74 | Arg::new("list_versions") 75 | .help("List the versions available") 76 | .short('l') 77 | .long("list-versions") 78 | .action(ArgAction::SetTrue) 79 | .required(false), 80 | ) 81 | .arg( 82 | Arg::new("download_source") 83 | .help("Where to download the APKs from") 84 | .short('d') 85 | .long("download-source") 86 | .default_value("apk-pure") 87 | .action(ArgAction::Set) 88 | .value_parser(EnumValueParser::::new()) 89 | .required(false), 90 | ) 91 | .arg( 92 | Arg::new("options") 93 | .help("A comma-separated list of additional options to pass to the download source") 94 | .short('o') 95 | .long("options") 96 | .action(ArgAction::Set) 97 | .required(false), 98 | ) 99 | .arg( 100 | Arg::new("ini") 101 | .help("The path to an ini file which contains configuration data") 102 | .short('i') 103 | .long("ini") 104 | .action(ArgAction::Set) 105 | .required(false), 106 | ) 107 | .arg( 108 | Arg::new("google_oauth_token") 109 | .help("Google oauth token, required to retrieve long-lived aas token") 110 | .long("oauth-token") 111 | .action(ArgAction::Set) 112 | ) 113 | .arg( 114 | Arg::new("google_email") 115 | .help("Google account email address (required if download source is Google Play)") 116 | .short('e') 117 | .long("email") 118 | .action(ArgAction::Set) 119 | ) 120 | .arg( 121 | Arg::new("google_aas_token") 122 | .help("Google aas token (required if download source is Google Play)") 123 | .short('t') 124 | .long("aas-token") 125 | .action(ArgAction::Set) 126 | ) 127 | .arg( 128 | Arg::new("google_accept_tos") 129 | .help("Accept Google Play Terms of Service") 130 | .long("accept-tos") 131 | .action(ArgAction::SetTrue) 132 | ) 133 | .arg( 134 | Arg::new("sleep_duration") 135 | .help("Sleep duration (in ms) before download requests") 136 | .short('s') 137 | .long("sleep-duration") 138 | .action(ArgAction::Set) 139 | .value_parser(value_parser!(u64)) 140 | .default_value("0"), 141 | ) 142 | .arg( 143 | Arg::new("parallel") 144 | .help("The number of parallel APK fetches to run at a time") 145 | .short('r') 146 | .long("parallel") 147 | .action(ArgAction::Set) 148 | .value_parser(value_parser!(usize)) 149 | .default_value("4") 150 | .required(false), 151 | ) 152 | .arg( 153 | Arg::new("OUTPATH") 154 | .help("Path to store output files") 155 | .action(ArgAction::Set) 156 | .index(1) 157 | .required_unless_present_any(["list_versions", "google_oauth_token"]), 158 | ) 159 | } 160 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | use std::fs; 4 | use std::path::PathBuf; 5 | 6 | #[derive(Debug)] 7 | pub enum ConfigDirError { 8 | NotFound, 9 | CouldNotCreate, 10 | } 11 | 12 | impl Error for ConfigDirError {} 13 | 14 | impl fmt::Display for ConfigDirError { 15 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 16 | match self { 17 | Self::NotFound => write!(f, "NotFound"), 18 | Self::CouldNotCreate => write!(f, "CouldNotCreate"), 19 | } 20 | } 21 | } 22 | 23 | pub fn create_dir(config_dir: &PathBuf) -> Result<(), ConfigDirError> { 24 | if !config_dir.is_dir() { 25 | fs::create_dir(config_dir).map_err(|_| { ConfigDirError::CouldNotCreate } )?; 26 | } 27 | Ok(()) 28 | } 29 | 30 | pub fn config_dir() -> Result { 31 | let mut config_dir = dirs::config_dir().ok_or(ConfigDirError::NotFound)?; 32 | create_dir(&config_dir)?; 33 | config_dir.push("apkeep"); 34 | create_dir(&config_dir)?; 35 | Ok(config_dir) 36 | } 37 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | pub const APKPURE_VERSIONS_URL_FORMAT: &str = "https://api.pureapk.com/m/v3/cms/app_version?hl=en-US&package_name="; 2 | pub const APKPURE_DOWNLOAD_URL_REGEX: &str = r"(X?APKJ)..(https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*))"; 3 | pub const FDROID_REPO: &str = "https://f-droid.org/repo"; 4 | pub const FDROID_INDEX_FINGERPRINT: &[u8] = &[67, 35, 141, 81, 44, 30, 94, 178, 214, 86, 159, 74, 58, 251, 245, 82, 52, 24, 184, 46, 10, 62, 209, 85, 39, 112, 171, 185, 169, 201, 204, 171]; 5 | pub const FDROID_SIGNATURE_BLOCK_FILE_REGEX: &str = r"^META-INF/.*\.(DSA|EC|RSA)$"; 6 | pub const HUAWEI_APP_GALLERY_CLIENT_API_URL: &str = "https://store-dre.hispace.dbankcloud.com/hwmarket/api/clientApi"; 7 | pub const PROGRESS_STYLE: &str ="[{elapsed_precise}] {bar:40.cyan/blue} {bytes}/{total_bytes} | {msg}"; 8 | -------------------------------------------------------------------------------- /src/download_sources/apkpure.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::{HashMap, HashSet}; 3 | use std::ops::Deref; 4 | use std::path::Path; 5 | use std::rc::Rc; 6 | 7 | use futures_util::StreamExt; 8 | use indicatif::MultiProgress; 9 | use regex::Regex; 10 | use reqwest::header::{HeaderMap, HeaderValue}; 11 | use reqwest::{Url, Response}; 12 | use serde_json::json; 13 | use tokio_dl_stream_to_disk::{AsyncDownload, error::ErrorKind as TDSTDErrorKind}; 14 | use tokio::time::{sleep, Duration as TokioDuration}; 15 | 16 | use crate::util::{OutputFormat, progress_bar::progress_wrapper}; 17 | 18 | fn http_headers() -> HeaderMap { 19 | let mut headers = HeaderMap::new(); 20 | headers.insert("x-cv", HeaderValue::from_static("3172501")); 21 | headers.insert("x-sv", HeaderValue::from_static("29")); 22 | headers.insert( 23 | "x-abis", 24 | HeaderValue::from_static("arm64-v8a,armeabi-v7a,armeabi"), 25 | ); 26 | headers.insert("x-gp", HeaderValue::from_static("1")); 27 | headers 28 | } 29 | 30 | pub async fn download_apps( 31 | apps: Vec<(String, Option)>, 32 | parallel: usize, 33 | sleep_duration: u64, 34 | outpath: &Path, 35 | ) { 36 | let mp = Rc::new(MultiProgress::new()); 37 | let http_client = Rc::new(reqwest::Client::new()); 38 | let headers = http_headers(); 39 | let re = Rc::new(Regex::new(crate::consts::APKPURE_DOWNLOAD_URL_REGEX).unwrap()); 40 | 41 | futures_util::stream::iter( 42 | apps.into_iter().map(|app| { 43 | let (app_id, app_version) = app; 44 | let http_client = Rc::clone(&http_client); 45 | let re = Rc::clone(&re); 46 | let headers = headers.clone(); 47 | let mp = Rc::clone(&mp); 48 | let mp_log = Rc::clone(&mp); 49 | async move { 50 | let app_string = match app_version { 51 | Some(ref version) => { 52 | mp_log.suspend(|| println!("Downloading {} version {}...", app_id, version)); 53 | format!("{}@{}", app_id, version) 54 | }, 55 | None => { 56 | mp_log.suspend(|| println!("Downloading {}...", app_id)); 57 | app_id.to_string() 58 | }, 59 | }; 60 | if sleep_duration > 0 { 61 | sleep(TokioDuration::from_millis(sleep_duration)).await; 62 | } 63 | let versions_url = Url::parse(&format!("{}{}", crate::consts::APKPURE_VERSIONS_URL_FORMAT, app_id)).unwrap(); 64 | let versions_response = http_client 65 | .get(versions_url) 66 | .headers(headers) 67 | .send().await.unwrap(); 68 | if let Some(app_version) = app_version { 69 | let regex_string = format!("[[:^digit:]]{}:(?s:.)+?{}", regex::escape(&app_version), crate::consts::APKPURE_DOWNLOAD_URL_REGEX); 70 | let re = Regex::new(®ex_string).unwrap(); 71 | download_from_response(versions_response, Box::new(Box::new(re)), app_string, outpath, mp).await; 72 | } else { 73 | download_from_response(versions_response, Box::new(re), app_string, outpath, mp).await; 74 | } 75 | } 76 | }) 77 | ).buffer_unordered(parallel).collect::>().await; 78 | } 79 | 80 | async fn download_from_response(response: Response, re: Box>, app_string: String, outpath: &Path, mp: Rc) { 81 | let mp_log = Rc::clone(&mp); 82 | let mp = Rc::clone(&mp); 83 | match response.status() { 84 | reqwest::StatusCode::OK => { 85 | let body = response.text().await.unwrap(); 86 | match re.captures(&body) { 87 | Some(caps) if caps.len() >= 2 => { 88 | let apk_xapk = caps.get(1).unwrap().as_str(); 89 | let download_url = caps.get(2).unwrap().as_str(); 90 | let fname = match apk_xapk { 91 | "XAPKJ" => format!("{}.xapk", app_string), 92 | _ => format!("{}.apk", app_string), 93 | }; 94 | 95 | match AsyncDownload::new(download_url, Path::new(outpath), &fname).get().await { 96 | Ok(mut dl) => { 97 | let length = dl.length(); 98 | let cb = match length { 99 | Some(length) => Some(progress_wrapper(mp)(fname.clone(), length)), 100 | None => None, 101 | }; 102 | 103 | match dl.download(&cb).await { 104 | Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_string)), 105 | Err(err) if matches!(err.kind(), TDSTDErrorKind::FileExists) => { 106 | mp_log.println(format!("File already exists for {}. Skipping...", app_string)).unwrap(); 107 | }, 108 | Err(err) if matches!(err.kind(), TDSTDErrorKind::PermissionDenied) => { 109 | mp_log.println(format!("Permission denied when attempting to write file for {}. Skipping...", app_string)).unwrap(); 110 | }, 111 | Err(_) => { 112 | mp_log.println(format!("An error has occurred attempting to download {}. Retry #1...", app_string)).unwrap(); 113 | match AsyncDownload::new(download_url, Path::new(outpath), &fname).download(&cb).await { 114 | Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_string)), 115 | Err(_) => { 116 | mp_log.println(format!("An error has occurred attempting to download {}. Retry #2...", app_string)).unwrap(); 117 | match AsyncDownload::new(download_url, Path::new(outpath), &fname).download(&cb).await { 118 | Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_string)), 119 | Err(_) => { 120 | mp_log.println(format!("An error has occurred attempting to download {}. Skipping...", app_string)).unwrap(); 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | }, 128 | Err(_) => { 129 | mp_log.println(format!("Invalid response for {}. Skipping...", app_string)).unwrap(); 130 | } 131 | } 132 | }, 133 | _ => { 134 | mp_log.println(format!("Could not get download URL for {}. Skipping...", app_string)).unwrap(); 135 | } 136 | } 137 | 138 | }, 139 | _ => { 140 | mp_log.println(format!("Invalid app response for {}. Skipping...", app_string)).unwrap(); 141 | } 142 | } 143 | } 144 | 145 | pub async fn list_versions(apps: Vec<(String, Option)>, options: HashMap<&str, &str>) { 146 | let http_client = Rc::new(reqwest::Client::new()); 147 | let re = Rc::new(Regex::new(r"([[:alnum:]\.-]+):\([[:xdigit:]]{40,}").unwrap()); 148 | let headers = http_headers(); 149 | let output_format = match options.get("output_format") { 150 | Some(val) if val.to_lowercase() == "json" => OutputFormat::Json, 151 | _ => OutputFormat::Plaintext, 152 | }; 153 | let json_root = Rc::new(RefCell::new(match output_format { 154 | OutputFormat::Json => Some(HashMap::new()), 155 | _ => None, 156 | })); 157 | 158 | for app in apps { 159 | let (app_id, _) = app; 160 | let http_client = Rc::clone(&http_client); 161 | let re = Rc::clone(&re); 162 | let json_root = Rc::clone(&json_root); 163 | let output_format = output_format.clone(); 164 | let headers = headers.clone(); 165 | async move { 166 | if output_format.is_plaintext() { 167 | println!("Versions available for {} on APKPure:", app_id); 168 | } 169 | let versions_url = Url::parse(&format!("{}{}", crate::consts::APKPURE_VERSIONS_URL_FORMAT, app_id)).unwrap(); 170 | let versions_response = http_client 171 | .get(versions_url) 172 | .headers(headers) 173 | .send().await.unwrap(); 174 | 175 | match versions_response.status() { 176 | reqwest::StatusCode::OK => { 177 | let body = versions_response.text().await.unwrap(); 178 | let mut versions = HashSet::new(); 179 | for caps in re.captures_iter(&body) { 180 | if caps.len() >= 2 { 181 | versions.insert(caps.get(1).unwrap().as_str().to_string()); 182 | } 183 | } 184 | let mut versions = versions.drain().collect::>(); 185 | versions.sort(); 186 | match output_format { 187 | OutputFormat::Plaintext => { 188 | println!("| {}", versions.join(", ")); 189 | }, 190 | OutputFormat::Json => { 191 | let mut app_root: HashMap>> = HashMap::new(); 192 | app_root.insert("available_versions".to_string(), versions.into_iter().map(|v| { 193 | let mut version_map = HashMap::new(); 194 | version_map.insert("version".to_string(), v); 195 | version_map 196 | }).collect()); 197 | json_root.borrow_mut().as_mut().unwrap().insert(app_id.to_string(), json!(app_root)); 198 | }, 199 | } 200 | } 201 | _ => { 202 | match output_format { 203 | OutputFormat::Plaintext => { 204 | eprintln!("| Invalid app response for {}. Skipping...", app_id); 205 | }, 206 | OutputFormat::Json => { 207 | let mut app_root = HashMap::new(); 208 | app_root.insert("error".to_string(), "Invalid app response.".to_string()); 209 | json_root.borrow_mut().as_mut().unwrap().insert(app_id.to_string(), json!(app_root)); 210 | }, 211 | } 212 | } 213 | } 214 | }.await; 215 | } 216 | if output_format.is_json() { 217 | println!("{{\"source\":\"APKPure\",\"apps\":{}}}", json!(*json_root)); 218 | }; 219 | } 220 | -------------------------------------------------------------------------------- /src/download_sources/fdroid.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{hash_map::DefaultHasher, HashSet, HashMap}; 2 | use std::error::Error; 3 | use std::fs::{self, File}; 4 | use std::hash::{Hash, Hasher}; 5 | use std::io; 6 | use std::io::prelude::*; 7 | use std::path::{Path, PathBuf}; 8 | use std::rc::Rc; 9 | 10 | use base64::{Engine as _, engine::general_purpose as b64_general_purpose}; 11 | use cryptographic_message_syntax::{SignedData, SignerInfo}; 12 | use futures_util::StreamExt; 13 | use indicatif::MultiProgress; 14 | use regex::Regex; 15 | use ring::digest::{Context, SHA256}; 16 | use serde_json::{json, Value}; 17 | use sha1::{Sha1, Digest as Sha1Digest}; 18 | use sha2::Sha256; 19 | use simple_error::SimpleError; 20 | use tempfile::{tempdir, TempDir}; 21 | use tokio::time::{sleep, Duration}; 22 | use tokio_dl_stream_to_disk::{AsyncDownload, error::ErrorKind as TDSTDErrorKind}; 23 | use x509_certificate::certificate::CapturedX509Certificate; 24 | 25 | use crate::consts; 26 | use crate::config::{self, ConfigDirError}; 27 | use crate::util::{OutputFormat, progress_bar::progress_wrapper}; 28 | mod error; 29 | use error::Error as FDroidError; 30 | 31 | async fn retrieve_index_or_exit(options: &HashMap<&str, &str>, mp: Rc, output_format: OutputFormat) -> Value { 32 | let temp_dir = match tempdir() { 33 | Ok(temp_dir) => temp_dir, 34 | Err(_) => { 35 | print_error("Could not create temporary directory for F-Droid package index. Exiting.", output_format); 36 | std::process::exit(1); 37 | } 38 | }; 39 | let mut custom_repo = false; 40 | let mut repo = consts::FDROID_REPO.to_string(); 41 | let mut fingerprint = Vec::from(consts::FDROID_INDEX_FINGERPRINT); 42 | let use_entry = match options.get("use_entry") { 43 | Some(val) if val == &"1" || val.to_lowercase() == "true" => true, 44 | _ => false, 45 | }; 46 | if let Some(full_repo_option) = options.get("repo") { 47 | custom_repo = true; 48 | if let Some((repo_option, fingerprint_option)) = full_repo_option.split_once("?fingerprint=") { 49 | fingerprint = match hex::decode(fingerprint_option) { 50 | Ok(hex_fingerprint) => hex_fingerprint, 51 | Err(_) => { 52 | print_error("Fingerprint must be specified as valid hex. Exiting.", output_format); 53 | std::process::exit(1); 54 | } 55 | }; 56 | repo = repo_option.to_string(); 57 | } else { 58 | repo = full_repo_option.to_string(); 59 | } 60 | } 61 | 62 | let display_error_and_exit = |err: ConfigDirError| { 63 | match err { 64 | ConfigDirError::NotFound => { 65 | print_error("Could not find a config directory for apkeep to store F-Droid package index. Exiting.", output_format.clone()); 66 | }, 67 | ConfigDirError::CouldNotCreate => { 68 | print_error("Could not create a config directory for apkeep to store F-Droid package index. Exiting.", output_format.clone()); 69 | }, 70 | } 71 | std::process::exit(1); 72 | }; 73 | let mut config_dir = config::config_dir().map_err(display_error_and_exit).unwrap(); 74 | if custom_repo { 75 | config_dir.push("fdroid-custom-repos"); 76 | config::create_dir(&config_dir).map_err(display_error_and_exit).unwrap(); 77 | let mut s = DefaultHasher::new(); 78 | repo.hash(&mut s); 79 | config_dir.push(format!("{}", s.finish())); 80 | config::create_dir(&config_dir).map_err(display_error_and_exit).unwrap(); 81 | } 82 | 83 | let mut latest_etag_file = PathBuf::from(&config_dir); 84 | if use_entry { 85 | latest_etag_file.push("latest_entry_etag"); 86 | } else { 87 | latest_etag_file.push("latest_etag"); 88 | } 89 | let latest_etag = match File::open(&latest_etag_file) { 90 | Ok(mut file) => { 91 | let mut contents = String::new(); 92 | if file.read_to_string(&mut contents).is_err() { 93 | print_error("Could not read etag file for F-Droid package index. Exiting.", output_format); 94 | std::process::exit(1); 95 | } 96 | Some(contents) 97 | }, 98 | Err(_) => None, 99 | }; 100 | 101 | let http_client = reqwest::Client::new(); 102 | let fdroid_jar_url = if use_entry { 103 | format!("{}/entry.jar", repo) 104 | } else { 105 | format!("{}/index-v1.jar", repo) 106 | }; 107 | let jar_response = http_client 108 | .head(fdroid_jar_url) 109 | .send().await.unwrap(); 110 | 111 | let etag = if jar_response.headers().contains_key("ETag") { 112 | jar_response.headers()["ETag"].to_str().unwrap() 113 | } else { 114 | print_error("Could not receive etag for F-Droid package index. Exiting.", output_format); 115 | std::process::exit(1); 116 | }; 117 | 118 | let mut index_file = PathBuf::from(&config_dir); 119 | if use_entry { 120 | index_file.push("index.json"); 121 | } else { 122 | index_file.push("index_v1.json"); 123 | } 124 | if latest_etag.is_some() && latest_etag.unwrap() == etag { 125 | let index = read_file_to_string(index_file); 126 | serde_json::from_str(&index).unwrap() 127 | } else { 128 | let files = download_and_extract_to_tempdir(&temp_dir, &repo, Rc::clone(&mp), use_entry, output_format.clone()).await; 129 | let verify_index = match options.get("verify-index") { 130 | Some(&"false") => false, 131 | _ => true, 132 | }; 133 | match verify_and_return_json(&temp_dir, &files, &fingerprint, verify_index, use_entry, Rc::clone(&mp)) { 134 | Ok(json) => { 135 | let index = if use_entry { 136 | match verify_and_return_index_from_entry(&temp_dir, &repo, &json, verify_index, mp, output_format.clone()).await { 137 | Ok(index_from_entry) => { 138 | index_from_entry 139 | } 140 | Err(_) => { 141 | print_error("Could verify and return package index from entry JSON. Exiting.", output_format); 142 | std::process::exit(1); 143 | } 144 | } 145 | } else { 146 | json 147 | }; 148 | 149 | match serde_json::from_str(&index) { 150 | Ok(index_value) => { 151 | if fs::write(index_file, index).is_err() { 152 | print_error("Could not write F-Droid package index to config file. Exiting.", output_format); 153 | std::process::exit(1); 154 | } 155 | if fs::write(latest_etag_file, etag).is_err() { 156 | print_error("Could not write F-Droid etag to config file. Exiting.", output_format); 157 | std::process::exit(1); 158 | } 159 | index_value 160 | } 161 | Err(_) => { 162 | print_error("Could not decode JSON for F-Droid package index. Exiting.", output_format); 163 | std::process::exit(1); 164 | } 165 | } 166 | }, 167 | Err(_) => { 168 | print_error("Could not verify F-Droid package index. Exiting.", output_format); 169 | std::process::exit(1); 170 | }, 171 | } 172 | } 173 | } 174 | 175 | fn print_error(err_msg: &str, output_format: OutputFormat) { 176 | match output_format { 177 | OutputFormat::Plaintext => eprintln!("{}", err_msg), 178 | OutputFormat::Json => println!("{{\"error\":\"{}\"}}", err_msg), 179 | } 180 | } 181 | 182 | fn read_file_to_string(file: PathBuf ) -> String { 183 | let mut file = File::open(&file).unwrap(); 184 | let mut contents = String::new(); 185 | file.read_to_string(&mut contents).unwrap(); 186 | contents 187 | } 188 | 189 | pub async fn download_apps( 190 | apps: Vec<(String, Option)>, 191 | parallel: usize, 192 | sleep_duration: u64, 193 | outpath: &Path, 194 | options: HashMap<&str, &str>, 195 | ) { 196 | let mp = Rc::new(MultiProgress::new()); 197 | let index = retrieve_index_or_exit(&options, Rc::clone(&mp), OutputFormat::Plaintext).await; 198 | 199 | let app_arch = options.get("arch").map(|x| x.to_string()); 200 | let (fdroid_apps, repo_address) = match parse_json_for_download_information(index, apps, app_arch.clone(), Rc::clone(&mp)) { 201 | Ok((fdroid_apps, repo_address)) => (fdroid_apps, repo_address), 202 | Err(_) => { 203 | println!("Could not parse JSON of F-Droid package index. Exiting."); 204 | std::process::exit(1); 205 | }, 206 | }; 207 | 208 | let repo_address = Rc::new(repo_address); 209 | futures_util::stream::iter( 210 | fdroid_apps.into_iter().map(|fdroid_app| { 211 | let (app_id, app_version, url_filename, hash) = fdroid_app; 212 | let repo_address = Rc::clone(&repo_address); 213 | let mp_log = Rc::clone(&mp); 214 | let mp = Rc::clone(&mp); 215 | let app_arch = app_arch.clone(); 216 | async move { 217 | let app_string = match (app_version, app_arch) { 218 | (None, None) => { 219 | mp_log.suspend(|| println!("Downloading {}...", app_id)); 220 | app_id.to_string() 221 | }, 222 | (None, Some(arch)) => { 223 | mp_log.suspend(|| println!("Downloading {} arch {}...", app_id, arch)); 224 | format!("{}@{}", app_id, arch) 225 | }, 226 | (Some(version), None) => { 227 | mp_log.suspend(|| println!("Downloading {} version {}...", app_id, version)); 228 | format!("{}@{}", app_id, version) 229 | }, 230 | (Some(version), Some(arch)) => { 231 | mp_log.suspend(|| println!("Downloading {} version {} arch {}...", app_id, version, arch)); 232 | format!("{}@{}@{}", app_id, version, arch) 233 | }, 234 | }; 235 | let fname = format!("{}.apk", app_string); 236 | if sleep_duration > 0 { 237 | sleep(Duration::from_millis(sleep_duration)).await; 238 | } 239 | let download_url = format!("{}/{}", repo_address, url_filename); 240 | match AsyncDownload::new(&download_url, Path::new(outpath), &fname).get().await { 241 | Ok(mut dl) => { 242 | let length = dl.length(); 243 | let cb = match length { 244 | Some(length) => Some(progress_wrapper(mp)(fname.clone(), length)), 245 | None => None, 246 | }; 247 | 248 | let sha256sum = match dl.download_and_return_sha256sum(&cb).await { 249 | Ok(sha256sum) => Some(sha256sum), 250 | Err(err) if matches!(err.kind(), TDSTDErrorKind::FileExists) => { 251 | mp_log.println(format!("File already exists for {}. Skipping...", app_string)).unwrap(); 252 | None 253 | }, 254 | Err(err) if matches!(err.kind(), TDSTDErrorKind::PermissionDenied) => { 255 | mp_log.println(format!("Permission denied when attempting to write file for {}. Skipping...", app_string)).unwrap(); 256 | None 257 | }, 258 | Err(_) => { 259 | mp_log.println(format!("An error has occurred attempting to download {}. Retry #1...", app_string)).unwrap(); 260 | match AsyncDownload::new(&download_url, Path::new(outpath), &fname).download_and_return_sha256sum(&cb).await { 261 | Ok(sha256sum) => Some(sha256sum), 262 | Err(_) => { 263 | mp_log.println(format!("An error has occurred attempting to download {}. Retry #2...", app_string)).unwrap(); 264 | match AsyncDownload::new(&download_url, Path::new(outpath), &fname).download_and_return_sha256sum(&cb).await { 265 | Ok(sha256sum) => Some(sha256sum), 266 | Err(_) => { 267 | mp_log.println(format!("An error has occurred attempting to download {}. Skipping...", app_string)).unwrap(); 268 | None 269 | } 270 | } 271 | } 272 | } 273 | } 274 | }; 275 | if let Some(sha256sum) = sha256sum { 276 | if sha256sum == hash { 277 | mp_log.suspend(|| println!("{} downloaded successfully!", app_string)); 278 | } else { 279 | mp_log.suspend(|| println!("{} downloaded, but the sha256sum does not match the one signed by F-Droid. Proceed with caution.", app_string)); 280 | } 281 | } 282 | }, 283 | Err(_) => { 284 | mp_log.println(format!("Invalid response for {}. Skipping...", app_string)).unwrap(); 285 | }, 286 | } 287 | } 288 | }) 289 | ).buffer_unordered(parallel).collect::>().await; 290 | } 291 | 292 | type DownloadInformation = (Vec<(String, Option, String, Vec)>, String); 293 | /// This currently works for `index-v1.json` as well as an index with version `20002`. It is 294 | /// flexible enough to parse either, and may work on future index versions as well. Since `sha256` 295 | /// digests are checked before proceeding, I don't foresee this having an insecure failure mode, so 296 | /// checking the index version and making the parsing overly brittle has no substantive advantage. 297 | fn parse_json_for_download_information(index: Value, apps: Vec<(String, Option)>, app_arch: Option, mp_log: Rc) -> Result { 298 | let index_map = index.as_object().ok_or(FDroidError::Dummy)?; 299 | let repo_address = index_map 300 | .get("repo").ok_or(FDroidError::Dummy)? 301 | .get("address").ok_or(FDroidError::Dummy)? 302 | .as_str().ok_or(FDroidError::Dummy)?; 303 | 304 | let packages = index_map 305 | .get("packages").ok_or(FDroidError::Dummy)? 306 | .as_object().ok_or(FDroidError::Dummy)?; 307 | 308 | let fdroid_apps: Vec<(String, Option, String, Vec)> = apps.into_iter().map(|app| { 309 | let (app_id, app_version) = app; 310 | match packages.get(&app_id) { 311 | Some(Value::Array(app_array)) => { 312 | for single_app in app_array { 313 | if let Value::Object(fdroid_app) = single_app { 314 | if let Some(Value::String(version_name)) = fdroid_app.get("versionName") { 315 | if app_version.is_none() || version_name == app_version.as_ref().unwrap() { 316 | if let (Some(Value::String(filename)), Some(Value::String(hash))) = (fdroid_app.get("apkName"), fdroid_app.get("hash")) { 317 | if let Ok(hash) = hex::decode(hash.to_string()) { 318 | if let Some(arch) = &app_arch { 319 | if let Some(Value::Array(nativecode_array)) = fdroid_app.get("nativecode") { 320 | if nativecode_array.iter().any(|value| { 321 | if let Value::String(value_str) = value{ 322 | value_str == arch 323 | } else { 324 | false 325 | } 326 | }) { 327 | return Some((app_id, app_version, filename.to_string(), hash)); 328 | } 329 | } 330 | } else { 331 | return Some((app_id, app_version, filename.to_string(), hash)); 332 | } 333 | } 334 | } 335 | } 336 | } 337 | } 338 | } 339 | let arch_str = app_arch.as_ref().map_or("".to_string(), |x| format!(" {}", x)); 340 | mp_log.println(format!("Could not find version {}{} of {}. Skipping...", app_version.unwrap(), arch_str, app_id)).unwrap(); 341 | return None; 342 | }, 343 | Some(Value::Object(app_object)) => { 344 | if let Some(Value::Object(versions)) = app_object.get("versions") { 345 | let mut latest_version = 0; 346 | let mut filename = String::new(); 347 | let mut hash = String::new(); 348 | for (_, version_value) in versions { 349 | if let Value::Object(version) = version_value { 350 | if let (Some(Value::Object(manifest)), Some(Value::Object(file))) = (version.get("manifest"), version.get("file")) { 351 | if let (Some(Value::String(name)), Some(Value::String(sha256))) = (file.get("name"), file.get("sha256")) { 352 | if app_version.is_some() { 353 | if let Some(Value::String(version_name)) = manifest.get("versionName") { 354 | if version_name == app_version.as_ref().unwrap() { 355 | if let Ok(sha256) = hex::decode(sha256.to_string()) { 356 | return Some((app_id, app_version, name.to_string(), sha256)); 357 | } 358 | } 359 | } 360 | } else { 361 | if let Some(Value::Number(version_code_number)) = manifest.get("versionCode") { 362 | if let Some(version_code) = version_code_number.as_u64() { 363 | if version_code > latest_version { 364 | latest_version = version_code; 365 | filename = name.to_string(); 366 | hash = sha256.to_string(); 367 | } 368 | } 369 | } 370 | } 371 | } 372 | } 373 | } 374 | } 375 | if app_version.is_none() { 376 | if let Ok(hash) = hex::decode(hash) { 377 | return Some((app_id, app_version, filename, hash)); 378 | } 379 | } 380 | } 381 | }, 382 | _ => mp_log.println(format!("Could not find {} in package list. Skipping...", app_id)).unwrap(), 383 | } 384 | None 385 | }).flatten().collect(); 386 | 387 | Ok((fdroid_apps, repo_address.to_string())) 388 | } 389 | 390 | pub async fn list_versions(apps: Vec<(String, Option)>, options: HashMap<&str, &str>) { 391 | let mp = Rc::new(MultiProgress::new()); 392 | let output_format = match options.get("output_format") { 393 | Some(val) if val.to_lowercase() == "json" => OutputFormat::Json, 394 | _ => OutputFormat::Plaintext, 395 | }; 396 | let index = retrieve_index_or_exit(&options, mp, output_format.clone()).await; 397 | 398 | if parse_json_display_versions(index, apps, output_format).is_err() { 399 | eprintln!("Could not parse JSON of F-Droid package index. Exiting."); 400 | std::process::exit(1); 401 | }; 402 | } 403 | 404 | /// The comments for `parse_json_for_download_information` apply here, too. 405 | fn parse_json_display_versions(index: Value, apps: Vec<(String, Option)>, output_format: OutputFormat) -> Result<(), FDroidError> { 406 | let index_map = index.as_object().ok_or(FDroidError::Dummy)?; 407 | 408 | let packages = index_map 409 | .get("packages").ok_or(FDroidError::Dummy)? 410 | .as_object().ok_or(FDroidError::Dummy)?; 411 | 412 | let mut json_root = match output_format { 413 | OutputFormat::Json => Some(HashMap::new()), 414 | _ => None, 415 | }; 416 | 417 | for app in apps { 418 | let (app_id, _) = app; 419 | if output_format.is_plaintext() { 420 | println!("Versions available for {} on F-Droid:", app_id); 421 | } 422 | let mut versions_set = HashSet::new(); 423 | match packages.get(&app_id) { 424 | Some(Value::Array(app_array)) => { 425 | for single_app in app_array { 426 | if let Value::Object(fdroid_app) = single_app { 427 | if let Some(Value::String(version_name)) = fdroid_app.get("versionName") { 428 | versions_set.insert(version_name.to_string()); 429 | } 430 | } 431 | } 432 | }, 433 | Some(Value::Object(app_object)) => { 434 | if let Some(Value::Object(versions)) = app_object.get("versions") { 435 | for (_, version_value) in versions { 436 | if let Value::Object(version) = version_value { 437 | if let Some(Value::Object(manifest)) = version.get("manifest") { 438 | if let Some(Value::String(version_name)) = manifest.get("versionName") { 439 | versions_set.insert(version_name.to_string()); 440 | } 441 | } 442 | } 443 | } 444 | } 445 | }, 446 | _ => { 447 | match output_format { 448 | OutputFormat::Plaintext => { 449 | eprintln!("| Could not find {} in package list. Skipping...", app_id); 450 | }, 451 | OutputFormat::Json => { 452 | let mut app_root = HashMap::new(); 453 | app_root.insert("error".to_string(), "Not found in package list.".to_string()); 454 | json_root.as_mut().unwrap().insert(app_id.to_string(), json!(app_root)); 455 | } 456 | } 457 | continue; 458 | } 459 | } 460 | let mut versions_set = versions_set.drain().collect::>(); 461 | versions_set.sort(); 462 | match output_format { 463 | OutputFormat::Plaintext => { 464 | println!("| {}", versions_set.join(", ")); 465 | }, 466 | OutputFormat::Json => { 467 | let mut app_root: HashMap>> = HashMap::new(); 468 | app_root.insert("available_versions".to_string(), versions_set.into_iter().map(|v| { 469 | let mut version_map = HashMap::new(); 470 | version_map.insert("version".to_string(), v); 471 | version_map 472 | }).collect()); 473 | json_root.as_mut().unwrap().insert(app_id.to_string(), json!(app_root)); 474 | } 475 | } 476 | } 477 | if output_format.is_json() { 478 | println!("{{\"source\":\"F-Droid\",\"apps\":{}}}", json!(json_root.unwrap())); 479 | }; 480 | Ok(()) 481 | } 482 | 483 | fn verify_and_return_json(dir: &TempDir, files: &[String], fingerprint: &[u8], verify_index: bool, use_entry: bool, mp: Rc) -> Result> { 484 | let re = Regex::new(consts::FDROID_SIGNATURE_BLOCK_FILE_REGEX).unwrap(); 485 | let cert_file = { 486 | let mut cert_files = vec![]; 487 | for file in files { 488 | if re.is_match(file) { 489 | cert_files.push(file.clone()); 490 | } 491 | } 492 | if cert_files.is_empty() { 493 | return Err(Box::new(SimpleError::new("Found no certificate file for F-Droid repository."))); 494 | } 495 | if cert_files.len() > 1 { 496 | return Err(Box::new(SimpleError::new("Found multiple certificate files for F-Droid repository."))); 497 | } 498 | dir.path().join(cert_files[0].clone()) 499 | }; 500 | let signed_file = { 501 | let mut signed_file = cert_file.clone(); 502 | signed_file.set_extension("SF"); 503 | signed_file 504 | }; 505 | 506 | let signed_content = fs::read(signed_file)?; 507 | 508 | if verify_index { 509 | mp.println("Verifying...").unwrap(); 510 | let signed_data = get_signed_data_from_cert_file(cert_file)?; 511 | let signer_info = signed_data.signers().next().unwrap(); 512 | signer_info.verify_signature_with_signed_data_and_content( 513 | &signed_data, 514 | &signed_content)?; 515 | let cert = signed_data.certificates().next().unwrap(); 516 | let mut context = Context::new(&SHA256); 517 | context.update(&cert.encode_ber()?); 518 | let cert_fingerprint = context.finish(); 519 | if cert_fingerprint.as_ref() != fingerprint { 520 | return Err(Box::new(SimpleError::new("Fingerprint of the key contained in the F-Droid repository does not match the expected fingerprint."))) 521 | }; 522 | } 523 | 524 | let signed_file_string = std::str::from_utf8(&signed_content)?; 525 | let manifest_file = dir.path().join("META-INF").join("MANIFEST.MF"); 526 | let manifest_file_data = fs::read(manifest_file)?; 527 | if verify_index { 528 | let (signed_file_regex, sha_algorithm_name) = if use_entry { 529 | (Regex::new(r"\r\nSHA-256-Digest-Manifest: (.*)\r\n").unwrap(), "sha256sum") 530 | } else { 531 | (Regex::new(r"\r\nSHA1-Digest-Manifest: (.*)\r\n").unwrap(), "sha1sum") 532 | }; 533 | let signed_file_manifest_shasum = b64_general_purpose::STANDARD.decode(match signed_file_regex.captures(signed_file_string) { 534 | Some(caps) if caps.len() >= 2 => caps.get(1).unwrap().as_str(), 535 | _ => { 536 | return Err(Box::new(SimpleError::new(format!("Could not retrieve the manifest {} from the signed file.", sha_algorithm_name)))); 537 | } 538 | })?; 539 | let actual_manifest_shasum = if use_entry { 540 | let mut hasher = Sha256::new(); 541 | hasher.update(manifest_file_data.clone()); 542 | Vec::from(hasher.finalize().as_slice()) 543 | } else { 544 | let mut hasher = Sha1::new(); 545 | hasher.update(manifest_file_data.clone()); 546 | Vec::from(hasher.finalize().as_slice()) 547 | }; 548 | if signed_file_manifest_shasum != actual_manifest_shasum[..] { 549 | return Err(Box::new(SimpleError::new(format!("The manifest {} from the signed file does not match the actual manifest {}.", sha_algorithm_name, sha_algorithm_name)))); 550 | } 551 | } 552 | 553 | let manifest_file_string = std::str::from_utf8(&manifest_file_data)?; 554 | let json_file = if use_entry { 555 | dir.path().join("entry.json") 556 | } else { 557 | dir.path().join("index-v1.json") 558 | }; 559 | let json_file_data = fs::read(json_file)?; 560 | if verify_index { 561 | let (manifest_file_regex, file_algo) = if use_entry { 562 | (Regex::new(r"\r\nName: entry\.json\r\nSHA-256-Digest: (.*)\r\n").unwrap(), "entry sha256sum") 563 | } else { 564 | (Regex::new(r"\r\nName: index-v1\.json\r\nSHA1-Digest: (.*)\r\n").unwrap(), "index sha1sum") 565 | }; 566 | let manifest_file_shasum = b64_general_purpose::STANDARD.decode(match manifest_file_regex.captures(manifest_file_string) { 567 | Some(caps) if caps.len() >= 2 => caps.get(1).unwrap().as_str(), 568 | _ => { 569 | return Err(Box::new(SimpleError::new(format!("Could not retrieve the {} from the manifest file.", file_algo)))); 570 | } 571 | })?; 572 | let actual_shasum = if use_entry { 573 | let mut hasher = Sha256::new(); 574 | hasher.update(json_file_data.clone()); 575 | Vec::from(hasher.finalize().as_slice()) 576 | } else { 577 | let mut hasher = Sha1::new(); 578 | hasher.update(json_file_data.clone()); 579 | Vec::from(hasher.finalize().as_slice()) 580 | }; 581 | if manifest_file_shasum != actual_shasum[..] { 582 | return Err(Box::new(SimpleError::new(format!("The {} from the manifest file does not match the actual {}.", file_algo, file_algo)))); 583 | } 584 | } 585 | 586 | Ok(String::from(std::str::from_utf8(&json_file_data)?)) 587 | } 588 | 589 | async fn verify_and_return_index_from_entry(dir: &TempDir, repo: &str, json: &str, verify_index: bool, mp: Rc, output_format: OutputFormat) -> Result> { 590 | let mp_log = Rc::clone(&mp); 591 | let (index_name, index_sha256) = match serde_json::from_str::(json) { 592 | Ok(entry) => { 593 | let entry_map = entry.as_object().ok_or(FDroidError::Dummy)?; 594 | let index_map = entry_map 595 | .get("index").ok_or(FDroidError::Dummy)?; 596 | (index_map.get("name").ok_or(FDroidError::Dummy)? 597 | .as_str().ok_or(FDroidError::Dummy)?.trim_start_matches("/").to_string(), 598 | index_map.get("sha256").ok_or(FDroidError::Dummy)? 599 | .as_str().ok_or(FDroidError::Dummy)?.to_string()) 600 | }, 601 | Err(_) => { 602 | print_error("Could not decode JSON for F-Droid entry file. Exiting.", output_format); 603 | std::process::exit(1); 604 | } 605 | }; 606 | let index_url = format!("{}/{}", repo, index_name); 607 | let mut dl = AsyncDownload::new(&index_url, dir.path(), &index_name).get().await.unwrap(); 608 | let length = dl.length(); 609 | let cb = match length { 610 | Some(length) => Some(progress_wrapper(mp)(index_name.to_string(), length)), 611 | None => None, 612 | }; 613 | match dl.download(&cb).await { 614 | Ok(_) => { 615 | mp_log.println(format!("Package index downloaded successfully!")).unwrap(); 616 | let index_file = dir.path().join(index_name); 617 | let index_file_data = fs::read(index_file)?; 618 | 619 | if verify_index { 620 | mp_log.println("Verifying...").unwrap(); 621 | let actual_index_shasum = { 622 | let mut hasher = Sha256::new(); 623 | hasher.update(index_file_data.clone()); 624 | Vec::from(hasher.finalize().as_slice()) 625 | }; 626 | let index_sha256 = match hex::decode(index_sha256) { 627 | Ok(index_sha256) => index_sha256, 628 | Err(_) => { 629 | print_error("Index sha256sum did not specify valid hex. Exiting.", output_format); 630 | std::process::exit(1); 631 | } 632 | }; 633 | if index_sha256 != actual_index_shasum { 634 | return Err(Box::new(SimpleError::new("The index sha256sum from the entry file does not match the actual index sha256sum."))); 635 | } 636 | } 637 | 638 | Ok(String::from(std::str::from_utf8(&index_file_data)?)) 639 | } 640 | Err(_) => { 641 | print_error("Could not download F-Droid package index. Exiting.", output_format); 642 | std::process::exit(1); 643 | } 644 | } 645 | } 646 | 647 | fn get_signed_data_from_cert_file(signature_block_file: PathBuf) -> Result> { 648 | let bytes = fs::read(signature_block_file).unwrap(); 649 | match SignedData::parse_ber(&bytes) { 650 | Ok(signed_data) => { 651 | let certificates: Vec<&CapturedX509Certificate> = signed_data.certificates().collect(); 652 | if certificates.len() > 1 { 653 | return Err(Box::new(SimpleError::new("Too many certificates provided."))); 654 | } 655 | if certificates.is_empty() { 656 | return Err(Box::new(SimpleError::new("No certificate provided."))); 657 | } 658 | let signatories: Vec<&SignerInfo> = signed_data.signers().collect(); 659 | if signatories.len() > 1 { 660 | return Err(Box::new(SimpleError::new("Too many signatories provided."))); 661 | } 662 | if signatories.is_empty() { 663 | return Err(Box::new(SimpleError::new("No signatories provided."))); 664 | } 665 | Ok(signed_data) 666 | }, 667 | Err(err) => { 668 | Err(Box::new(err)) 669 | } 670 | } 671 | } 672 | 673 | async fn download_and_extract_to_tempdir(dir: &TempDir, repo: &str, mp: Rc, use_entry: bool, output_format: OutputFormat) -> Vec { 674 | let mp_log = Rc::clone(&mp); 675 | mp_log.suspend(|| println!("Downloading F-Droid package repository...")); 676 | let mut files = vec![]; 677 | let fdroid_jar_url = if use_entry { 678 | format!("{}/entry.jar", repo) 679 | } else { 680 | format!("{}/index-v1.jar", repo) 681 | }; 682 | let jar_local_file = "jar.zip"; 683 | let mut dl = AsyncDownload::new(&fdroid_jar_url, dir.path(), jar_local_file).get().await.unwrap(); 684 | let length = dl.length(); 685 | let cb = match length { 686 | Some(length) => Some(progress_wrapper(mp)(jar_local_file.to_string(), length)), 687 | None => None, 688 | }; 689 | match dl.download(&cb).await { 690 | Ok(_) => { 691 | mp_log.suspend(|| println!("Package repository downloaded successfully!\nExtracting...")); 692 | let file = fs::File::open(dir.path().join(jar_local_file)).unwrap(); 693 | match zip::ZipArchive::new(file) { 694 | Ok(mut archive) => { 695 | for i in 0..archive.len() { 696 | let mut file = archive.by_index(i).unwrap(); 697 | let outpath = match file.enclosed_name() { 698 | Some(path) => dir.path().join(path.to_owned()), 699 | None => continue, 700 | }; 701 | if (&*file.name()).ends_with('/') { 702 | fs::create_dir_all(&outpath).unwrap(); 703 | } else { 704 | if let Some(p) = outpath.parent() { 705 | if !p.exists() { 706 | fs::create_dir_all(&p).unwrap(); 707 | } 708 | } 709 | files.push(file.enclosed_name().unwrap().to_owned().into_os_string().into_string().unwrap()); 710 | let mut outfile = fs::File::create(&outpath).unwrap(); 711 | io::copy(&mut file, &mut outfile).unwrap(); 712 | } 713 | 714 | // Get and Set permissions 715 | #[cfg(unix)] 716 | { 717 | use std::os::unix::fs::PermissionsExt; 718 | 719 | if let Some(mode) = file.unix_mode() { 720 | fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)).unwrap(); 721 | } 722 | } 723 | } 724 | }, 725 | Err(_) => { 726 | print_error("F-Droid package repository could not be extracted. Please try again.", output_format); 727 | std::process::exit(1); 728 | } 729 | } 730 | } 731 | Err(_) => { 732 | print_error("Could not download F-Droid package repository.", output_format); 733 | std::process::exit(1); 734 | } 735 | } 736 | files 737 | } 738 | -------------------------------------------------------------------------------- /src/download_sources/fdroid/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error as StdError; 2 | use std::fmt; 3 | 4 | #[derive(Debug)] 5 | pub enum Error { 6 | Dummy, 7 | Other(Box), 8 | } 9 | 10 | impl From> for Error { 11 | fn from(err: Box) -> Error { 12 | Self::Other(err) 13 | } 14 | } 15 | 16 | impl StdError for Error {} 17 | 18 | impl fmt::Display for Error { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 20 | match self { 21 | Self::Dummy => write!(f, "Dummy"), 22 | Self::Other(err) => err.fmt(f), 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/download_sources/google_play.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::path::Path; 3 | use std::rc::Rc; 4 | 5 | use futures_util::StreamExt; 6 | use gpapi::error::ErrorKind as GpapiErrorKind; 7 | use gpapi::Gpapi; 8 | use indicatif::MultiProgress; 9 | use tokio::time::{sleep, Duration as TokioDuration}; 10 | 11 | use crate::util::progress_bar::progress_wrapper; 12 | 13 | pub async fn download_apps( 14 | apps: Vec<(String, Option)>, 15 | parallel: usize, 16 | sleep_duration: u64, 17 | email: &str, 18 | aas_token: &str, 19 | outpath: &Path, 20 | accept_tos: bool, 21 | mut options: HashMap<&str, &str>, 22 | ) { 23 | let device = options.remove("device").unwrap_or("px_7a"); 24 | let split_apk = match options.remove("split_apk") { 25 | Some(val) if val == "1" || val.to_lowercase() == "true" => true, 26 | _ => false, 27 | }; 28 | let include_additional_files = match options.remove("include_additional_files") { 29 | Some(val) if val == "1" || val.to_lowercase() == "true" => true, 30 | _ => false, 31 | }; 32 | let mut gpa = Gpapi::new(device, email); 33 | 34 | if let Some(locale) = options.remove("locale") { 35 | gpa.set_locale(locale); 36 | } 37 | if let Some(timezone) = options.remove("timezone") { 38 | gpa.set_timezone(timezone); 39 | } 40 | 41 | gpa.set_aas_token(aas_token); 42 | if let Err(err) = gpa.login().await { 43 | match err.kind() { 44 | GpapiErrorKind::TermsOfService => { 45 | if accept_tos { 46 | match gpa.accept_tos().await { 47 | Ok(_) => { 48 | if let Err(_) = gpa.login().await { 49 | eprintln!("Could not log in, even after accepting the Google Play Terms of Service"); 50 | std::process::exit(1); 51 | } 52 | println!("Google Play Terms of Service accepted."); 53 | }, 54 | _ => { 55 | eprintln!("Could not accept Google Play Terms of Service"); 56 | std::process::exit(1); 57 | }, 58 | } 59 | } else { 60 | println!("{}\nPlease read the ToS here: https://play.google.com/about/play-terms/index.html\nIf you accept, please pass the --accept-tos flag.", err); 61 | std::process::exit(1); 62 | } 63 | }, 64 | _ => { 65 | eprintln!("Could not log in to Google Play. Please check your credentials and try again later. {}", err); 66 | std::process::exit(1); 67 | } 68 | } 69 | } 70 | 71 | let mp = Rc::new(MultiProgress::new()); 72 | let gpa = Rc::new(gpa); 73 | futures_util::stream::iter( 74 | apps.into_iter().map(|app| { 75 | let (app_id, app_version) = app; 76 | let gpa = Rc::clone(&gpa); 77 | let mp_dl1 = Rc::clone(&mp); 78 | let mp_dl2 = Rc::clone(&mp); 79 | let mp_dl3 = Rc::clone(&mp); 80 | let mp_log = Rc::clone(&mp); 81 | 82 | async move { 83 | if app_version.is_none() { 84 | mp_log.suspend(|| println!("Downloading {}...", app_id)); 85 | if sleep_duration > 0 { 86 | sleep(TokioDuration::from_millis(sleep_duration)).await; 87 | } 88 | match gpa.download(&app_id, None, split_apk, include_additional_files, Path::new(outpath), Some(&progress_wrapper(mp_dl1))).await { 89 | Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_id)), 90 | Err(err) if matches!(err.kind(), GpapiErrorKind::FileExists) => { 91 | mp_log.println(format!("File already exists for {}. Skipping...", app_id)).unwrap(); 92 | } 93 | Err(err) if matches!(err.kind(), GpapiErrorKind::DirectoryExists) => { 94 | mp_log.println(format!("Split APK directory already exists for {}. Skipping...", app_id)).unwrap(); 95 | } 96 | Err(err) if matches!(err.kind(), GpapiErrorKind::InvalidApp) => { 97 | mp_log.println(format!("Invalid app response for {}. Skipping...", app_id)).unwrap(); 98 | } 99 | Err(err) if matches!(err.kind(), GpapiErrorKind::PermissionDenied) => { 100 | mp_log.println(format!("Permission denied when attempting to write file for {}. Skipping...", app_id)).unwrap(); 101 | } 102 | Err(_) => { 103 | mp_log.println(format!("An error has occurred attempting to download {}. Retry #1...", app_id)).unwrap(); 104 | match gpa.download(&app_id, None, split_apk, include_additional_files, Path::new(outpath), Some(&progress_wrapper(mp_dl2))).await { 105 | Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_id)), 106 | Err(_) => { 107 | mp_log.println(format!("An error has occurred attempting to download {}. Retry #2...", app_id)).unwrap(); 108 | match gpa.download(&app_id, None, split_apk, include_additional_files, Path::new(outpath), Some(&progress_wrapper(mp_dl3))).await { 109 | Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_id)), 110 | Err(_) => { 111 | mp_log.println(format!("An error has occurred attempting to download {}. Skipping...", app_id)).unwrap(); 112 | } 113 | } 114 | } 115 | } 116 | } 117 | } 118 | } else { 119 | mp_log.println(format!("Specific versions can not be downloaded from Google Play ({}@{}). Skipping...", app_id, app_version.unwrap())).unwrap(); 120 | } 121 | } 122 | }) 123 | ).buffer_unordered(parallel).collect::>().await; 124 | } 125 | 126 | pub async fn request_aas_token( 127 | email: &str, 128 | oauth_token: &str, 129 | mut options: HashMap<&str, &str>, 130 | ) { 131 | let device = options.remove("device").unwrap_or("px_7a"); 132 | let mut api = Gpapi::new(device, email); 133 | match api.request_aas_token(oauth_token).await { 134 | Ok(()) => { 135 | let aas_token = api.get_aas_token().unwrap(); 136 | println!("AAS Token: {}", aas_token); 137 | }, 138 | Err(_) => { 139 | println!("Error: was not able to retrieve AAS token with the provided OAuth token. Please provide new OAuth token and try again."); 140 | } 141 | } 142 | } 143 | 144 | pub fn list_versions(apps: Vec<(String, Option)>) { 145 | for app in apps { 146 | let (app_id, _) = app; 147 | println!("Versions available for {} on Google Play:", app_id); 148 | println!("| Google Play does not make old versions of apps available."); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/download_sources/huawei_app_gallery.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::rc::Rc; 3 | 4 | use futures_util::StreamExt; 5 | use indicatif::MultiProgress; 6 | use reqwest::header::{HeaderMap, HeaderValue}; 7 | use reqwest::{Url, Response}; 8 | use serde_json::Value; 9 | use tokio_dl_stream_to_disk::{AsyncDownload, error::ErrorKind as TDSTDErrorKind}; 10 | use tokio::time::{sleep, Duration as TokioDuration}; 11 | 12 | use crate::util::progress_bar::progress_wrapper; 13 | 14 | fn http_headers() -> HeaderMap { 15 | let mut headers = HeaderMap::new(); 16 | headers.insert("User-Agent", HeaderValue::from_static("UpdateSDK##4.0.1.300##Android##Pixel 2##com.huawei.appmarket##12.0.1.301")); 17 | headers 18 | } 19 | 20 | fn client_api_body(app_id: &str) -> String { 21 | format!("agVersion=12.0.1&brand=Android&buildNumber=QQ2A.200405.005.2020.04.07.17&density=420&deviceSpecParams=%7B%22abis%22%3A%22arm64-v8a%2Carmeabi-v7a%2Carmeabi%22%2C%22deviceFeatures%22%3A%22U%2CP%2CB%2C0c%2Ce%2C0J%2Cp%2Ca%2Cb%2C04%2Cm%2Candroid.hardware.wifi.rtt%2Ccom.google.hardware.camera.easel%2Ccom.google.android.feature.PIXEL_2017_EXPERIENCE%2C08%2C03%2CC%2CS%2C0G%2Cq%2CL%2C2%2C6%2CY%2CZ%2C0M%2Candroid.hardware.vr.high_performance%2Cf%2C1%2C07%2C8%2C9%2Candroid.hardware.sensor.hifi_sensors%2CO%2CH%2Ccom.google.android.feature.TURBO_PRELOAD%2Candroid.hardware.vr.headtracking%2CW%2Cx%2CG%2Co%2C06%2C0N%2Ccom.google.android.feature.PIXEL_EXPERIENCE%2C3%2CR%2Cd%2CQ%2Cn%2Candroid.hardware.telephony.carrierlock%2Cy%2CT%2Ci%2Cr%2Cu%2Ccom.google.android.feature.WELLBEING%2Cl%2C4%2C0Q%2CN%2CM%2C01%2C09%2CV%2C7%2C5%2C0H%2Cg%2Cs%2Cc%2C0l%2Ct%2C0L%2C0W%2C0X%2Ck%2C00%2Ccom.google.android.feature.GOOGLE_EXPERIENCE%2Candroid.hardware.sensor.assist%2Candroid.hardware.audio.pro%2CK%2CE%2C02%2CI%2CJ%2Cj%2CD%2Ch%2Candroid.hardware.wifi.aware%2C05%2CX%2Cv%22%2C%22dpi%22%3A420%2C%22preferLan%22%3A%22en%22%7D&emuiApiLevel=0&firmwareVersion=10&getSafeGame=1&gmsSupport=0&hardwareType=0&harmonyApiLevel=0&harmonyDeviceType=&installCheck=0&isFullUpgrade=0&isUpdateSdk=1&locale=en_US&magicApiLevel=0&magicVer=&manufacturer=Google&mapleVer=0&method=client.updateCheck&odm=0&packageName=com.huawei.appmarket&phoneType=Pixel%202&pkgInfo=%7B%22params%22%3A%5B%7B%22isPre%22%3A0%2C%22maple%22%3A0%2C%22oldVersion%22%3A%221.0%22%2C%22package%22%3A%22{}%22%2C%22pkgMode%22%3A0%2C%22shellApkVer%22%3A0%2C%22targetSdkVersion%22%3A19%2C%22versionCode%22%3A1%7D%5D%7D&resolution=1080_1794&sdkVersion=4.0.1.300&serviceCountry=IE&serviceType=0&supportMaple=0&ts=1649970862661&ver=1.2&version=12.0.1.301&versionCode=120001301", app_id) 22 | } 23 | 24 | pub async fn download_apps( 25 | apps: Vec<(String, Option)>, 26 | parallel: usize, 27 | sleep_duration: u64, 28 | outpath: &Path, 29 | ) { 30 | let http_client = Rc::new(reqwest::Client::new()); 31 | let headers = http_headers(); 32 | 33 | let mp = Rc::new(MultiProgress::new()); 34 | futures_util::stream::iter( 35 | apps.into_iter().map(|app| { 36 | let (app_id, app_version) = app; 37 | let http_client = Rc::clone(&http_client); 38 | let headers = headers.clone(); 39 | let mp = Rc::clone(&mp); 40 | let mp_log = Rc::clone(&mp); 41 | async move { 42 | if app_version.is_none() { 43 | mp_log.suspend(|| println!("Downloading {}...", app_id)); 44 | if sleep_duration > 0 { 45 | sleep(TokioDuration::from_millis(sleep_duration)).await; 46 | } 47 | let client_api_url = Url::parse(crate::consts::HUAWEI_APP_GALLERY_CLIENT_API_URL).unwrap(); 48 | let client_api_response = http_client 49 | .post(client_api_url) 50 | .body(client_api_body(&app_id)) 51 | .headers(headers) 52 | .send().await.unwrap(); 53 | download_from_response(client_api_response, app_id.to_string(), outpath, mp).await; 54 | } else { 55 | mp_log.println(format!("Specific versions can not be downloaded from Huawei AppGallery ({}@{}). Skipping...", app_id, app_version.unwrap())).unwrap(); 56 | } 57 | } 58 | }) 59 | ).buffer_unordered(parallel).collect::>().await; 60 | } 61 | 62 | async fn download_from_response(response: Response, app_string: String, outpath: &Path, mp: Rc) { 63 | let mp_log = Rc::clone(&mp); 64 | let mp = Rc::clone(&mp); 65 | let fname = format!("{}.apk", app_string); 66 | match response.status() { 67 | reqwest::StatusCode::OK => { 68 | let body = response.text().await.unwrap(); 69 | let response_value: Value = serde_json::from_str(&body).unwrap(); 70 | if response_value.is_object() { 71 | let response_obj = response_value.as_object().unwrap(); 72 | if response_obj.contains_key("list") { 73 | let list_value = response_obj.get("list").unwrap(); 74 | if list_value.is_array() { 75 | let list = list_value.as_array().unwrap(); 76 | if !list.is_empty() && list[0].is_object(){ 77 | let first_list_entry = list[0].as_object().unwrap(); 78 | if first_list_entry.contains_key("downurl") { 79 | let downurl = first_list_entry.get("downurl").unwrap(); 80 | if downurl.is_string() { 81 | let download_url = downurl.as_str().unwrap(); 82 | match AsyncDownload::new(download_url, Path::new(outpath), &fname).get().await { 83 | Ok(mut dl) => { 84 | let length = dl.length(); 85 | let cb = match length { 86 | Some(length) => Some(progress_wrapper(mp)(fname.clone(), length)), 87 | None => None, 88 | }; 89 | 90 | match dl.download(&cb).await { 91 | Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_string)), 92 | Err(err) if matches!(err.kind(), TDSTDErrorKind::FileExists) => { 93 | mp_log.println(format!("File already exists for {}. Skipping...", app_string)).unwrap(); 94 | }, 95 | Err(err) if matches!(err.kind(), TDSTDErrorKind::PermissionDenied) => { 96 | mp_log.println(format!("Permission denied when attempting to write file for {}. Skipping...", app_string)).unwrap(); 97 | }, 98 | Err(_) => { 99 | mp_log.println(format!("An error has occurred attempting to download {}. Retry #1...", app_string)).unwrap(); 100 | match AsyncDownload::new(download_url, Path::new(outpath), &fname).download(&cb).await { 101 | Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_string)), 102 | Err(_) => { 103 | mp_log.println(format!("An error has occurred attempting to download {}. Retry #2...", app_string)).unwrap(); 104 | match AsyncDownload::new(download_url, Path::new(outpath), &fname).download(&cb).await { 105 | Ok(_) => mp_log.suspend(|| println!("{} downloaded successfully!", app_string)), 106 | Err(_) => { 107 | mp_log.println(format!("An error has occurred attempting to download {}. Skipping...", app_string)).unwrap(); 108 | } 109 | } 110 | } 111 | } 112 | } 113 | } 114 | }, 115 | Err(_) => { 116 | mp_log.println(format!("Invalid response for {}. Skipping...", app_string)).unwrap(); 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } 123 | } 124 | } 125 | }, 126 | _ => { 127 | mp_log.println(format!("Invalid app response for {}. Skipping...", app_string)).unwrap(); 128 | } 129 | } 130 | } 131 | 132 | pub async fn list_versions(apps: Vec<(String, Option)>) { 133 | for app in apps { 134 | let (app_id, _) = app; 135 | println!("Versions available for {} on Huawei AppGallery:", app_id); 136 | println!("| Huawei AppGallery does not make old versions of apps available."); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/download_sources/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod google_play; 2 | pub mod fdroid; 3 | pub mod apkpure; 4 | pub mod huawei_app_gallery; 5 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! # Installation 2 | //! 3 | //! Precompiled binaries for `apkeep` on various platforms can be downloaded 4 | //! [here](https://github.com/EFForg/apkeep/releases). 5 | //! 6 | //! To install from `crates.io`, simply [install rust](https://www.rust-lang.org/tools/install) and 7 | //! run 8 | //! 9 | //! ```shell 10 | //! cargo install apkeep 11 | //! ``` 12 | //! 13 | //! Or to install from the latest commit in our repository, run 14 | //! 15 | //! ```shell 16 | //! cargo install --git https://github.com/EFForg/apkeep.git 17 | //! ``` 18 | //! 19 | //! If using on an Android platform, [`termux`](https://termux.org/) must be installed first. 20 | //! Upgrade to the latest packages with `pkg update`, then install the `apkeep` precompiled binary 21 | //! as described above or run `pkg install apkeep` to install from the `termux` repository. 22 | //! 23 | //! Docker images are also available through the GitHub Container Registry. Aside from using a 24 | //! specific release version, the following floating tags are available: 25 | //! 26 | //! - stable: tracks the latest stable release (recommended) 27 | //! - latest: tracks the latest release, including pre-releases 28 | //! - edge: tracks the latest commit 29 | //! 30 | //! # Usage 31 | //! 32 | //! See [`USAGE`](https://github.com/EFForg/apkeep/blob/master/USAGE). 33 | //! 34 | //! # Examples 35 | //! 36 | //! The simplest example is to download a single APK to the current directory: 37 | //! 38 | //! ```shell 39 | //! apkeep -a com.instagram.android . 40 | //! ``` 41 | //! 42 | //! This downloads from the default source, APKPure, which does not require credentials. To 43 | //! download directly from the google play store, you will first have to [obtain an AAS token](USAGE-google-play.md). 44 | //! Then, 45 | //! 46 | //! ```shell 47 | //! apkeep -a com.instagram.android -d google-play -e 'someone@gmail.com' -t aas_token . 48 | //! ``` 49 | //! 50 | //! For more google play usage examples, such as specifying a device configuration, timezone or 51 | //! locale, refer to the [`USAGE-google-play.md`](USAGE-google-play.md) document. 52 | //! 53 | //! To download from the F-Droid open source repository: 54 | //! 55 | //! ```shell 56 | //! apkeep -a org.mozilla.fennec_fdroid -d f-droid . 57 | //! ``` 58 | //! 59 | //! For more F-Droid usage examples, such as downloading from F-Droid mirrors or other F-Droid 60 | //! repositories, refer to the [`USAGE-fdroid.md`](USAGE-fdroid.md) document. 61 | //! 62 | //! Or, to download from the Huawei AppGallery: 63 | //! 64 | //! ```shell 65 | //! apkeep -a com.elysiumlabs.newsbytes -d huawei-app-gallery . 66 | //! ``` 67 | //! 68 | //! To download a specific version of an APK (possible for APKPure or F-Droid), use the `@version` 69 | //! convention: 70 | //! 71 | //! ```shell 72 | //! apkeep -a com.instagram.android@1.2.3 . 73 | //! ``` 74 | //! 75 | //! Or, to list what versions are available, use `-l`: 76 | //! 77 | //! ```shell 78 | //! apkeep -l -a org.mozilla.fennec_fdroid -d f-droid 79 | //! ``` 80 | //! 81 | //! Refer to [`USAGE`](https://github.com/EFForg/apkeep/blob/master/USAGE) to download multiple 82 | //! APKs in a single run. 83 | //! 84 | //! All the above examples can also be used in Docker with minimal changes. For example, to 85 | //! download a single APK to your chosen output directory: 86 | //! 87 | //! ```shell 88 | //! docker run --rm -v output_path:/output ghcr.io/efforg/apkeep:stable -a com.instagram.android 89 | //! /output 90 | //! ``` 91 | //! 92 | //! # Specify a CSV file or individual app ID 93 | //! 94 | //! You can either specify a CSV file which lists the apps to download, or an individual app ID. 95 | //! If you specify a CSV file and the app ID is not specified by the first column, you'll have to 96 | //! use the --field option as well. If you have a simple file with one app ID per line, you can 97 | //! just treat it as a CSV with a single field. 98 | //! 99 | //! # Download Sources 100 | //! 101 | //! You can use this tool to download from a few distinct sources. 102 | //! 103 | //! * The Google Play Store (`-d google-play`), given an email address and AAS token 104 | //! * APKPure (`-d apk-pure`), a third-party site hosting APKs available on the Play Store 105 | //! * F-Droid (`-d f-droid`), a repository for free and open-source Android apps. `apkeep` 106 | //! verifies that these APKs are signed by the F-Droid maintainers, and alerts the user if an APK 107 | //! was downloaded but could not be verified 108 | //! * The Huawei AppGallery (`-d huawei-app-gallery`), an app store popular in China 109 | //! 110 | //! # Usage Note 111 | //! 112 | //! Users should not use app lists or choose so many parallel APK fetches as to place unreasonable 113 | //! or disproportionately large load on the infrastructure of the app distributor. 114 | //! 115 | //! When using with the Google Play Store as the download source, a few considerations should be 116 | //! made: 117 | //! 118 | //! * Google may terminate your Google account based on Terms of Service violations. Read their 119 | //! [Terms of Service](https://play.google.com/about/play-terms/index.html), avoid violating it, 120 | //! and choose an account where this outcome is acceptable. 121 | //! * Paid and DRM apps will not be available. 122 | //! * Using Tor will make it a lot more likely that the download will fail. 123 | 124 | use std::collections::HashMap; 125 | use std::error::Error; 126 | use std::fs::{self, File}; 127 | use std::io::{self, Write, Read}; 128 | use std::path::{Path, PathBuf}; 129 | 130 | use configparser::ini::Ini; 131 | 132 | mod cli; 133 | use cli::DownloadSource; 134 | 135 | mod config; 136 | mod consts; 137 | mod util; 138 | 139 | mod download_sources; 140 | use download_sources::google_play; 141 | use download_sources::fdroid; 142 | use download_sources::apkpure; 143 | use download_sources::huawei_app_gallery; 144 | 145 | type CSVList = Vec<(String, Option)>; 146 | fn fetch_csv_list(csv: &str, field: usize, version_field: Option) -> Result> { 147 | Ok(parse_csv_text(fs::read_to_string(csv)?, field, version_field)) 148 | } 149 | 150 | fn parse_csv_text(text: String, field: usize, version_field: Option) -> Vec<(String, Option)> { 151 | let field = field - 1; 152 | let version_field = version_field.map(|version_field| version_field - 1); 153 | text.split('\n') 154 | .filter_map(|l| { 155 | let entry = l.trim(); 156 | let mut entry_vec = entry.split(',').collect::>(); 157 | if entry_vec.len() > field && !(entry_vec.len() == 1 && entry_vec[0].is_empty()) { 158 | match version_field { 159 | Some(mut version_field) if entry_vec.len() > version_field => { 160 | if version_field > field { 161 | version_field -= 1; 162 | } 163 | let app_id = String::from(entry_vec.remove(field)); 164 | let app_version = String::from(entry_vec.remove(version_field)); 165 | if !app_version.is_empty() { 166 | Some((app_id, Some(app_version))) 167 | } else { 168 | Some((app_id, None)) 169 | } 170 | }, 171 | _ => Some((String::from(entry_vec.remove(field)), None)), 172 | } 173 | } else { 174 | None 175 | } 176 | }) 177 | .collect() 178 | } 179 | 180 | fn load_config(ini_file: Option) -> Result> { 181 | let mut conf = Ini::new(); 182 | let config_path = match ini_file { 183 | Some(ini_file) => ini_file, 184 | None => { 185 | let mut config_path = config::config_dir()?; 186 | config_path.push("apkeep.ini"); 187 | config_path 188 | } 189 | }; 190 | let mut config_fp = File::open(&config_path)?; 191 | let mut contents = String::new(); 192 | config_fp.read_to_string(&mut contents)?; 193 | conf.read(contents)?; 194 | Ok(conf) 195 | } 196 | 197 | #[tokio::main] 198 | async fn main() { 199 | let usage = { 200 | cli::app().render_usage() 201 | }; 202 | let matches = cli::app().get_matches(); 203 | 204 | let mut download_source = *matches.get_one::("download_source").unwrap(); 205 | let options: HashMap<&str, &str> = match matches.get_one::("options") { 206 | Some(options) => { 207 | let mut options_map = HashMap::new(); 208 | for option in options.split(",") { 209 | match option.split_once("=") { 210 | Some((key, value)) => { 211 | options_map.insert(key, value); 212 | }, 213 | None => {} 214 | } 215 | } 216 | options_map 217 | }, 218 | None => HashMap::new() 219 | }; 220 | 221 | let oauth_token = matches.get_one::("google_oauth_token").map(|v| v.to_string()); 222 | if oauth_token.is_some() { 223 | download_source = DownloadSource::GooglePlay; 224 | } 225 | let list: Vec<(String, Option)> = if oauth_token.is_none() { 226 | match matches.get_one::("app") { 227 | Some(app) => { 228 | let mut app_vec: Vec = app.splitn(2, '@').map(String::from).collect(); 229 | let app_id = app_vec.remove(0); 230 | let app_version = match app_vec.len() { 231 | 1 => Some(app_vec.remove(0)), 232 | _ => None, 233 | }; 234 | vec![(app_id, app_version)] 235 | }, 236 | None => { 237 | let csv = matches.get_one::("csv").unwrap(); 238 | let field = *matches.get_one::("field").unwrap(); 239 | let version_field = matches.get_one::("version_field").map(|v| *v); 240 | if field < 1 { 241 | println!("{}\n\nApp ID field must be 1 or greater", usage); 242 | std::process::exit(1); 243 | } 244 | if let Some(version_field) = version_field { 245 | if version_field < 1 { 246 | println!("{}\n\nVersion field must be 1 or greater", usage); 247 | std::process::exit(1); 248 | } 249 | if field == version_field { 250 | println!("{}\n\nApp ID and Version fields must be different", usage); 251 | std::process::exit(1); 252 | } 253 | } 254 | match fetch_csv_list(csv, field, version_field) { 255 | Ok(csv_list) => csv_list, 256 | Err(err) => { 257 | println!("{}\n\n{:?}", usage, err); 258 | std::process::exit(1); 259 | } 260 | } 261 | } 262 | } 263 | } else { Vec::new() }; 264 | 265 | if let Some(true) = matches.get_one::("list_versions") { 266 | match download_source { 267 | DownloadSource::APKPure => { 268 | apkpure::list_versions(list, options).await; 269 | } 270 | DownloadSource::GooglePlay => { 271 | google_play::list_versions(list); 272 | } 273 | DownloadSource::FDroid => { 274 | fdroid::list_versions(list, options).await; 275 | } 276 | DownloadSource::HuaweiAppGallery => { 277 | huawei_app_gallery::list_versions(list).await; 278 | } 279 | } 280 | } else { 281 | let parallel = matches.get_one::("parallel").map(|v| *v).unwrap(); 282 | let sleep_duration = matches.get_one::("sleep_duration").map(|v| *v).unwrap(); 283 | let outpath = matches.get_one::("OUTPATH").map_or_else(|| { 284 | if oauth_token.is_none() { 285 | println!("{}\n\nOUTPATH must be specified when downloading files", usage); 286 | std::process::exit(1); 287 | } 288 | None 289 | }, |outpath| { 290 | match fs::canonicalize(outpath) { 291 | Ok(outpath) if Path::new(&outpath).is_dir() => { 292 | Some(outpath) 293 | }, 294 | _ => { 295 | println!("{}\n\nOUTPATH is not a valid directory", usage); 296 | std::process::exit(1); 297 | } 298 | } 299 | }); 300 | 301 | match download_source { 302 | DownloadSource::APKPure => { 303 | apkpure::download_apps(list, parallel, sleep_duration, &outpath.unwrap()).await; 304 | } 305 | DownloadSource::GooglePlay => { 306 | let mut email = matches.get_one::("google_email").map(|v| v.to_string()); 307 | 308 | if email.is_some() && oauth_token.is_some() { 309 | google_play::request_aas_token( 310 | &email.unwrap(), 311 | &oauth_token.unwrap(), 312 | options, 313 | ).await; 314 | } else { 315 | let mut aas_token = matches.get_one::("google_aas_token").map(|v| v.to_string()); 316 | let accept_tos = match matches.get_one::("list_versions") { 317 | Some(true) => true, 318 | _ => false, 319 | }; 320 | 321 | let ini_file = matches.get_one::("ini").map(|ini_file| { 322 | match fs::canonicalize(ini_file) { 323 | Ok(ini_file) if Path::new(&ini_file).is_file() => { 324 | ini_file 325 | }, 326 | _ => { 327 | println!("{}\n\nSpecified ini is not a valid file", usage); 328 | std::process::exit(1); 329 | }, 330 | } 331 | }); 332 | 333 | if email.is_none() || aas_token.is_none() { 334 | if let Ok(conf) = load_config(ini_file) { 335 | if email.is_none() { 336 | email = conf.get("google", "email"); 337 | } 338 | if aas_token.is_none() { 339 | aas_token = conf.get("google", "aas_token"); 340 | } 341 | } 342 | } 343 | 344 | if email.is_none() { 345 | let mut prompt_email = String::new(); 346 | print!("Email: "); 347 | io::stdout().flush().unwrap(); 348 | io::stdin().read_line(&mut prompt_email).unwrap(); 349 | email = Some(prompt_email.trim().to_string()); 350 | } 351 | 352 | if aas_token.is_none() { 353 | let mut prompt_aas_token = String::new(); 354 | print!("AAS Token: "); 355 | io::stdout().flush().unwrap(); 356 | io::stdin().read_line(&mut prompt_aas_token).unwrap(); 357 | aas_token = Some(prompt_aas_token.trim().to_string()); 358 | } 359 | 360 | google_play::download_apps( 361 | list, 362 | parallel, 363 | sleep_duration, 364 | &email.unwrap(), 365 | &aas_token.unwrap(), 366 | &outpath.unwrap(), 367 | accept_tos, 368 | options, 369 | ) 370 | .await; 371 | } 372 | } 373 | DownloadSource::FDroid => { 374 | fdroid::download_apps(list, 375 | parallel, 376 | sleep_duration, 377 | &outpath.unwrap(), 378 | options, 379 | ).await; 380 | } 381 | DownloadSource::HuaweiAppGallery => { 382 | huawei_app_gallery::download_apps(list, parallel, sleep_duration, &outpath.unwrap()).await; 383 | } 384 | } 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod progress_bar; 2 | 3 | #[derive(Clone)] 4 | pub enum OutputFormat { 5 | Json, 6 | Plaintext, 7 | } 8 | 9 | impl OutputFormat { 10 | pub fn is_json(&self) -> bool { 11 | if let Self::Json = self { 12 | true 13 | } else { 14 | false 15 | } 16 | } 17 | 18 | pub fn is_plaintext(&self) -> bool { 19 | if let Self::Plaintext = self { 20 | true 21 | } else { 22 | false 23 | } 24 | } 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/util/progress_bar.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; 4 | 5 | use crate::consts; 6 | 7 | pub fn progress_wrapper(mp: Rc) -> Box Box ()>> { 8 | Box::new(move |filename, length| { 9 | let mp1 = Rc::clone(&mp); 10 | let mp2 = Rc::clone(&mp); 11 | let pb = ProgressBar::new(length).with_message(filename); 12 | pb.set_style(ProgressStyle::with_template( 13 | consts::PROGRESS_STYLE).unwrap()); 14 | let pb = mp1.add(pb); 15 | Box::new(move |downloaded| { 16 | if !pb.is_finished() { 17 | pb.set_position(downloaded); 18 | if length == downloaded { 19 | pb.finish(); 20 | mp2.remove(&pb); 21 | } 22 | } 23 | }) 24 | }) 25 | } 26 | --------------------------------------------------------------------------------