diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f3dff49 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text eol=lf + +*.srf binary +*.pbo binary \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..451af60 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: ci +on: + pull_request: + push: + branches: + - master +jobs: + test: + name: test + env: + RUST_BACKTRACE: 1 + RUSTFLAGS: "-Dwarnings" + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Build + run: cargo build --verbose + + - name: Run Clippy + run: cargo clippy --verbose + + - name: Run tests + run: cargo test --verbose diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1dffbcb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,291 @@ +# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/ +# +# Copyright 2022-2024, axodotdev +# SPDX-License-Identifier: MIT or Apache-2.0 +# +# CI that: +# +# * checks for a Git Tag that looks like a release +# * builds artifacts with dist (archives, installers, hashes) +# * uploads those artifacts to temporary workflow zip +# * on success, uploads the artifacts to a GitHub Release +# +# Note that the GitHub Release will be created with a generated +# title/body based on your changelogs. + +name: Release +permissions: + "contents": "write" + +# This task will run whenever you push a git tag that looks like a version +# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. +# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where +# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION +# must be a Cargo-style SemVer Version (must have at least major.minor.patch). +# +# If PACKAGE_NAME is specified, then the announcement will be for that +# package (erroring out if it doesn't have the given version or isn't dist-able). +# +# If PACKAGE_NAME isn't specified, then the announcement will be for all +# (dist-able) packages in the workspace with that version (this mode is +# intended for workspaces with only one dist-able package, or with all dist-able +# packages versioned/released in lockstep). +# +# If you push multiple tags at once, separate instances of this workflow will +# spin up, creating an independent announcement for each one. However, GitHub +# will hard limit this to 3 tags per commit, as it will assume more tags is a +# mistake. +# +# If there's a prerelease-style suffix to the version, then the release(s) +# will be marked as a prerelease. +on: + pull_request: + push: + tags: + - '**[0-9]+.[0-9]+.[0-9]+*' + +jobs: + # Run 'dist plan' (or host) to determine what tasks we need to do + plan: + runs-on: "ubuntu-20.04" + outputs: + val: ${{ steps.plan.outputs.manifest }} + tag: ${{ !github.event.pull_request && github.ref_name || '' }} + tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} + publishing: ${{ !github.event.pull_request }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install dist + # we specify bash to get pipefail; it guards against the `curl` command + # failing. otherwise `sh` won't catch that `curl` returned non-0 + shell: bash + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.27.0/cargo-dist-installer.sh | sh" + - name: Cache dist + uses: actions/upload-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/dist + # sure would be cool if github gave us proper conditionals... + # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible + # functionality based on whether this is a pull_request, and whether it's from a fork. + # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* + # but also really annoying to build CI around when it needs secrets to work right.) + - id: plan + run: | + dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "dist ran successfully" + cat plan-dist-manifest.json + echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v4 + with: + name: artifacts-plan-dist-manifest + path: plan-dist-manifest.json + + # Build and packages all the platform-specific things + build-local-artifacts: + name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) + # Let the initial task tell us to not run (currently very blunt) + needs: + - plan + if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} + strategy: + fail-fast: false + # Target platforms/runners are computed by dist in create-release. + # Each member of the matrix has the following arguments: + # + # - runner: the github runner + # - dist-args: cli flags to pass to dist + # - install-dist: expression to run to install dist on the runner + # + # Typically there will be: + # - 1 "global" task that builds universal installers + # - N "local" tasks that build each platform's binaries and platform-specific installers + matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} + runs-on: ${{ matrix.runner }} + container: ${{ matrix.container && matrix.container.image || null }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json + steps: + - name: enable windows longpaths + run: | + git config --global core.longpaths true + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install Rust non-interactively if not already installed + if: ${{ matrix.container }} + run: | + if ! command -v cargo > /dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + fi + - name: Install dist + run: ${{ matrix.install_dist.run }} + # Get the dist-manifest + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - name: Install dependencies + run: | + ${{ matrix.packages_install }} + - name: Build artifacts + run: | + # Actually do builds and make zips and whatnot + dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "dist ran successfully" + - id: cargo-dist + name: Post-build + # We force bash here just because github makes it really hard to get values up + # to "real" actions without writing to env-vars, and writing to env-vars has + # inconsistent syntax between shell and powershell. + shell: bash + run: | + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-local-${{ join(matrix.targets, '_') }} + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + + # Build and package all the platform-agnostic(ish) things + build-global-artifacts: + needs: + - plan + - build-local-artifacts + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cached dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist + # Get all the local artifacts for the global tasks to use (for e.g. checksums) + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: cargo-dist + shell: bash + run: | + dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "dist ran successfully" + + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-global + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + # Determines if we should publish/announce + host: + needs: + - plan + - build-local-artifacts + - build-global-artifacts + # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) + if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + runs-on: "ubuntu-20.04" + outputs: + val: ${{ steps.host.outputs.manifest }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cached dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist + # Fetch artifacts from scratch-storage + - name: Fetch artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: host + shell: bash + run: | + dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + echo "artifacts uploaded and released successfully" + cat dist-manifest.json + echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v4 + with: + # Overwrite the previous copy + name: artifacts-dist-manifest + path: dist-manifest.json + # Create a GitHub Release while uploading all files to it + - name: "Download GitHub Artifacts" + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: artifacts + merge-multiple: true + - name: Cleanup + run: | + # Remove the granular manifests + rm -f artifacts/*-dist-manifest.json + - name: Create GitHub Release + env: + PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" + ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" + ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" + RELEASE_COMMIT: "${{ github.sha }}" + run: | + # Write and read notes from a file to avoid quoting breaking things + echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt + + gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* + + announce: + needs: + - plan + - host + # use "always() && ..." to allow us to wait for all publish jobs while + # still allowing individual publish jobs to skip themselves (for prereleases). + # "host" however must run to completion, no skipping allowed! + if: ${{ always() && needs.host.result == 'success' }} + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive diff --git a/Cargo.lock b/Cargo.lock index 1a39bbe..c442492 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,15 +21,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "bitflags" @@ -39,18 +39,18 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" -version = "0.9.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ "generic-array", ] [[package]] name = "bumpalo" -version = "3.9.1" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "byteorder" @@ -60,9 +60,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cc" -version = "1.0.72" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" [[package]] name = "cfg-if" @@ -78,50 +78,125 @@ checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" [[package]] name = "clap" -version = "3.1.3" +version = "4.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f8c0e2a6b902acc18214e24a6935cdaf8a8e34231913d4404dcaee659f65a1" +checksum = "2148adefda54e14492fb9bddcc600b4344c5d1a3123bd666dcb939c6f0e0e57e" dependencies = [ "atty", "bitflags", "clap_derive", - "indexmap", - "lazy_static", - "os_str_bytes", + "clap_lex", + "once_cell", "strsim", "termcolor", - "textwrap", ] [[package]] name = "clap_derive" -version = "3.1.2" +version = "4.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d42c94ce7c2252681b5fed4d3627cc807b13dfc033246bd05d5b252399000e" +checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" dependencies = [ - "heck 0.4.0", + "heck", "proc-macro-error", "proc-macro2", "quote", "syn", ] +[[package]] +name = "clap_lex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "console" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "terminal_size", + "unicode-width", + "winapi", +] + [[package]] name = "crc32fast" -version = "1.3.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "738c290dfaea84fc1ca15ad9c168d083b05a714e1efddd8edaab678dc28d2836" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ "cfg-if", ] [[package]] -name = "digest" -version = "0.9.0" +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bf8df95e795db1a4aca2957ad884a2df35413b24bbeb3114422f3cc21498e8" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "422f23e724af1240ec469ea1e834d87a4b59ce2efe2c6a96256b0c47e2fd86aa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", ] [[package]] @@ -130,53 +205,56 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + [[package]] name = "flate2" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ - "cfg-if", "crc32fast", - "libc", "miniz_oxide", ] [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", ] -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - [[package]] name = "heck" version = "0.4.0" @@ -192,38 +270,54 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "idna" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] [[package]] -name = "indexmap" -version = "1.8.0" +name = "indicatif" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +checksum = "4295cbb7573c16d310e99e713cf9e75101eb190ab31fccd35f2d2691b4352b19" dependencies = [ - "autocfg", - "hashbrown", + "console", + "number_prefix", + "portable-atomic", + "unicode-width", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", ] [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -236,50 +330,44 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.112" +version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] name = "log" -version = "0.4.14" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", ] -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "md-5" -version = "0.9.1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ - "block-buffer", "digest", - "opaque-debug", ] [[package]] -name = "memchr" -version = "2.4.1" +name = "memoffset" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] [[package]] name = "miniz_oxide" -version = "0.4.4" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", - "autocfg", ] [[package]] @@ -288,40 +376,76 @@ version = "0.1.0" dependencies = [ "byteorder", "clap", + "hex", + "indicatif", "md-5", + "open", + "percent-encoding", + "rayon", "relative-path", "serde", "serde_json", "snafu", + "tempfile", "ureq", + "walkdir", ] [[package]] -name = "once_cell" -version = "1.9.0" +name = "num_cpus" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +dependencies = [ + "hermit-abi", + "libc", +] [[package]] -name = "opaque-debug" -version = "0.3.0" +name = "number_prefix" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] -name = "os_str_bytes" -version = "6.0.0" +name = "once_cell" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "open" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" dependencies = [ - "memchr", + "pathdiff", + "windows-sys", ] +[[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "portable-atomic" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15eb2c6e362923af47e13c23ca5afb859e83d54452c55b0b9ac763b8f7c1ac16" [[package]] name = "proc-macro-error" @@ -349,31 +473,72 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.30" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.10" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] +[[package]] +name = "rayon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e060280438193c554f654141c9ea9417886713b7acd75974c85b18a69a88e0b" +dependencies = [ + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + [[package]] name = "relative-path" -version = "1.6.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a49a831dc1e13c9392b660b162333d4cb0033bbbdfe6a1687177e59e89037c86" +checksum = "0df32d82cedd1499386877b062ebe8721f806de80b08d183c70184ef17dd1d42" dependencies = [ "serde", ] +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "ring" version = "0.16.20" @@ -391,9 +556,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.2" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d37e5e2290f3e040b594b1a9e04377c2c671f1a1cfd9bfdef82106ac1c113f84" +checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c" dependencies = [ "log", "ring", @@ -403,9 +568,24 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "sct" @@ -419,18 +599,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.130" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.130" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" dependencies = [ "proc-macro2", "quote", @@ -439,9 +619,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.68" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +checksum = "8e8b3801309262e8184d9687fb697586833e939767aea0dda89f5a8e650e8bd7" dependencies = [ "itoa", "ryu", @@ -450,9 +630,9 @@ dependencies = [ [[package]] name = "snafu" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eba135d2c579aa65364522eb78590cdf703176ef71ad4c32b00f58f7afb2df5" +checksum = "a152ba99b054b22972ee794cf04e5ef572da1229e33b65f3c57abbff0525a454" dependencies = [ "doc-comment", "snafu-derive", @@ -460,11 +640,11 @@ dependencies = [ [[package]] name = "snafu-derive" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7fe9b0669ef117c5cabc5549638528f36771f058ff977d7689deb517833a75" +checksum = "d5e79cdebbabaebb06a9bdbaedc7f159b410461f63611d4d0e3fb0fab8fed850" dependencies = [ - "heck 0.3.3", + "heck", "proc-macro2", "quote", "syn", @@ -484,35 +664,53 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.80" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", ] [[package]] name = "termcolor" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" dependencies = [ "winapi-util", ] [[package]] -name = "textwrap" -version = "0.14.2" +name = "terminal_size" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] [[package]] name = "tinyvec" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] @@ -525,36 +723,36 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "typenum" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "unicode-bidi" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" [[package]] name = "unicode-normalization" -version = "0.1.19" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] -name = "unicode-segmentation" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" - -[[package]] -name = "unicode-xid" -version = "0.2.2" +name = "unicode-width" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "untrusted" @@ -564,9 +762,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "ureq" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9399fa2f927a3d327187cbd201480cee55bee6ac5d3c77dd27f0c6814cff16d5" +checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f" dependencies = [ "base64", "chunked_transfer", @@ -583,27 +781,37 @@ dependencies = [ [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -611,13 +819,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -626,9 +834,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -636,9 +844,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -649,15 +857,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "web-sys" -version = "0.3.55" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" dependencies = [ "js-sys", "wasm-bindgen", @@ -675,9 +883,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.2" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552ceb903e957524388c4d3475725ff2c8b7960922063af6ce53c9a43da07449" +checksum = "368bfe657969fb01238bb756d351dcade285e0f6fcbd36dcb23359a5169975be" dependencies = [ "webpki", ] @@ -712,3 +920,60 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" diff --git a/Cargo.toml b/Cargo.toml index a943c13..671ead7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,15 +4,28 @@ version = "0.1.0" edition = "2021" authors = ["Victor Chiletto "] license = "GPL-3.0-or-later" +repository = "https://github.com/vitorhnn/nimble/" [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" snafu = "0.7" -md-5 = { version = "0.9", features = [] } +md-5 = { version = "0.10", features = [] } byteorder = "1" ureq = { version = "2", features = ["tls", "json"] } relative-path = { version = "1", features = ["serde"] } -clap = { version = "3", features = ["derive"] } +clap = { version = "4", features = ["derive"] } +rayon = "1" +walkdir = "2" +indicatif = "0.17" +tempfile = "3" +hex = "0.4" +open = "3" +percent-encoding = "2" + +# The profile that 'dist' will build with +[profile.dist] +inherits = "release" +lto = "thin" # tinyvec = { version = "1.5", features = ["alloc", "rustc_1_55"] } diff --git a/README.md b/README.md index 122b01f..58941ce 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,47 @@ # Nimble -Nimble is (or will be) a Swifty-compatible, cross platform, (currently) CLI only mod manager for Arma 3. -It is being built because I want to play with my Swifty using group on Linux :) - -## Goals -In order of priority: - * Full compatibility with Swifty - * Support all major platforms where you can reasonably run Arma 3 (Windows, Linux w/ Proton, maybe macOS?) - * Have decent usability - * This implies some form of user interface system. Rust's GUI story is kinda wonky, so maybe we'll have to settle for a TUI - * Generate Swifty repositories (swifty-cli create equivalent) - * Create symlinks instead of copying mod files - -## Big bucket list of things to do: - * Switch to proper errors instead of `snafu::Whatever` - * There's also way too much unwrapping because I got lazy - * Implement part level downloading, instead of redownloading entire files - * Clean up Path vs PathBuf vs &str vs String - * RelativePath and RelativePathBuf should be used in most cases. - * We still need to convert Windows backslashes to a sane separator on *nix platforms - * Use rayon for srf generation - * Investigate why download speeds are kinda wonky - * Seems to be an issue with my connection to the repo I was using for testing? - * Properly deal with invalid PBOs \ No newline at end of file +Nimble is a Swifty-compatible, cross platform, (currently) CLI only mod manager for Arma 3. + +# Installing + +Nimble can be installed via Cargo: + +``` +cargo install --git https://github.com/vitorhnn/nimble.git +``` + +# Usage + +## Mod synchronization + +Unlike Swifty, Nimble (currently) is not capable of detecting when a repo is outdated, +so whenever your group pushes updates or when first installing, you must run: + +``` +nimble sync --repo-url --path +``` + +### Storage path restriction +For Linux under Proton, the mod storage path must be inside Arma 3's Proton prefix "drive_c", e.g: +``` +nimble sync --repo-url https://example.com/swifty/ --path /home/foo/.local/share/Steam/steamapps/compatdata/107410/pfx/drive_c/arma_mods +``` + +This restriction will be removed in the future. + +## Arma 3 launching + +On Windows and Linux with Proton, Nimble can launch Arma 3 using the `steam://` protocol: + +``` +nimble launch --path +``` + +## SRF generation + +The mod cache can be forcefully regenerated if required: +``` +nimble gen-srf --path +``` + +This should only be needed if you manually made changes to the mods. diff --git a/dist-workspace.toml b/dist-workspace.toml new file mode 100644 index 0000000..a9c05e2 --- /dev/null +++ b/dist-workspace.toml @@ -0,0 +1,13 @@ +[workspace] +members = ["cargo:."] + +# Config for 'dist' +[dist] +# The preferred dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.27.0" +# CI backends to support +ci = "github" +# The installers to generate for each app +installers = [] +# Target platforms to build apps for (Rust target-triple syntax) +targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] diff --git a/src/commands/gen_srf.rs b/src/commands/gen_srf.rs new file mode 100644 index 0000000..19333db --- /dev/null +++ b/src/commands/gen_srf.rs @@ -0,0 +1,55 @@ +use crate::md5_digest::Md5Digest; +use crate::mod_cache::ModCache; +use crate::{mod_cache, srf}; +use rayon::prelude::*; +use std::collections::HashMap; +use std::fs::File; +use std::io::BufWriter; +use std::path::Path; +use walkdir::WalkDir; + +pub fn gen_srf_for_mod(mod_path: &Path) -> srf::Mod { + let generated_srf = srf::scan_mod(mod_path).unwrap(); + + let path = mod_path.join("mod.srf"); + + let writer = BufWriter::new(File::create(path).unwrap()); + serde_json::to_writer(writer, &generated_srf).unwrap(); + + generated_srf +} + +pub fn open_cache_or_gen_srf(base_path: &Path) -> Result { + match ModCache::from_disk(base_path) { + Ok(cache) => Ok(cache), + Err(mod_cache::Error::FileOpen { source }) + if source.kind() == std::io::ErrorKind::NotFound => + { + println!("nimble-cache.json not found, generating..."); + gen_srf(base_path); + ModCache::from_disk_or_empty(base_path) + } + Err(e) => Err(e), + } +} + +pub fn gen_srf(base_path: &Path) { + let mods: HashMap = WalkDir::new(base_path) + .min_depth(1) + .max_depth(1) + .into_iter() + .par_bridge() + .filter_map(Result::ok) + .filter(|e| e.file_type().is_dir() && e.file_name().to_string_lossy().starts_with('@')) + .map(|entry| { + let path = entry.path(); + let srf = gen_srf_for_mod(path); + + (srf.checksum.clone(), srf) + }) + .collect(); + + let cache = ModCache::new(mods); + + cache.to_disk(base_path).unwrap(); +} diff --git a/src/commands/launch.rs b/src/commands/launch.rs new file mode 100644 index 0000000..63674e5 --- /dev/null +++ b/src/commands/launch.rs @@ -0,0 +1,96 @@ +use crate::commands::gen_srf::open_cache_or_gen_srf; +use crate::mod_cache; +use crate::mod_cache::ModCache; +use snafu::{ResultExt, Snafu}; +use std::cfg; +use std::path::{Path, PathBuf}; + +#[cfg(not(windows))] +use snafu::OptionExt; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to open ModCache: {}", source))] + ModCacheOpen { source: mod_cache::Error }, + #[snafu(display("failed to find drive_c"))] + #[cfg(not(windows))] + FailedToFindDriveC, +} + +fn generate_mod_args(base_path: &Path, mod_cache: &ModCache) -> String { + mod_cache + .mods + .values() + .fold(String::from("-noLauncher -mod="), |acc, r#mod| { + let mod_name = &r#mod.name; + let full_path = base_path + .join(Path::new(mod_name)) + .to_string_lossy() + .to_string(); + format!("{acc}{full_path};") + }) +} + +// if we're on windows we don't have to do anything +#[cfg(windows)] +fn convert_host_base_path_to_proton_base_path(host_base_path: &Path) -> Result { + Ok(host_base_path.to_owned()) +} + +// if we're not on windows, try to find a "drive_c" dir in the ancestors of base_path +#[cfg(not(windows))] +fn convert_host_base_path_to_proton_base_path(host_base_path: &Path) -> Result { + let drive_c_path = host_base_path + .ancestors() + .find(|&x| x.ends_with("drive_c")) + .context(FailedToFindDriveCSnafu)?; + + let relative = host_base_path + .strip_prefix(drive_c_path) + .expect("drive_c_path was not a prefix of host_base_path, this should never happen"); + + Ok(Path::new("c:/").join(relative)) +} + +pub fn launch(base_path: &Path) -> Result<(), Error> { + let mod_cache = open_cache_or_gen_srf(base_path).context(ModCacheOpenSnafu)?; + + let proton_base_path = convert_host_base_path_to_proton_base_path(base_path)?; + + let binding = generate_mod_args(&proton_base_path, &mod_cache); + let cmdline = + percent_encoding::utf8_percent_encode(&binding, percent_encoding::NON_ALPHANUMERIC); + + let steam_url = format!("steam://run/107410//{cmdline}/"); + + dbg!(&steam_url); + + open::that(steam_url).unwrap(); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(windows)] + fn test_proton_path_conversion() { + // on windows, this should do nothing + let original_path = PathBuf::from("C:\\random\\paths\\drive_c\\banana_repo"); + let converted = convert_host_base_path_to_proton_base_path(&original_path).unwrap(); + + assert_eq!(original_path, converted); + } + + #[test] + #[cfg(not(windows))] + fn test_proton_path_conversion() { + // on windows, this should do nothing + let original_path = PathBuf::from("/home/random/paths/drive_c/banana_repo"); + let converted = convert_host_base_path_to_proton_base_path(&original_path).unwrap(); + + assert_eq!(converted, PathBuf::from("c:/banana_repo")); + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index eac0ca6..4b6307e 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,3 +1,3 @@ - +pub mod gen_srf; +pub mod launch; pub mod sync; - diff --git a/src/commands/sync.rs b/src/commands/sync.rs index 3b671f8..65804b8 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -1,92 +1,122 @@ +use crate::commands::gen_srf::{gen_srf_for_mod, open_cache_or_gen_srf}; +use crate::mod_cache::ModCache; +use crate::{repository, srf}; +use indicatif::{ProgressBar, ProgressState, ProgressStyle}; +use snafu::{ResultExt, Snafu}; use std::collections::HashMap; use std::fs::File; -use std::io::{BufReader, Read}; +use std::io::{BufReader, BufWriter, Cursor, Read, Seek, SeekFrom}; use std::path::Path; -use snafu::{ResultExt, Whatever}; -use crate::{srf, repository}; +use tempfile::tempfile; -fn diff_repos<'a>( - local_repo: &repository::Repository, +#[derive(Debug)] +struct DownloadCommand { + file: String, + + // These are currently unused. TODO: implement file diffing. + #[allow(dead_code)] + begin: u64, + #[allow(dead_code)] + end: u64, +} + +#[derive(Snafu, Debug)] +pub enum Error { + #[snafu(display("io error: {}", source))] + Io { source: std::io::Error }, + #[snafu(display("Error while requesting repository data: {}", source))] + Http { + url: String, + + #[snafu(source(from(ureq::Error, Box::new)))] + source: Box, + }, + #[snafu(display("Failed to fetch repository info: {}", source))] + RepositoryFetch { source: repository::Error }, + #[snafu(display("SRF deserialization failure: {}", source))] + SrfDeserialization { source: serde_json::Error }, + #[snafu(display("Legacy SRF deserialization failure: {}", source))] + LegacySrfDeserialization { source: srf::Error }, + #[snafu(display("Failed to generate SRF: {}", source))] + SrfGeneration { source: srf::Error }, + #[snafu(display("Failed to open ModCache: {}", source))] + ModCacheOpen { source: crate::mod_cache::Error }, +} + +fn diff_repo<'a>( + mod_cache: &ModCache, remote_repo: &'a repository::Repository, ) -> Vec<&'a repository::Mod> { let mut downloads = Vec::new(); - if local_repo.checksum == remote_repo.checksum { - return vec![]; - } - - let mut checksum_map = HashMap::new(); + // repo checksums use the repo generation timestamp in the checksum calculation, so we can't really + // generate them for comparison. they aren't that useful anyway - for _mod in &local_repo.required_mods { - checksum_map.insert(&_mod.checksum, _mod); - } - - for _mod in &remote_repo.required_mods { - match checksum_map.get(&_mod.checksum) { - None => downloads.push(_mod), - Some(local_mod) if local_mod.checksum != _mod.checksum => downloads.push(_mod), - _ => (), + for r#mod in &remote_repo.required_mods { + if !mod_cache.mods.contains_key(&r#mod.checksum) { + downloads.push(r#mod); } } downloads } -#[derive(Debug)] -struct DownloadCommand { - file: String, - begin: u64, - end: u64, -} - fn diff_mod( agent: &ureq::Agent, repo_base_path: &str, local_base_path: &Path, remote_mod: &repository::Mod, -) -> Result, Whatever> { +) -> Result, Error> { // HACK HACK: this REALLY should be parsed through streaming rather than through buffering the whole thing + let remote_srf_url = format!("{}{}/mod.srf", repo_base_path, remote_mod.mod_name); let mut remote_srf = agent - .get(&format!( - "{}{}/mod.srf", - repo_base_path, remote_mod.mod_name - )) + .get(&remote_srf_url) .call() - .unwrap() + .context(HttpSnafu { + url: remote_srf_url, + })? .into_reader(); let mut buf = String::new(); - let _len = remote_srf.read_to_string(&mut buf).unwrap(); + let _len = remote_srf.read_to_string(&mut buf).context(IoSnafu)?; // yeet utf-8 bom, which is bad, not very useful and not supported by serde - let bomless = buf.trim_start_matches("\u{feff}"); + let bomless = buf.trim_start_matches('\u{feff}'); + + let remote_is_legacy = srf::is_legacy_srf(&mut Cursor::new(bomless)).context(IoSnafu)?; - let remote_srf: srf::Mod = serde_json::from_str(&bomless).unwrap(); /*.or_else(|_| { - srf::deserialize_legacy_srf(&mut BufReader::new(Cursor::new(remote_srf))) - }).with_whatever_context(|_| "failed to deserialize remote srf")?;*/ + let remote_srf: srf::Mod = if remote_is_legacy { + srf::deserialize_legacy_srf(&mut BufReader::new(Cursor::new(bomless))) + .context(LegacySrfDeserializationSnafu)? + } else { + serde_json::from_str(bomless).context(SrfDeserializationSnafu)? + }; let local_path = local_base_path.join(Path::new(&format!("{}/", remote_mod.mod_name))); let srf_path = local_path.join(Path::new("mod.srf")); let local_srf = { - if !local_path.exists() { - srf::Mod::generate_invalid(&remote_srf) - } else { - let file = File::open(&srf_path); + if local_path.exists() { + let file = File::open(srf_path); match file { Ok(file) => { let mut reader = BufReader::new(file); - serde_json::from_reader(&mut reader) - .or_else(|_| srf::deserialize_legacy_srf(&mut reader)) - .with_whatever_context(|_| "failed to deserialize local srf")? + if srf::is_legacy_srf(&mut reader).context(IoSnafu)? { + srf::deserialize_legacy_srf(&mut reader) + .context(LegacySrfDeserializationSnafu)? + } else { + serde_json::from_reader(&mut reader).context(SrfDeserializationSnafu)? + } } Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - srf::scan_mod(&local_path).unwrap() + srf::scan_mod(&local_path).context(SrfGenerationSnafu)? } - _ => panic!(), + Err(e) => return Err(Error::Io { source: e }), } + } else { + srf::Mod::generate_invalid(&remote_srf) } }; @@ -116,75 +146,143 @@ fn diff_mod( // TODO: implement file diffing. for now, just download everything download_list.push(DownloadCommand { - file: format!("{}/{}", remote_srf.name, path.to_string().to_string()), + file: format!("{}/{}", remote_srf.name, path), begin: 0, end: file.length, - }) + }); } } else { download_list.push(DownloadCommand { - file: format!("{}/{}", remote_srf.name, path.to_string().to_string()), + file: format!("{}/{}", remote_srf.name, path), begin: 0, end: file.length, - }) + }); } } + // remove any local files that remain here + remove_leftover_files(local_base_path, &remote_srf, local_files.into_values()) + .context(IoSnafu)?; + Ok(download_list) } +// remove files that are present in the local disk but not in the remote repo +fn remove_leftover_files<'a>( + local_base_path: &Path, + r#mod: &srf::Mod, + files: impl Iterator, +) -> Result<(), std::io::Error> { + for file in files { + let path = file + .path + .to_path(local_base_path.join(Path::new(&r#mod.name))); + + println!("removing leftover file {}", &path.display()); + + std::fs::remove_file(&path)?; + } + + Ok(()) +} + fn execute_command_list( agent: &mut ureq::Agent, remote_base: &str, local_base: &Path, commands: &[DownloadCommand], -) { +) -> Result<(), Error> { for (i, command) in commands.iter().enumerate() { - println!("downloading {} of {}", i, commands.len()); + println!("downloading {} of {} - {}", i, commands.len(), command.file); - let file_path = local_base.join(Path::new(&command.file)); - std::fs::create_dir_all(file_path.parent().unwrap()).unwrap(); - let mut local_file = File::create(&file_path).unwrap(); + // download into temp file first in case we have a failure. this avoids us writing garbage data + // which will later make us crash in gen_srf + let mut temp_download_file = tempfile().context(IoSnafu)?; let remote_url = format!("{}{}", remote_base, command.file); - let mut reader = agent.get(&remote_url).call().unwrap().into_reader(); + let response = agent.get(&remote_url).call().context(HttpSnafu { + url: remote_url.clone(), + })?; + + let pb = response + .header("Content-Length") + .and_then(|len| len.parse().ok()) + .map_or_else(ProgressBar::new_spinner, ProgressBar::new); - std::io::copy(&mut reader, &mut local_file).unwrap(); + pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})") + .unwrap() + .with_key("eta", |state: &ProgressState, w: &mut dyn std::fmt::Write| write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()) + .progress_chars("#>-")); + + let reader = response.into_reader(); + + std::io::copy(&mut pb.wrap_read(reader), &mut temp_download_file).context(IoSnafu)?; + + // copy from temp to permanent file + let file_path = local_base.join(Path::new(&command.file)); + std::fs::create_dir_all(file_path.parent().expect("file_path did not have a parent")) + .context(IoSnafu)?; + let mut local_file = File::create(&file_path).context(IoSnafu)?; + + temp_download_file + .seek(SeekFrom::Start(0)) + .context(IoSnafu)?; + std::io::copy(&mut temp_download_file, &mut local_file).context(IoSnafu)?; } + + Ok(()) } -pub fn sync(agent: &mut ureq::Agent, repo_url: &str, base_path: &Path) { - let remote_repo = - repository::get_repository_info(agent, &format!("{}/repo.json", repo_url)).unwrap(); +pub fn sync( + agent: &mut ureq::Agent, + repo_url: &str, + base_path: &Path, + dry_run: bool, +) -> Result<(), Error> { + let remote_repo = repository::get_repository_info(agent, &format!("{repo_url}/repo.json")) + .context(RepositoryFetchSnafu)?; - let local_repo: repository::Repository = { - let file = std::fs::File::open(base_path.join("./repo.json")); + let mut mod_cache = open_cache_or_gen_srf(base_path).context(ModCacheOpenSnafu)?; - match file { - Err(e) => { - if e.kind() == std::io::ErrorKind::NotFound { - repository::replicate_remote_repo_info(&remote_repo) - } else { - panic!(); - } - } - Ok(file) => serde_json::from_reader(BufReader::new(file)).unwrap(), - } - }; + let check = diff_repo(&mod_cache, &remote_repo); - let check = diff_repos(&local_repo, &remote_repo); + println!("mods to check: {check:#?}"); - println!("mods to check: {:#?}", check); + // remove all mods to check from cache, we'll read them later + for r#mod in &check { + mod_cache.remove(&r#mod.checksum); + } let mut download_commands = vec![]; - for _mod in check { - download_commands.extend(diff_mod(agent, repo_url, base_path, _mod).unwrap()); + for r#mod in &check { + download_commands.extend(diff_mod(agent, repo_url, base_path, r#mod).unwrap()); } - println!("download commands: {:#?}", download_commands); + println!("download commands: {download_commands:#?}"); - execute_command_list(agent, repo_url, base_path, &download_commands); -} + if dry_run { + return Ok(()); + } + + let res = execute_command_list(agent, repo_url, base_path, &download_commands); + if let Err(e) = res { + println!("an error occured while downloading: {e}"); + println!("you should retry this command"); + } + + // gen_srf for the mods we downloaded + for r#mod in &check { + let srf = gen_srf_for_mod(&base_path.join(Path::new(&r#mod.mod_name))); + + mod_cache.insert(srf); + } + + // reserialize the cache + let writer = BufWriter::new(File::create(base_path.join("nimble-cache.json")).unwrap()); + serde_json::to_writer(writer, &mod_cache).unwrap(); + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 9aeb806..9b8c910 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,13 @@ -use snafu::{ResultExt, Whatever}; -use std::fs::File; -use std::io::{BufReader, Read}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use clap::{Parser, Subcommand}; +mod commands; +mod md5_digest; +mod mod_cache; mod pbo; mod repository; mod srf; -mod commands; - #[derive(Subcommand)] enum Commands { @@ -18,7 +16,18 @@ enum Commands { repo_url: String, #[clap(short, long)] - local_path: PathBuf, + path: PathBuf, + + #[clap(short, long)] + dry_run: bool, + }, + GenSrf { + #[clap(short, long)] + path: PathBuf, + }, + Launch { + #[clap(short, long)] + path: PathBuf, }, } @@ -38,7 +47,16 @@ fn main() { match args.command { Commands::Sync { repo_url, - local_path, - } => commands::sync::sync(&mut agent, &repo_url, &local_path), + path, + dry_run, + } => { + commands::sync::sync(&mut agent, &repo_url, &path, dry_run).unwrap(); + } + Commands::GenSrf { path } => { + commands::gen_srf::gen_srf(&path); + } + Commands::Launch { path } => { + commands::launch::launch(&path).unwrap(); + } } } diff --git a/src/md5_digest.rs b/src/md5_digest.rs new file mode 100644 index 0000000..61ac67f --- /dev/null +++ b/src/md5_digest.rs @@ -0,0 +1,61 @@ +use hex::FromHexError; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use snafu::{ResultExt, Snafu}; +use std::fmt::{Debug, Formatter}; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("hex digest decode error: {}", source))] + HexDecode { source: FromHexError }, +} + +#[derive(Default, Hash, PartialEq, Eq, Clone)] +pub struct Md5Digest { + inner: [u8; 16], +} + +impl Md5Digest { + pub fn new(digest: &str) -> Result { + let mut inner = [0; 16]; + hex::decode_to_slice(digest, &mut inner).context(HexDecodeSnafu)?; + + Ok(Self { inner }) + } + + pub fn from_bytes(bytes: [u8; 16]) -> Self { + Self { inner: bytes } + } +} + +impl Serialize for Md5Digest { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let digest = hex::encode_upper(self.inner); + + serializer.serialize_str(&digest) + } +} + +impl<'de> Deserialize<'de> for Md5Digest { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let digest = String::deserialize(deserializer)?; + + let mut inner = [0; 16]; + hex::decode_to_slice(digest, &mut inner).map_err(serde::de::Error::custom)?; + + Ok(Self::from_bytes(inner)) + } +} + +impl Debug for Md5Digest { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Md5Digest") + .field("inner", &hex::encode_upper(self.inner)) + .finish() + } +} diff --git a/src/mod_cache.rs b/src/mod_cache.rs new file mode 100644 index 0000000..ad8e529 --- /dev/null +++ b/src/mod_cache.rs @@ -0,0 +1,94 @@ +use crate::md5_digest::Md5Digest; +use serde::{Deserialize, Serialize}; +use snafu::{ResultExt, Snafu}; +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufReader, BufWriter}; +use std::path::Path; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to create cache file: {}", source))] + FileCreation { source: std::io::Error }, + #[snafu(display("failed to open cache file: {}", source))] + FileOpen { source: std::io::Error }, + #[snafu(display("serde failed to serialize: {}", source))] + Serialization { source: serde_json::Error }, + #[snafu(display("serde failed to deserialize: {}", source))] + Deserialization { source: serde_json::Error }, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Mod { + pub name: String, +} + +impl From for Mod { + fn from(value: crate::srf::Mod) -> Self { + Mod { name: value.name } + } +} + +type SrfMod = crate::srf::Mod; + +#[derive(Serialize, Deserialize)] +pub struct ModCache { + version: u32, + pub mods: HashMap, +} + +impl ModCache { + pub fn new(mods: HashMap) -> Self { + Self { + version: 1, + mods: mods.into_iter().map(|(k, v)| (k, v.into())).collect(), + } + } + + pub fn new_empty() -> Self { + Self { + version: 1, + mods: HashMap::new(), + } + } + + pub fn from_disk(repo_path: &Path) -> Result { + let path = repo_path.join("nimble-cache.json"); + let open_result = File::open(path); + match open_result { + Ok(file) => { + let reader = BufReader::new(file); + serde_json::from_reader(reader).context(DeserializationSnafu) + } + Err(e) => Err(Error::FileOpen { source: e }), + } + } + + pub fn from_disk_or_empty(repo_path: &Path) -> Result { + match Self::from_disk(repo_path) { + Ok(cache) => Ok(cache), + Err(Error::FileOpen { source }) if source.kind() == std::io::ErrorKind::NotFound => { + Ok(Self::new_empty()) + } + Err(e) => Err(e), + } + } + + pub fn to_disk(&self, repo_path: &Path) -> Result<(), Error> { + let path = repo_path.join("nimble-cache.json"); + let file = File::create(path).context(FileCreationSnafu)?; + let writer = BufWriter::new(file); + + serde_json::to_writer(writer, &self).context(SerializationSnafu)?; + + Ok(()) + } + + pub fn remove(&mut self, checksum: &Md5Digest) { + self.mods.remove(checksum); + } + + pub fn insert(&mut self, r#mod: crate::srf::Mod) { + self.mods.insert(r#mod.checksum.clone(), r#mod.into()); + } +} diff --git a/src/pbo.rs b/src/pbo.rs index db1066c..9b232b2 100644 --- a/src/pbo.rs +++ b/src/pbo.rs @@ -1,22 +1,24 @@ +use std::ffi::FromVecWithNulError; use std::{ collections::HashMap, - ffi::CStr, + ffi::CString, io::{BufRead, Seek}, }; use byteorder::{LittleEndian, ReadBytesExt}; -use snafu::{ResultExt, Whatever}; +use snafu::{ResultExt, Snafu}; #[derive(Debug)] pub struct Pbo { pub input: I, - pub header: PboEntry, pub header_len: u64, + // We parse this but never really use it. + #[allow(dead_code)] pub extensions: HashMap, pub entries: Vec, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum EntryType { Vers, Cprs, @@ -28,52 +30,54 @@ pub enum EntryType { pub struct PboEntry { pub filename: String, pub r#type: EntryType, + pub data_size: u32, + // We parse this but never really use it. + #[allow(dead_code)] pub original_size: u32, + #[allow(dead_code)] pub offset: u32, + #[allow(dead_code)] pub timestamp: u32, - pub data_size: u32, } -fn read_string(input: &mut I) -> Result { +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("io error: {}", source))] + Io { source: std::io::Error }, + #[snafu(display("unknown pbo type: {}", r#type))] + PboType { r#type: u32 }, + #[snafu(display("string deserialization error: {}", source))] + StringDeserialization { source: FromVecWithNulError }, +} + +fn read_string(input: &mut I) -> Result { let mut buf = Vec::new(); - input - .read_until(b'\0', &mut buf) - .with_whatever_context(|_| "read failure")?; + input.read_until(b'\0', &mut buf).context(IoSnafu {})?; - let str = unsafe { CStr::from_bytes_with_nul_unchecked(&buf) }.to_string_lossy(); + let cstring = CString::from_vec_with_nul(buf).context(StringDeserializationSnafu)?; - Ok(str.to_string()) + Ok(cstring.to_string_lossy().to_string()) } impl PboEntry { - fn read(input: &mut I) -> Result { + fn read(input: &mut I) -> Result { let filename = read_string(input)?; - let r#type = input - .read_u32::() - .with_whatever_context(|_| "read failure")?; + let r#type = input.read_u32::().context(IoSnafu {})?; let r#type = match r#type { 0x56657273 => EntryType::Vers, 0x43707273 => EntryType::Cprs, 0x456e6372 => EntryType::Enco, 0x00000000 => EntryType::None, - _ => panic!(), + _ => return Err(Error::PboType { r#type }), }; - let original_size = input - .read_u32::() - .with_whatever_context(|_| "read failure")?; - let offset = input - .read_u32::() - .with_whatever_context(|_| "read failure")?; - let timestamp = input - .read_u32::() - .with_whatever_context(|_| "read failure")?; - let data_size = input - .read_u32::() - .with_whatever_context(|_| "read failure")?; + let original_size = input.read_u32::().context(IoSnafu {})?; + let offset = input.read_u32::().context(IoSnafu {})?; + let timestamp = input.read_u32::().context(IoSnafu {})?; + let data_size = input.read_u32::().context(IoSnafu {})?; Ok(PboEntry { filename, @@ -86,7 +90,7 @@ impl PboEntry { } } -fn read_extensions(input: &mut I) -> Result, Whatever> { +fn read_extensions(input: &mut I) -> Result, Error> { let mut output_map = HashMap::new(); loop { @@ -103,14 +107,8 @@ fn read_extensions(input: &mut I) -> Result Pbo { - pub fn read(mut input: I) -> Result { - let header = PboEntry::read(&mut input)?; - - if !header.filename.is_empty() || header.r#type != EntryType::Vers { - panic!(); - } - - let extensions = read_extensions(&mut input)?; + pub fn read(mut input: I) -> Result { + let mut extensions = HashMap::new(); let mut entries = Vec::new(); @@ -121,6 +119,10 @@ impl Pbo { break; } + if entry.r#type == EntryType::Vers { + extensions = read_extensions(&mut input)?; + } + entries.push(entry); } @@ -128,7 +130,6 @@ impl Pbo { Ok(Pbo { input, - header, header_len, extensions, entries, @@ -139,16 +140,12 @@ impl Pbo { #[cfg(test)] mod tests { use super::*; + use std::io::Cursor; #[test] - fn magic_check() { - dbg!(Pbo::read(&mut std::io::BufReader::new( - std::fs::File::open( - "/home/vitorhnn/arma_crap/mods/@ACE/addons/ace_advanced_ballistics.pbo", - ) - .unwrap(), - )) - .unwrap()); - //Pbo::read(&mut std::io::BufReader::new(std::fs::File::open("/home/vitorhnn/arma_crap/mods/@ACE/addons/ace_advanced_ballistics.pbo.ace_3.13.6.60-8bd4922f.bisign").unwrap())).unwrap(); + fn basic_pbo_test() { + let bytes = include_bytes!("../test_files/@ace/addons/ace_advanced_ballistics.pbo"); + let pbo = Pbo::read(Cursor::new(&bytes)).unwrap(); + assert_eq!(pbo.entries.len(), 49); } } diff --git a/src/repository.rs b/src/repository.rs index 4bd28bc..1a4d76c 100644 --- a/src/repository.rs +++ b/src/repository.rs @@ -1,11 +1,17 @@ +use crate::md5_digest::Md5Digest; use serde::{Deserialize, Deserializer, Serialize}; use snafu::prelude::*; use std::{fmt::Display, net::IpAddr, str::FromStr}; #[derive(Debug, Snafu)] -pub enum Error<'a> { +pub enum Error { #[snafu(display("Error while requesting repository data: {}", source))] - Http { url: &'a str, source: ureq::Error }, + Http { + url: String, + + #[snafu(source(from(ureq::Error, Box::new)))] + source: Box, + }, #[snafu(display("Error while deserializing: {}", source))] Deserialization { source: std::io::Error }, } @@ -34,7 +40,7 @@ where pub struct Mod { pub mod_name: String, #[serde(rename = "checkSum")] // why - pub checksum: String, + pub checksum: Md5Digest, pub enabled: bool, } @@ -69,23 +75,11 @@ pub struct Repository { pub servers: Vec, } -pub fn replicate_remote_repo_info(remote: &Repository) -> Repository { - Repository { - required_mods: vec![], - optional_mods: vec![], - checksum: "INVALID".into(), - ..remote.clone() - } -} - -pub fn get_repository_info<'a>( - agent: &'a mut ureq::Agent, - url: &'a str, -) -> Result> { +pub fn get_repository_info(agent: &mut ureq::Agent, url: &str) -> Result { agent .get(url) .call() .context(HttpSnafu { url })? .into_json() - .context(DeserializationSnafu {}) + .context(DeserializationSnafu) } diff --git a/src/srf.rs b/src/srf.rs index ceb2dcb..8477742 100644 --- a/src/srf.rs +++ b/src/srf.rs @@ -1,12 +1,17 @@ +use crate::md5_digest::Md5Digest; use md5::{Digest, Md5}; +use rayon::prelude::*; use relative_path::RelativePathBuf; use serde::{Deserialize, Deserializer, Serialize}; -use snafu::{OptionExt, ResultExt, Whatever}; +use snafu::{OptionExt, ResultExt, Snafu}; +use std::ffi::OsStr; use std::io::{BufReader, Seek, SeekFrom}; use std::{ + io, io::{BufRead, Read}, path::Path, }; +use walkdir::WalkDir; #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "PascalCase")] @@ -25,12 +30,28 @@ pub enum FileType { Pbo, } +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("io error: {}", source))] + Io { source: io::Error }, + #[snafu(display("pbo error: {}", source))] + Pbo { source: crate::pbo::Error }, + #[snafu(display("legacy srf parse failure: {}", description))] + LegacySrfParseFailure { description: &'static str }, + #[snafu(display("legacy srf failed to parse size as u32: {}", source))] + LegacySrfU32ParseFailure { source: std::num::ParseIntError }, + #[snafu(display("failed to decode md5 digest: {}", source))] + DigestParse { source: crate::md5_digest::Error }, +} + impl FileType { - fn from_legacy_srf(legacy_type: &str) -> Self { + fn from_legacy_srf(legacy_type: &str) -> Result { match legacy_type { - "PBO" => Self::Pbo, - "FILE" => Self::File, - _ => panic!("unknown legacy file type"), + "PBO" => Ok(Self::Pbo), + "FILE" => Ok(Self::File), + _ => Err(Error::LegacySrfParseFailure { + description: "unknown legacy file type", + }), } } } @@ -59,42 +80,40 @@ pub struct File { #[serde(rename_all = "PascalCase")] pub struct Mod { pub name: String, - pub checksum: String, + pub checksum: Md5Digest, pub files: Vec, } impl Mod { pub fn generate_invalid(remote: &Self) -> Self { Self { - checksum: "INVALID".into(), + checksum: Md5Digest::default(), files: vec![], ..remote.clone() } } } -fn generate_hash(file: &mut BufReader, len: u64) -> Result { +fn generate_hash(file: &mut BufReader, len: u64) -> Result { let mut hasher = Md5::new(); let mut stream = file.take(len); - std::io::copy(&mut stream, &mut hasher).with_whatever_context(|_| "hashing failure")?; + std::io::copy(&mut stream, &mut hasher).context(IoSnafu {})?; let hash = hasher.finalize(); - Ok(format!("{:X}", hash)) + Ok(format!("{hash:X}")) } -pub fn scan_pbo(path: &Path, base_path: &Path) -> Result { - let mut file = BufReader::new( - std::fs::File::open(&path).with_whatever_context(|_| "failed to open file")?, - ); +pub fn scan_pbo(path: &Path, base_path: &Path) -> Result { + let mut file = BufReader::new(std::fs::File::open(path).context(IoSnafu)?); let mut parts = Vec::new(); - let pbo = crate::pbo::Pbo::read(&mut file)?; + let pbo = crate::pbo::Pbo::read(&mut file).context(PboSnafu)?; let mut offset = 0; - let length = pbo.input.seek(SeekFrom::End(0)).unwrap(); - pbo.input.seek(SeekFrom::Start(0)).unwrap(); + let length = pbo.input.seek(SeekFrom::End(0)).context(IoSnafu)?; + pbo.input.seek(SeekFrom::Start(0)).context(IoSnafu)?; { let header_hash = generate_hash(pbo.input, pbo.header_len)?; @@ -109,17 +128,17 @@ pub fn scan_pbo(path: &Path, base_path: &Path) -> Result { } // swifty, as always, does very strange things - for entry in &pbo.entries { - let hash = generate_hash(pbo.input, entry.data_size as u64)?; + for entry in pbo.entries.iter().skip(1) { + let hash = generate_hash(pbo.input, u64::from(entry.data_size))?; parts.push(Part { path: entry.filename.clone(), - length: entry.data_size as u64, + length: u64::from(entry.data_size), checksum: hash, start: offset, }); - offset += entry.data_size as u64; + offset += u64::from(entry.data_size); } { @@ -156,16 +175,13 @@ pub fn scan_pbo(path: &Path, base_path: &Path) -> Result { }) } -pub fn scan_file(path: &Path, base_path: &Path) -> Result { - let file = std::fs::File::open(&path).with_whatever_context(|_| "failed to open file")?; +pub fn scan_file(path: &Path, base_path: &Path) -> Result { + let file = std::fs::File::open(path).context(IoSnafu)?; let mut parts = Vec::new(); - let file_len = file - .metadata() - .with_whatever_context(|_| "failed to acquire file metadata")? - .len(); + let file_len = file.metadata().context(IoSnafu)?.len(); - let mut reader = std::io::BufReader::new(file); + let mut reader = BufReader::new(file); let mut pos = 0; while pos < file_len { @@ -173,14 +189,13 @@ pub fn scan_file(path: &Path, base_path: &Path) -> Result { let mut stream = reader.by_ref().take(5000000); let pre_copy_pos = pos; - let copied = std::io::copy(&mut stream, &mut hasher) - .with_whatever_context(|_| "failed to io copy into hasher")?; + let copied = std::io::copy(&mut stream, &mut hasher).context(IoSnafu {})?; pos += copied; let hash = hasher.finalize(); parts.push(Part { - checksum: format!("{:X}", hash), + checksum: format!("{hash:X}"), length: copied, path: format!( "{}_{}", @@ -192,7 +207,7 @@ pub fn scan_file(path: &Path, base_path: &Path) -> Result { pos ), start: pre_copy_pos, - }) + }); } // final checksum generation @@ -200,7 +215,7 @@ pub fn scan_file(path: &Path, base_path: &Path) -> Result { let mut hasher = Md5::new(); for part in &parts { - hasher.update(&part.checksum) + hasher.update(&part.checksum); } let path = RelativePathBuf::from_path(path.strip_prefix(base_path).unwrap()).unwrap(); @@ -214,47 +229,47 @@ pub fn scan_file(path: &Path, base_path: &Path) -> Result { }) } -fn recurse(path: &Path, base_path: &Path) -> Result, Whatever> { +fn recurse(path: &Path, base_path: &Path) -> Result, Error> { println!("recursing into {:#?}", &path); - let entries = path - .read_dir() - .with_whatever_context(|_| "failed to read directory entries")?; - - let mut files = Vec::new(); - - for entry in entries { - let entry = entry.with_whatever_context(|_| "failed to read directory entry")?; - let metadata = entry - .metadata() - .with_whatever_context(|_| "failed to read direntry metadata")?; - let path = entry.path(); - - if metadata.is_dir() { - files.append(&mut recurse(&path, base_path)?); - continue; - } - - let extension = path.extension(); - - match extension { - Some(extension) if extension == "pbo" => files.push(scan_pbo(&path, base_path)?), - _ => files.push(scan_file(&path, base_path)?), - } - } + let entries: Vec<_> = WalkDir::new(path) + .into_iter() + .filter_entry(|e| e.file_name() != OsStr::new("mod.srf")) + .filter_map(Result::ok) + .filter(|e| { + // someday this spaghetti can just be replaced by Option::contains + if let Some(is_dir) = e.metadata().ok().map(|metadata| metadata.is_dir()) { + !is_dir + } else { + false + } + }) + .map(|entry| entry.path().to_owned()) + .collect(); + + let files: Result, _> = entries + .par_iter() + .map(|path| { + let extension = path.extension(); + + match extension { + Some(extension) if extension == "pbo" => scan_pbo(path, base_path), + _ => scan_file(path, base_path), + } + }) + .collect(); - Ok(files) + files } -// FIXME: ditch whatever errors -pub fn scan_mod(path: &Path) -> Result { +pub fn scan_mod(path: &Path) -> Result { let mut files = recurse(path, path)?; files.sort_by(|a, b| { a.path - .to_string() - .to_lowercase() - .cmp(&b.path.to_string().to_lowercase()) + .as_str() + .to_uppercase() + .cmp(&b.path.as_str().to_uppercase()) }); let checksum = { @@ -262,10 +277,12 @@ pub fn scan_mod(path: &Path) -> Result { for file in &files { hasher.update(&file.checksum); - hasher.update(file.path.to_string().to_lowercase().replace("\\", "/")); + let relpath = file.path.as_str().to_lowercase().replace('\\', "/"); + hasher.update(relpath); } - format!("{:X}", hasher.finalize()) + let output = hasher.finalize(); + Md5Digest::from_bytes(output.into()) }; Ok(Mod { @@ -281,33 +298,42 @@ pub fn scan_mod(path: &Path) -> Result { }) } -fn read_legacy_srf_addon(line: &str) -> Result<(Mod, u32), Whatever> { +fn read_legacy_srf_addon(line: &str) -> Result<(Mod, u32), Error> { let mut split = line.split(':'); let r#type = split .next() - .with_whatever_context(|| "no first element?")? + .context(LegacySrfParseFailureSnafu { + description: "addon line missing type", + })? .to_string(); - if r#type != "ADDON" { - panic!("wrong magic"); - } + assert_eq!(r#type, "ADDON", "wrong magic"); let name = split .next() - .with_whatever_context(|| "no second element?")? + .context(LegacySrfParseFailureSnafu { + description: "addon line missing name", + })? .to_string(); let size = split .next() - .with_whatever_context(|| "no third element?")? + .context(LegacySrfParseFailureSnafu { + description: "addon line missing size", + })? .parse() - .with_whatever_context(|_| "failed to parse size")?; - let checksum = split + .context(LegacySrfU32ParseFailureSnafu)?; + + let checksum_digest = split .next() - .with_whatever_context(|| "no fourth element?")? + .context(LegacySrfParseFailureSnafu { + description: "addon line missing checksum", + })? .to_string(); + let checksum = Md5Digest::new(&checksum_digest).context(DigestParseSnafu)?; + Ok(( Mod { name, @@ -318,35 +344,43 @@ fn read_legacy_srf_addon(line: &str) -> Result<(Mod, u32), Whatever> { )) } -fn read_legacy_srf_part(line: &str) -> Result { +fn read_legacy_srf_part(line: &str) -> Result { let mut split = line.split(':'); let path = split .next() - .with_whatever_context(|| "no first element")? + .context(LegacySrfParseFailureSnafu { + description: "part line missing path", + })? .to_string(); let start: u64 = split .next() - .with_whatever_context(|| "no second element")? + .context(LegacySrfParseFailureSnafu { + description: "part line missing start", + })? .parse() - .with_whatever_context(|_| "start was not a u64")?; + .context(LegacySrfU32ParseFailureSnafu)?; let length: u64 = split .next() - .with_whatever_context(|| "no third element")? + .context(LegacySrfParseFailureSnafu { + description: "part line missing length", + })? .parse() - .with_whatever_context(|_| "start was not a u64")?; + .context(LegacySrfU32ParseFailureSnafu)?; let checksum = split .next() - .with_whatever_context(|| "no fourth element")? + .context(LegacySrfParseFailureSnafu { + description: "part line missing checksum", + })? .to_string(); Ok(Part { path, - start, length, + start, checksum, }) } @@ -354,67 +388,91 @@ fn read_legacy_srf_part(line: &str) -> Result { fn read_legacy_srf_file( line: &str, lines: &mut impl Iterator, -) -> Result { +) -> Result { let mut split = line.split(':'); - let r#type = - FileType::from_legacy_srf(split.next().with_whatever_context(|| "no first element")?); + let r#type = FileType::from_legacy_srf(split.next().context(LegacySrfParseFailureSnafu { + description: "no first element", + })?)?; let path = RelativePathBuf::from( split .next() - .with_whatever_context(|| "no second element")? + .context(LegacySrfParseFailureSnafu { + description: "file line missing path", + })? .to_string(), ); let length: u64 = split .next() - .with_whatever_context(|| "no third element")? + .context(LegacySrfParseFailureSnafu { + description: "file line missing length", + })? .parse() - .with_whatever_context(|_| "length was not a u64")?; + .context(LegacySrfU32ParseFailureSnafu)?; let part_count: u32 = split .next() - .with_whatever_context(|| "no fourth element")? + .context(LegacySrfParseFailureSnafu { + description: "file line missing part count", + })? .parse() - .with_whatever_context(|_| "file_count was not a u32")?; + .context(LegacySrfU32ParseFailureSnafu)?; let checksum = split .next() - .with_whatever_context(|| "no fifth element")? + .context(LegacySrfParseFailureSnafu { + description: "file line missing checksum", + })? .to_string(); let mut parts = Vec::new(); for _ in 0..part_count { - let line = lines.next().with_whatever_context(|| "missing line")?; + let line = lines.next().context(LegacySrfParseFailureSnafu { + description: "part line missing", + })?; parts.push(read_legacy_srf_part(&line)?); } Ok(File { - r#type, path, length, checksum, + r#type, parts, }) } -pub fn deserialize_legacy_srf(input: &mut I) -> Result { +pub fn is_legacy_srf(input: &mut I) -> Result { + let start = input.stream_position()?; + let mut buf = [0; 5]; + input.read_exact(&mut buf)?; + input.seek(SeekFrom::Start(start))?; + + Ok(String::from_utf8_lossy(&buf) == "ADDON") +} + +pub fn deserialize_legacy_srf(input: &mut I) -> Result { // swifty's legacy srf format is stateful - input.seek(SeekFrom::Start(0)).with_whatever_context(|_| "failed to rewind file")?; + input.seek(SeekFrom::Start(0)).context(IoSnafu)?; let mut files = Vec::::new(); let mut iter = input.lines().map(|line| line.expect("input.lines failed")); - let first_line = iter.next().with_whatever_context(|| "no first line")?; + let first_line = iter.next().context(LegacySrfParseFailureSnafu { + description: "no first line", + })?; let (addon, file_count) = read_legacy_srf_addon(&first_line)?; for _ in 0..file_count { let file = read_legacy_srf_file( - &iter.next().with_whatever_context(|| "missing lines")?, + &iter.next().context(LegacySrfParseFailureSnafu { + description: "line missing", + })?, &mut iter, )?; @@ -428,12 +486,34 @@ pub fn deserialize_legacy_srf(input: &mut I) -> Result(), + ) + .unwrap(); + + assert_eq!( + r#mod.checksum, + Md5Digest::new("787662722D70C36DF28CD1D5EE8D8E86").unwrap() + ); } } diff --git a/test_files/@ace/addons/ace_advanced_ballistics.pbo b/test_files/@ace/addons/ace_advanced_ballistics.pbo new file mode 100644 index 0000000..759ccbc Binary files /dev/null and b/test_files/@ace/addons/ace_advanced_ballistics.pbo differ diff --git a/test_files/@ace/addons/ace_advancedthis_will_break_tests_if_we_order_incorrectly.txt b/test_files/@ace/addons/ace_advancedthis_will_break_tests_if_we_order_incorrectly.txt new file mode 100644 index 0000000..54e2b78 --- /dev/null +++ b/test_files/@ace/addons/ace_advancedthis_will_break_tests_if_we_order_incorrectly.txt @@ -0,0 +1 @@ +this is a dummy file diff --git a/test_files/legacy_format_mod.srf b/test_files/legacy_format_mod.srf new file mode 100644 index 0000000..479f993 --- /dev/null +++ b/test_files/legacy_format_mod.srf @@ -0,0 +1,404 @@ +ADDON:@lambs_danger:19:44C1B8021822F80E1E560689D2AAB0BF +FILE:addons\lambs_formations.pbo.lambs_danger_2.5.3-6bb8150d.bisign:580:1:737EA58E2EE46B8239598668575EAFB0 +lambs_formations.pbo.lambs_danger_2.5.3-6bb8150d.bisign_580:0:580:71E2B570D4B0316E7858050739578DB7 +PBO:addons\lambs_formations.pbo:2819:6:220C39158BE1C18AB20687E0E03B1D58 +$$HEADER$$:0:216:BE7418C36416DCD00F882E27348FC1CB +CfgFSMs.hpp:216:987:CFDE4D162DB701BC4DCD5FC79A7BFD38 +CfgVehicles.hpp:1203:440:C99C6EE330AF584F99B94F7C859932B4 +config.bin:1643:747:D310780B5E1CC02CF2229553FC816ED2 +script_component.hpp:2390:408:6A5498A0B99942090A4A50B5D7F37ABA +$$END$$:2798:21:1FC9E69678E63C547ECDFC3BD67A8398 +FILE:addons\lambs_eventhandlers.pbo.lambs_danger_2.5.3-6bb8150d.bisign:580:1:F48D237CC6B2C99E0842ED2D5F1D8AB0 +lambs_eventhandlers.pbo.lambs_danger_2.5.3-6bb8150d.bisign_580:0:580:ECB2C30761439838F33DB3B4ADA42FA0 +FILE:readme.txt:957:1:02FEF8C4604C7189377ABFF90C1B47E4 +readme.txt_957:0:957:EA3A793730701C4F27FE39DCB88F4706 +FILE:readme.md:2387:1:2DF21248D5DB1DE3654C57E993A38FD6 +readme.md_2387:0:2387:658850005DA11A7043565BB6B0B1661B +FILE:mod.cpp:479:1:9DF16337588972D424E5DC119972BDAA +mod.cpp_479:0:479:46B36C9107AB26E0EAACCA84FB0A423A +FILE:meta.cpp:104:1:FF97B8075E3B8C773D44096EAF9A07C2 +meta.cpp_104:0:104:F0F08A7D2045FA96A496C2D1B0AAC2CD +FILE:license:19534:1:7B2591755967A7F41FDB0C542CFAE9FF +license_19534:0:19534:511B28AFA1D29E84D2E89C059ADD0D8B +FILE:lambs_logo.paa:31929:1:9C27128E68D2FE2D5EFB21A950EC52BF +lambs_logo.paa_31929:0:31929:D1B1E9C7753605181DB216C03A999528 +FILE:keys\lambs_danger_2.5.3.bikey:180:1:5198A9AC6BC619F64D55551AA73A1A2F +lambs_danger_2.5.3.bikey_180:0:180:BA9044BDBF484C52F9B662B2F961D7A0 +PBO:addons\lambs_eventhandlers.pbo:12198:15:91269D931987E7A9B853CB75A8EFBF21 +$$HEADER$$:0:594:76CE2F41B30974AAFB086FB9C3F733CB +CfgEventHandlers.hpp:594:599:237618E58F277CEBEF86825829F13870 +config.bin:1193:702:B31828C7496258D7E413BD4F9018DB8C +functions\fnc_explosionEH.sqf:1895:2111:F42042EDA0629CBA3E2080702EEFEB3A +functions\fnc_explosionEH.sqfc:4006:2701:30D8ECCB02C90041C0970B1A9B25E088 +functions\script_component.hpp:6707:63:F79D7AE49F671841B156C25B72AF15AF +script_component.hpp:6770:430:3C4ADD25AEC8ADB48962EFB9B769FA73 +settings.sqf:7200:648:303108E1A095639FF86661A795CF9B4D +stringtable.xml:7848:2340:C53769754347A5901C2831167EF60305 +XEH_preInit.sqf:10188:120:721B158055283E540726854A39D547FA +XEH_preInit.sqfc:10308:1232:F005399A8A9A7EE833856818ECB1F832 +XEH_PREP.hpp:11540:20:7F11106C12EA7F81AF0510904479C370 +XEH_preStart.sqf:11560:58:5B7DDBAB47A5596BA09F89A83B6AD057 +XEH_preStart.sqfc:11618:559:0425F41C63171062DF8D1170E6FE1921 +$$END$$:12177:21:3E859B4848D6C40F0D2A2BD1AF38F8FA +FILE:addons\lambs_danger.pbo.lambs_danger_2.5.3-6bb8150d.bisign:580:1:5CF0BD7560284DA7F384929E14EE01B5 +lambs_danger.pbo.lambs_danger_2.5.3-6bb8150d.bisign_580:0:580:82A1F0DC78AD4F7E27DFD8B8A6EB4680 +PBO:addons\lambs_danger.pbo:330183:96:A1D0F4ECC96FD6CF0FBB7DC32A047E63 +$$HEADER$$:0:4877:9688762986C766A9A8B68936CA54EF50 +Cfg3DEN.hpp:4877:2845:FB02A50320CC954B8685E961DD49B729 +CfgEventHandlers.hpp:7722:457:19B8184236818F7BF8FC4E94ECFF8FD5 +CfgVehicles.hpp:8179:2597:73D0CB005FCF18CB1D14B2FF2E9476EC +config.bin:10776:5919:2FBF3787DF34068438FA736DDF1905D4 +functions\fnc_brain.sqf:16695:3771:3A9E17C16BB38A0EAD5850F00834BEDA +functions\fnc_brain.sqfc:20466:3372:EFC448DD590FBDFFBA053F9F93B290B3 +functions\fnc_brainAdjust.sqf:23838:412:946A22E915FEFD2954657487DD242A65 +functions\fnc_brainAdjust.sqfc:24250:560:D592D1B4163BD520C462528A0ED823A6 +functions\fnc_brainAssess.sqf:24810:1751:AB3F120DC27A4819B48B7A4D1BB00BC0 +functions\fnc_brainAssess.sqfc:26561:2247:51286F997D93C314008A96E8D980B5E4 +functions\fnc_brainEngage.sqf:28808:1710:70042BA127974CC0B785972C71745F33 +functions\fnc_brainEngage.sqfc:30518:2370:77B6E2B603232F55D0B87FA1A626D46A +functions\fnc_brainForced.sqf:32888:1466:5B7E71848A10E7CF666AC655102AA832 +functions\fnc_brainForced.sqfc:34354:2014:5B4D4A6E156E1EF442E61607DF554A83 +functions\fnc_brainHide.sqf:36368:2558:3072D954DF214FCC5E07840D0C805709 +functions\fnc_brainHide.sqfc:38926:2428:19C4C7C0158E7E4E6CE74354BBCB97AF +functions\fnc_brainReact.sqf:41354:1080:91E9704802337E172C9FCB6360102562 +functions\fnc_brainReact.sqfc:42434:1438:8CACC28FBE61D874A891F9D307C77900 +functions\fnc_brainVehicle.sqf:43872:6748:2CE95C45C83B343A1BB8615E297A75A2 +functions\fnc_brainVehicle.sqfc:50620:6619:87D48E29D216CDC85BB264A20099DC1E +functions\fnc_fsmAllowAnimation.sqf:57239:608:D90B2547C2EE441D591058AA8C5EFAA0 +functions\fnc_fsmAllowAnimation.sqfc:57847:1137:A845CF4E75CA65DDE01ABA228EEE91F3 +functions\fnc_isForced.sqf:58984:569:E53B7D5C5B4A58E659E79A93616C0BC7 +functions\fnc_isForced.sqfc:59553:1103:FECC511F584721A497E53BB138E6F691 +functions\fnc_isForcedExit.sqf:60656:412:8506F5CCCF703712B5C003F09476BF4D +functions\fnc_isForcedExit.sqfc:61068:798:3A154A5486E7175243A0B040DE7B8947 +functions\fnc_isLeader.sqf:61866:498:50779E28210548D6EE6A85E522F1F315 +functions\fnc_isLeader.sqfc:62364:983:BC523F2526B9B2D405D2CFF28F51FFA6 +functions\fnc_tactics.sqf:63347:1135:E985F8A752889ADA56144A0507629EA9 +functions\fnc_tactics.sqfc:64482:1160:4FD2693D83CD899E5D04961DB9C8203C +functions\fnc_tacticsAssault.sqf:65642:3756:DD75B27F76BD51592ABA2899002F6AFA +functions\fnc_tacticsAssault.sqfc:69398:4196:2F4FEAE88F5AC940BF5ADD4AF6B1B24A +functions\fnc_tacticsAssess.sqf:73594:8686:66A08ADDDBDA703B490D30B016E437F5 +functions\fnc_tacticsAssess.sqfc:82280:8151:6E8F51E0E3B95EE3732F20BD4F1CBC91 +functions\fnc_tacticsAttack.sqf:90431:2769:17F8C4AB6AFB2A852DD1A52F2133F246 +functions\fnc_tacticsAttack.sqfc:93200:3199:0828F1F397E3B29CFABCBE98A48C0B34 +functions\fnc_tacticsContact.sqf:96399:5004:471B285FEED25F5E5420420380AC5B90 +functions\fnc_tacticsContact.sqfc:101403:5003:BE65C94637F46F65F050D8B373691D1F +functions\fnc_tacticsCQB.sqf:106406:2428:5EE6C94F964216C5FF52E4DACB6CCC4C +functions\fnc_tacticsCQB.sqfc:108834:2758:45FBB4262079D9929DDDB73A33C5E02F +functions\fnc_tacticsFlank.sqf:111592:4658:D77851D1E238BE7CA42E354C40610BA0 +functions\fnc_tacticsFlank.sqfc:116250:5000:639EA5390C7F05384B524FC40C2EAA1C +functions\fnc_tacticsGarrison.sqf:121250:3752:DBF0ACB97C9D18CE9A56E9C05D09A80E +functions\fnc_tacticsGarrison.sqfc:125002:3949:89D3B0CE538FA93B00C45B0F943C14C7 +functions\fnc_tacticsHide.sqf:128951:4442:CFCA1A653A5B2936B9366933DA377052 +functions\fnc_tacticsHide.sqfc:133393:4813:E71C653767E774692641BAD3A9843764 +functions\fnc_tacticsHold.sqf:138206:3071:3105D7CAEFB156AD4920E4C8E5784188 +functions\fnc_tacticsHold.sqfc:141277:3532:EF81CD606C026AE3150EEDA945364A66 +functions\fnc_tacticsProfiles.sqf:144809:484:D302541CA34A064AD8914FD52D038576 +functions\fnc_tacticsProfiles.sqfc:145293:422:28DA7C8425CCF98D89E3250B7818FA8C +functions\fnc_tacticsReinforce.sqf:145715:4217:4AB16F23B62182522F9568E5DA422448 +functions\fnc_tacticsReinforce.sqfc:149932:4508:2BE299CD74BF6E52D90E813A6C554BBF +functions\fnc_tacticsSuppress.sqf:154440:3268:3C21D73CD268B5836726A010F4DA0E8B +functions\fnc_tacticsSuppress.sqfc:157708:3739:A3E232627316D2849877860A7B0410D4 +functions\script_component.hpp:161447:357:4160B1B37A93ECD21FF00001262FCCD3 +functions\ZEN\fnc_setDisableAI.sqf:161804:181:ECE155DD9CD00877D7955B6FD531EEB9 +functions\ZEN\fnc_setDisableAI.sqfc:161985:749:D40B1D1C0DF93E1819077A8F5AC7AA0B +functions\ZEN\fnc_setDisableGroupAI.sqf:162734:184:F9A74D127BA6B527B873D686AF3038CC +functions\ZEN\fnc_setDisableGroupAI.sqfc:162918:760:3F50E40B52D2304CBD26057EFCB38A25 +functions\ZEN\fnc_setHasRadio.sqf:163678:183:65D5D8980AAAE46372B9E0BF666DE73D +functions\ZEN\fnc_setHasRadio.sqfc:163861:749:C2889AC1BFBE4CD8983A536475716E30 +functions\ZEN\fnc_setReinforcement.sqf:164610:192:946065547BAAB7F96E470FB40BE2BF9A +functions\ZEN\fnc_setReinforcement.sqfc:164802:766:B18542DA7A8CE721CC9DEA807F23838E +functions\ZEN\fnc_showHasRadio.sqf:165568:194:19C74EE06EE6CEEA28239424E1DB0D65 +functions\ZEN\fnc_showHasRadio.sqfc:165762:796:2A1774F606B7EE00740A67257E3623DF +functions\ZEN\fnc_showReinforcement.sqf:166558:203:B6C5B264295CC59B35F7F6A9C634E832 +functions\ZEN\fnc_showReinforcement.sqfc:166761:817:8D31B801CB81F5C16BD1152AA2219748 +functions\ZEN\fnc_showSetDisableAI.sqf:167578:192:8988FA12B081A0717FD3C42DECD47B54 +functions\ZEN\fnc_showSetDisableAI.sqfc:167770:805:D273D30520954D4EF4C77F9F821DC764 +functions\ZEN\fnc_showSetDisableGroupAI.sqf:168575:195:ACF4AE12A2F8F739784B55962CDB60CD +functions\ZEN\fnc_showSetDisableGroupAI.sqfc:168770:809:A81D2B883778414AAFF0509CFA82D9EF +functions\ZEN\script_component.hpp:169579:56:D866B7162B3E59053882EDD1982858AC +functions\ZeusModules\fnc_moduleConfigureGroupAI.sqf:169635:1641:D557389BEAECF8A60F6A1B82C196D134 +functions\ZeusModules\fnc_moduleConfigureGroupAI.sqfc:171276:2319:D7663AF54A879B456E5E7A2396B3C973 +functions\ZeusModules\fnc_moduleDisableAI.sqf:173595:1367:58ED437E775F2F92E5A81549CAE19A02 +functions\ZeusModules\fnc_moduleDisableAI.sqfc:174962:2219:7A508C69451460004EE7B642483272D5 +functions\ZeusModules\fnc_moduleSetRadio.sqf:177181:1366:99935E475D6C3011E0372C77F3FD57C5 +functions\ZeusModules\fnc_moduleSetRadio.sqfc:178547:2222:6F905A86662D3DC839D42C08853BD06E +functions\ZeusModules\script_component.hpp:180769:56:D866B7162B3E59053882EDD1982858AC +script_component.hpp:180825:388:4C1299C1F16A25975D34BAFEDC88BDDA +scripts\lambs_danger.fsm:181213:36867:8F0A6A6A6596E013CBD20AA3CEC580A4 +scripts\lambs_dangerCivilian.fsm:218080:55323:440D26AE2FCCF3327127196E59DCE2F7 +settings.sqf:273403:4277:79225537134DDFCE65051672CC34EB11 +stringtable.xml:277680:26386:B14E244B044120D132E6E42D7C5C6168 +XEH_postInit.sqf:304066:980:6F419C8BBD9C45E8565F0F7A41AF2CA8 +XEH_postInit.sqfc:305046:1085:3E9EB95DF0CA796CC232C8B629C1EE7E +XEH_preInit.sqf:306131:2029:4B6DD4D5C5D4EB7AFD3014F1B08BF879 +XEH_preInit.sqfc:308160:6000:D47F47CDCD8511F2C198C82AACEE012F +XEH_preInitClient.sqf:314160:5674:BC0455DA96D03E975B791DE8FAA9C17E +XEH_preInitClient.sqfc:319834:4951:C2A7436EAADC1D49E810F243D18A3FD0 +XEH_PREP.hpp:324785:895:268350334D76F121F5569F3D7590BAFC +XEH_preStart.sqf:325680:58:5B7DDBAB47A5596BA09F89A83B6AD057 +XEH_preStart.sqfc:325738:2392:0D909AEE25FF887EBFD0C10EA20C03F6 +ZEN_CfgContext.hpp:328130:2032:365581A19621888A7BFDDBFCF19EEC85 +$$END$$:330162:21:93378E0B3224E6BBC32D02CE6BA853D2 +FILE:addons\lambs_wp.pbo.lambs_danger_2.5.3-6bb8150d.bisign:580:1:4F93826E1262C4A7999C53844D1AD750 +lambs_wp.pbo.lambs_danger_2.5.3-6bb8150d.bisign_580:0:580:69CF0E69A89F9182D919A18918311F4D +PBO:addons\lambs_wp.pbo:426889:113:F132945DBD24A70ECC362CFEEF75D495 +$$HEADER$$:0:5619:0224664498A2303EC3B78734AE50CE51 +Cfg3DEN.hpp:5619:1978:CADCB90987799698F9B60BCAC95CB985 +CfgEventHandlers.hpp:7597:388:B509493BA0169AA3124D9197146A51BD +CfgVehicles.hpp:7985:1407:F5B3530F6CA3F454A34EDBC3255ED28B +CfgWaypoints.hpp:9392:3347:2AEBE2863527EAC45221A83C5AB97C6E +config.bin:12739:31992:35925542E897A91482CA7353B3C6D8E0 +functions\fnc_doArtillery.sqf:44731:6077:E478AF32801F0E64E5AAA3CEA75BD387 +functions\fnc_doArtillery.sqfc:50808:5461:1F4C2EFEE243149244F27D1BB9E395B1 +functions\fnc_doAssaultUnitReset.sqf:56269:1127:9E87C183C682BE2B8126614FDFD653D9 +functions\fnc_doAssaultUnitReset.sqfc:57396:1628:4B7B42E58BE6E915474274E1927369A4 +functions\fnc_sideHasArtillery.sqf:59024:769:F66C971929BB4BEFB95A6AC5B172683B +functions\fnc_sideHasArtillery.sqfc:59793:1263:0AA2B666D52562917DBB97E1896460FD +functions\fnc_taskArtillery.sqf:61056:783:2E720389D841D102676D615CBF3A556A +functions\fnc_taskArtillery.sqfc:61839:810:208FBD0B1FE7642C82FAAEFF84930389 +functions\fnc_taskArtilleryRegister.sqf:62649:1186:1FE9C681E10EEA097BD051D9C3D42DDB +functions\fnc_taskArtilleryRegister.sqfc:63835:1772:78E519DA5CA98B0DD771EF835E6B0EDC +functions\fnc_taskAssault.sqf:65607:6470:660BC148C72385F9810B56E8698F68BA +functions\fnc_taskAssault.sqfc:72077:5914:40ACA6DE7F5FA3C0BBD572FC34D8C6DA +functions\fnc_taskCamp.sqf:77991:8641:721EF05DA11DDE6E4069072E00D0A663 +functions\fnc_taskCamp.sqfc:86632:8592:41BFA0DC0F050EA66BB8C45D0C7DE29A +functions\fnc_taskCQB.sqf:95224:8063:922079F5C1D76154AB746A90D30D88BB +functions\fnc_taskCQB.sqfc:103287:7332:8A8691E7AA5F22B9DDEA5384F05DE5F4 +functions\fnc_taskCreep.sqf:110619:4292:4933B6BC2B0EBC928A795DD2BA555D4C +functions\fnc_taskCreep.sqfc:114911:4394:A1C4AC0894ABA8EF66053DD998EFE06A +functions\fnc_taskGarrison.sqf:119305:8304:107A8B1B993A7E22270B51D94EAD2C5F +functions\fnc_taskGarrison.sqfc:127609:8084:F0808EAB62D79DAB015BFBE939698536 +functions\fnc_taskHunt.sqf:135693:3724:63957CE21CAD1B977C5CDF77EADEF411 +functions\fnc_taskHunt.sqfc:139417:3738:AB8A45F11BB1D6F52AE1D9D09B022A41 +functions\fnc_taskPatrol.sqf:143155:4974:8C935FB5556CA516273A9552F76518AE +functions\fnc_taskPatrol.sqfc:148129:4872:15059228862F40B045E642D18E2E9869 +functions\fnc_taskReset.sqf:153001:2972:15B405F68055C1FE0AAE146B9A4140F2 +functions\fnc_taskReset.sqfc:155973:3135:7F6AC11954A1DDA51E321A19BD9B32FB +functions\fnc_taskRush.sqf:159108:3285:F8E9A12CFB3B7637AF311404A4361AD9 +functions\fnc_taskRush.sqfc:162393:3513:819F2D36255103F65377E828AE68FE74 +functions\Modules\fnc_moduleArtillery.sqf:165906:3311:9D6BFA7F54F9B668EB19AFDCAFC46277 +functions\Modules\fnc_moduleArtillery.sqfc:169217:3173:CD41FCF1719990511A3CAE572F589E75 +functions\Modules\fnc_moduleArtilleryRegister.sqf:172390:2546:F4A1C40C75C6567070C1B26ADCB71F41 +functions\Modules\fnc_moduleArtilleryRegister.sqfc:174936:3489:4B8F575F074B74743828723D5224696C +functions\Modules\fnc_moduleAssault.sqf:178425:6769:24F078A728D388AEBE405C760F0BB335 +functions\Modules\fnc_moduleAssault.sqfc:185194:5339:9D7306707BAB3213EE390E1B5BA512B7 +functions\Modules\fnc_moduleCamp.sqf:190533:5831:BAE7F7771CF5A658601147B78F1EC1C6 +functions\Modules\fnc_moduleCamp.sqfc:196364:4980:BED8106FBE956B973E4175FE239FE03A +functions\Modules\fnc_moduleCQB.sqf:201344:6041:AE3ED70C7C6826D1F6988065C039C4A4 +functions\Modules\fnc_moduleCQB.sqfc:207385:5345:EFB43DB0A77CBAED7AA521C968105D32 +functions\Modules\fnc_moduleCreep.sqf:212730:3688:0F7E2E604B2B210F608D2D63D23C42DF +functions\Modules\fnc_moduleCreep.sqfc:216418:3806:07CE6662771864CC73469F75F1CFE605 +functions\Modules\fnc_moduleGarrison.sqf:220224:6709:4D7AC8E118351EE82DB4BBBEE7370592 +functions\Modules\fnc_moduleGarrison.sqfc:226933:5244:9408EC1C0AD082003BF35F4E86C557C6 +functions\Modules\fnc_moduleHunt.sqf:232177:4489:360EA09A21C593DD8D2C9170FE874B16 +functions\Modules\fnc_moduleHunt.sqfc:236666:4266:2D039546E3A51915C7AB168906CBEEE3 +functions\Modules\fnc_modulePatrol.sqf:240932:6130:5D1F7821FB93ECCBB91B968775F7C390 +functions\Modules\fnc_modulePatrol.sqfc:247062:5045:3AC56D2079BA36863439B7B99F951E7E +functions\Modules\fnc_moduleReset.sqf:252107:1685:EEE43796E630DD9052E075646F1DB3F6 +functions\Modules\fnc_moduleReset.sqfc:253792:2544:1F41E2EA4AEE48EBB108DD1BD7733A27 +functions\Modules\fnc_moduleRush.sqf:256336:3707:E7B4A2E94B7D8E1D09C42BFD958DBE15 +functions\Modules\fnc_moduleRush.sqfc:260043:3838:71D79B528267E327AF052864E2E96AAB +functions\Modules\fnc_moduleTarget.sqf:263881:1306:A9F1AB6944FBEC9209B1AFC1575A1332 +functions\Modules\fnc_moduleTarget.sqfc:265187:2308:B9D69B4B72524C3EA410BD157AB2126A +functions\Modules\script_component.hpp:267495:52:BCFEB3A429EE725D21BBBF99534BEF4A +functions\script_component.hpp:267547:52:BCFEB3A429EE725D21BBBF99534BEF4A +functions\ZEN\fnc_setArtilleryRegister.sqf:267599:160:077ADFFA68FD4676CF19958C5EDE5DD2 +functions\ZEN\fnc_setArtilleryRegister.sqfc:267759:1149:85FFA1F58B5F725ECDEB7539E1BC6CE7 +functions\ZEN\fnc_setCamp.sqf:268908:230:3F7E4910BB232C6BB2D48B357C639B15 +functions\ZEN\fnc_setCamp.sqfc:269138:1212:78252EE564033B5013E0364EC5F23938 +functions\ZEN\fnc_setCQB.sqf:270350:222:B09C99DA3A20E79789336DDBB73A6511 +functions\ZEN\fnc_setCQB.sqfc:270572:1189:8FE1353BB5E1628FE215AE793A794F5B +functions\ZEN\fnc_setCreep.sqf:271761:183:F25FAAAB7000DB256A1CA242C8EF5614 +functions\ZEN\fnc_setCreep.sqfc:271944:1161:0828B42725A0DD5F21EF1984C3672F28 +functions\ZEN\fnc_setGarrison.sqf:273105:234:DFE08419BD900C84A02CC1B5BEA25619 +functions\ZEN\fnc_setGarrison.sqfc:273339:1219:071C5C64EEAFD85B1B3EBD99AAE3E0E0 +functions\ZEN\fnc_setHunt.sqf:274558:182:D0F7AB7D5F70EBD754409F1B26004757 +functions\ZEN\fnc_setHunt.sqfc:274740:1157:C73C8BFB688B6CF742B8F47E5E2DBE23 +functions\ZEN\fnc_setPatrol.sqf:275897:232:606694DDD29CFC2729C6B9AA23D0AFE0 +functions\ZEN\fnc_setPatrol.sqfc:276129:1214:B6DA6C9392DA8DAD7CEEA8E128A934F9 +functions\ZEN\fnc_setReset.sqf:277343:183:A8BED9C430FB1DCD357A72DCA3A48DEC +functions\ZEN\fnc_setReset.sqfc:277526:1160:3A73EE6262320534E4F8198640CB0C48 +functions\ZEN\fnc_setRush.sqf:278686:182:B54A75E680F1169AC235A7B6C85252A4 +functions\ZEN\fnc_setRush.sqfc:278868:1158:A552EE8CC1FECAD633A616FCA1B16F1A +functions\ZEN\fnc_setTarget.sqf:280026:1147:22B897EAA5A3DCFA6F34034AF4E43A1E +functions\ZEN\fnc_setTarget.sqfc:281173:2092:C05222A7FD408671C900DD91B038C98B +functions\ZEN\script_component.hpp:283265:385:508A3ACF17E36C1965D4805A32BB6A43 +modules.hpp:283650:23821:D30791E8FC728143064A42CF4F5ED79E +script_component.hpp:307471:1718:808193A6D0F40D20C6BA946543DB738E +scripts\fnc_wpAssault.sqf:309189:684:559EC322507056D98AA471967F583927 +scripts\fnc_wpAssault.sqfc:309873:955:D37B229ED96DBB53DA374788264C34F7 +scripts\fnc_wpCQB.sqf:310828:595:4079E74F884552642D954500F4F4A964 +scripts\fnc_wpCQB.sqfc:311423:930:03D3A96E70D04F0DA0C24FB4A10B02AA +scripts\fnc_wpCreep.sqf:312353:635:5A5D4DF463D820C6856CE1E283A6284B +scripts\fnc_wpCreep.sqfc:312988:996:ED9B713167BA202EB7E4BE7EADDB54BA +scripts\fnc_wpGarrison.sqf:313984:530:24FADA58C60C9F1695953A9226F695C3 +scripts\fnc_wpGarrison.sqfc:314514:889:04302F165ABD89BE3C91F3A47A8BEFA3 +scripts\fnc_wpHunt.sqf:315403:561:5E4A8607E5790024C2ED7BF166E876BB +scripts\fnc_wpHunt.sqfc:315964:902:F1191558D79CA47D2807A80BD4608EB6 +scripts\fnc_wpPatrol.sqf:316866:525:793B2A5A2F67A2885E376066103FEE47 +scripts\fnc_wpPatrol.sqfc:317391:878:DD3EAA144979A664379FC51B5BFE7AAB +scripts\fnc_wpRetreat.sqf:318269:681:79B8B455246BDFDA7EE8536C8763ECD7 +scripts\fnc_wpRetreat.sqfc:318950:1004:9E483A4CB4525931AFE48FC47BBBE30A +scripts\fnc_wpRush.sqf:319954:658:62195CCC01378C372D4D244975CB6FD1 +scripts\fnc_wpRush.sqfc:320612:1037:1E327B631B55E83A737666614E710AE0 +scripts\script_component.hpp:321649:52:BCFEB3A429EE725D21BBBF99534BEF4A +settings.sqf:321701:1029:CDC77462AF9B2EB0B7857E19AFBDFAC0 +stringtable.xml:322730:77881:166A637A88A924BA034EB255D0EE9F29 +XEH_postInit.sqf:400611:84:842CF923F9828E021028826AE0217070 +XEH_postInit.sqfc:400695:478:54884901134010D7A5F93C085D6FD840 +XEH_preInit.sqf:401173:3054:7564B853180AC5AC1B455E844F83D06C +XEH_preInit.sqfc:404227:6928:B81B65864D2918EF63656869442E28F0 +XEH_PREP.hpp:411155:935:071475B2B86A0FA5522AC2414B1E43B9 +XEH_preStart.sqf:412090:58:5B7DDBAB47A5596BA09F89A83B6AD057 +XEH_preStart.sqfc:412148:2291:368A7DCAD157F069C531637171C20A8E +ZEN_CfgContext.hpp:414439:2978:D0CC9FE087A8860720DF5D7840EC89C4 +ZEN_CfgWaypointTypes.hpp:417417:1779:00819D5EF726DC19E03D9F73F10E8AE4 +zeusModules.hpp:419196:7672:40FB62A2CEEA0B8D28C3B9650ED27CC7 +$$END$$:426868:21:7C6728859852B43D71F0419C93D2FF72 +FILE:addons\lambs_range.pbo.lambs_danger_2.5.3-6bb8150d.bisign:580:1:4C70F16F5657AC703564A9F35524FB23 +lambs_range.pbo.lambs_danger_2.5.3-6bb8150d.bisign_580:0:580:5E4C344EAEE65BA6E997FFD20C0BA230 +PBO:addons\lambs_range.pbo:990:5:1E5583825F8B718B804DF72CACF43564 +$$HEADER$$:0:179:DFD124320264A6CC217C3C36E559F045 +CfgVehicles.hpp:179:100:E37CC5586921F842B3CD200B8D47C43A +config.bin:279:302:3CFBE4CA898ED67CEB1C7E75C5CBF14A +script_component.hpp:581:388:2D8B206DB8749D5DB4FF95447C769739 +$$END$$:969:21:F28F4590461D05514A4C2BB797E5C7FC +FILE:addons\lambs_main.pbo.lambs_danger_2.5.3-6bb8150d.bisign:580:1:58D683758460D7190FFB9846E7D8E543 +lambs_main.pbo.lambs_danger_2.5.3-6bb8150d.bisign_580:0:580:E6B1E4CD9835898694C1D49D8E0CB781 +PBO:addons\lambs_main.pbo:368123:136:0A0EB200A2A220C33C6F240A58D65E10 +$$HEADER$$:0:7568:D422F222D087A6F7E8C990675DFDC6E7 +CfgEventHandlers.hpp:7568:388:B509493BA0169AA3124D9197146A51BD +CfgFactionClasses.hpp:7956:493:91641BBB00E31D60DEAAB154A490B08D +config.bin:8449:1102:CF30AF2E9611DAB64D067C5958BE781D +functions\debug\fnc_debugDangerType.sqf:9551:1025:BF095511B0F80EE3B6FFAE74069437BF +functions\debug\fnc_debugDangerType.sqfc:10576:1386:4213D0DC5A2DA264634EB877291DE303 +functions\debug\fnc_debugDraw.sqf:11962:9774:B397638148B5D1371483BF9073813693 +functions\debug\fnc_debugDraw.sqfc:21736:9258:FD451A235B6589E750692274DF921D76 +functions\debug\fnc_debugLog.sqf:30994:393:E7A437911EE4A7FE8B78E3FDF2486947 +functions\debug\fnc_debugLog.sqfc:31387:759:320BB76FF4DAFBDF0496055F38BD8F34 +functions\debug\fnc_debugMarkerColor.sqf:32146:600:13297C4EFA81A6A0F53757DFB500BAB1 +functions\debug\fnc_debugMarkerColor.sqfc:32746:915:1D9434CD070B1CB63F906902251ACFD6 +functions\debug\fnc_debugObjectColor.sqf:33661:723:E5486A83B7B111C35B4DEC72363166D0 +functions\debug\fnc_debugObjectColor.sqfc:34384:941:CCDFBCCF5F36C588F5D81DA2589B0540 +functions\debug\fnc_dotMarker.sqf:35325:1026:F495F2A491D4B55B470FFB4C7EB8F31C +functions\debug\fnc_dotMarker.sqfc:36351:1261:C46ACA0F4FE13903914571BEA8870C51 +functions\debug\fnc_zoneMarker.sqf:37612:1374:57DB12693F53C5868C6FF7DCE448EFE7 +functions\debug\fnc_zoneMarker.sqfc:38986:1259:7420AB82CC3CB6183431C7046D5C40B0 +functions\debug\script_component.hpp:40245:54:6CCE9C47B1794B440210AFE451B227F3 +functions\fnc_addShareInformationHandler.sqf:40299:768:B9A7EF1547857E04AD60962C9910AFCE +functions\fnc_addShareInformationHandler.sqfc:41067:634:53CE2EE3494C5BA0A40ABC763C5163B9 +functions\fnc_doAnimation.sqf:41701:2715:DD95E8DB9B8BEB995CF825842206DB69 +functions\fnc_doAnimation.sqfc:44416:2963:03F8CE5784AF6D1F95F2CC1FD8DF8E07 +functions\fnc_doCallout.sqf:47379:4119:886247A542F088212AB88DC3A34CC582 +functions\fnc_doCallout.sqfc:51498:4740:9FB7D50746717021A050AE6F90E59DC4 +functions\fnc_doGesture.sqf:56238:881:146D76790AD0931B54FF99BF7FCF4DD7 +functions\fnc_doGesture.sqfc:57119:1129:5704974E4DB443CF1521E6A4D9B9E04D +functions\fnc_doShareInformation.sqf:58248:3604:7ADCB6295B454860BA1791D0DDA6C2FE +functions\fnc_doShareInformation.sqfc:61852:4262:23A2F50E2DD929DA5E5CCE90A61F1EA9 +functions\fnc_eventCallback.sqf:66114:407:9E8A4068EC37A2E2D88A784F4FA5CE09 +functions\fnc_eventCallback.sqfc:66521:570:D2DE8C7C341B4C4B42AED6EC7CBC5D93 +functions\fnc_findBuildings.sqf:67091:1568:C18D8DF7219B33F4780080F49F5046FE +functions\fnc_findBuildings.sqfc:68659:1852:0C3D906F791A6B1733D92418927AD8A4 +functions\fnc_findClosestTarget.sqf:70511:1415:5F9BF2C6A7BE81226CBFF6D5FC3359AE +functions\fnc_findClosestTarget.sqfc:71926:2283:003F6EA8EF9C0FE9C391A2981461F3DC +functions\fnc_findCover.sqf:74209:4254:E117AAE9FD8C4D57C37C64A9C46061BC +functions\fnc_findCover.sqfc:78463:4029:E9FD799B42F8027B4E4CD08D5EB48856 +functions\fnc_findNearbyFriendlies.sqf:82492:718:5E3A45180FC54C775AEF57B216188E1A +functions\fnc_findNearbyFriendlies.sqfc:83210:1071:7DE9CDE6B618F08AD4966FEB2218FEC7 +functions\fnc_findOverwatch.sqf:84281:2311:906B9C025573BFDEC74DFCEE1924BCD0 +functions\fnc_findOverwatch.sqfc:86592:2430:71DFB088585E96D3891EB08D2FFEB260 +functions\fnc_findReadyUnits.sqf:89022:1135:3EF7A8BDC1A5F92DC463992C375C3199 +functions\fnc_findReadyUnits.sqfc:90157:1750:C7B6BC24D8B56599F3F3A32FB486C67C +functions\fnc_findReadyVehicles.sqf:91907:794:ACFE0122120C9B1DA2FE9AA9115095A3 +functions\fnc_findReadyVehicles.sqfc:92701:1137:C568D8D60FCC92B05225A9BDA338BC2B +functions\fnc_getShareInformationParams.sqf:93838:1511:C1718F94693184C38497FB12D751955C +functions\fnc_getShareInformationParams.sqfc:95349:2171:F6016F5AF23234F248210CA6F4C93B90 +functions\fnc_initModules.sqf:97520:756:B514410FFDF9C851E84F4793FE95BADF +functions\fnc_initModules.sqfc:98276:1405:E800BEEBAD4CEADBC232E2EBE725FF86 +functions\fnc_isAlive.sqf:99681:330:D04893CE48C9F38ACA343B20E3D1E1CE +functions\fnc_isAlive.sqfc:100011:580:DE46A9838EBD1793123611B5B45A409D +functions\fnc_isIndoor.sqf:100591:515:A361941DFC17161D58A62C9B6DE51FF6 +functions\fnc_isIndoor.sqfc:101106:1033:068FF953D1F2573ADF39716020714474 +functions\fnc_isNight.sqf:102139:528:34914BCE2B8BBEA3B777E54269F51362 +functions\fnc_isNight.sqfc:102667:834:6165DB54B060972A277C33E43FD867AF +functions\fnc_parseData.sqf:103501:1599:8130D308EA072D54405314A60244FBEC +functions\fnc_parseData.sqfc:105100:2101:AAB1909D219DD6C363D4A0E227E839E6 +functions\fnc_removeEventhandlers.sqf:107201:358:B008188ECD6556EE54EF928C857CAA08 +functions\fnc_removeEventhandlers.sqfc:107559:631:99536077754FC331382E73E3441F3EEF +functions\fnc_shouldSuppressPosition.sqf:108190:1734:457C2197E33627461A31F1D880EB6542 +functions\fnc_shouldSuppressPosition.sqfc:109924:2045:BDF54333D899DD5A36B04DB953E71B2F +functions\fnc_showDialog.sqf:111969:17484:6D336E3F536E5D53927CF060B5330BD3 +functions\fnc_showDialog.sqfc:129453:14165:089DDDBDAA108C6639E3549888AC0AD7 +functions\GroupAction\fnc_doGroupAssault.sqf:143618:1310:34BAE09754E0794D4424CB45BD189E11 +functions\GroupAction\fnc_doGroupAssault.sqfc:144928:2054:2847BF4987D9BBE25E2BF39F335C644B +functions\GroupAction\fnc_doGroupFlank.sqf:146982:1709:60049778C18682541DFD487013CD0FCF +functions\GroupAction\fnc_doGroupFlank.sqfc:148691:2531:7D2409FCE2672C859835F115D09C0ABF +functions\GroupAction\fnc_doGroupHide.sqf:151222:1306:60260116385013714F7EC1FCF3C93443 +functions\GroupAction\fnc_doGroupHide.sqfc:152528:1817:DD9131CF8023A4005DDD6948EA04398D +functions\GroupAction\fnc_doGroupStaticDeploy.sqf:154345:5372:6018D726A77E9CDAFFE842F7A3A06D96 +functions\GroupAction\fnc_doGroupStaticDeploy.sqfc:159717:5490:C8504C70E3B9BFA9DFDADF48EC0F831E +functions\GroupAction\fnc_doGroupStaticFind.sqf:165207:1459:E39A5896C2C944B9C74549D3288D1434 +functions\GroupAction\fnc_doGroupStaticFind.sqfc:166666:2072:AA6A220115C32AA26A0F6BC8CBD6E9DE +functions\GroupAction\fnc_doGroupStaticPack.sqf:168738:4096:FCEE7BBE0224CB50753F6A461D19ED8D +functions\GroupAction\fnc_doGroupStaticPack.sqfc:172834:4140:7F1840E13A7C060007478451CBC40DBF +functions\GroupAction\fnc_doGroupSuppress.sqf:176974:1660:4571F76BC3ECA04D39C870D87F662B7B +functions\GroupAction\fnc_doGroupSuppress.sqfc:178634:2271:F219A12618BAD15846B747E68C8CB473 +functions\GroupAction\script_component.hpp:180905:54:6CCE9C47B1794B440210AFE451B227F3 +functions\script_component.hpp:180959:1674:6C818BE2E1A844391B7C56A5BACB409B +functions\UnitAction\fnc_doAssault.sqf:182633:2409:4643604854D185431ECFC04E47E0B993 +functions\UnitAction\fnc_doAssault.sqfc:185042:2857:39DF87B25B3385A497A1A5E3F35623BB +functions\UnitAction\fnc_doAssaultCQB.sqf:187899:3689:812F62DBF0A037238BC6F3C952B8ADB7 +functions\UnitAction\fnc_doAssaultCQB.sqfc:191588:4257:6E4D3BF460A03F245E51EC99DF36D986 +functions\UnitAction\fnc_doAssaultMemory.sqf:195845:2575:A817071761005081E05C64C879793A90 +functions\UnitAction\fnc_doAssaultMemory.sqfc:198420:3219:5C72DB8B3DE4CA41049107699E6E4B33 +functions\UnitAction\fnc_doAssaultSpeed.sqf:201639:849:BCBD4783703D0FD7908065BD2B58E5D5 +functions\UnitAction\fnc_doAssaultSpeed.sqfc:202488:1275:75813FC01333DC14869C50FD5F0DFFA5 +functions\UnitAction\fnc_doCallArtillery.sqf:203763:1958:67605BB3ED4FA120D5E17E8531AEC12D +functions\UnitAction\fnc_doCallArtillery.sqfc:205721:2448:CDF1C706501FDDCE407142E1E011CF7A +functions\UnitAction\fnc_doCheckBody.sqf:208169:1841:A902B901C5311355BA424E3047CA5540 +functions\UnitAction\fnc_doCheckBody.sqfc:210010:2251:C26F9D32117FF9B3F83ECEB4D91DD0A2 +functions\UnitAction\fnc_doCover.sqf:212261:1392:7D26369D093AF8D7682F87C90FB43B93 +functions\UnitAction\fnc_doCover.sqfc:213653:1902:BBA503899ABD6318F461EBA1ACF71AF1 +functions\UnitAction\fnc_doDodge.sqf:215555:2138:21B1F1B7002E5AEC4E5A44D7BDEC44CB +functions\UnitAction\fnc_doDodge.sqfc:217693:2741:4E28BE43D9D4EE5AB530A4441BF13CC3 +functions\UnitAction\fnc_doFleeing.sqf:220434:4040:E4FD8B7CBA13090864EA92944D5C8B2A +functions\UnitAction\fnc_doFleeing.sqfc:224474:4538:846B5B4E94185D0CCCDFD1C8B4A48835 +functions\UnitAction\fnc_doHide.sqf:229012:2449:C8369FFEF10B696F102ABDCFFDF70FF6 +functions\UnitAction\fnc_doHide.sqfc:231461:2914:5A9BB3DF82216FE9DFFFA5957C4895B7 +functions\UnitAction\fnc_doPanic.sqf:234375:1075:C850B3184FE1C84063E782BCCB004BCC +functions\UnitAction\fnc_doPanic.sqfc:235450:1392:ACB386692A52D49E89B29F4B01171B11 +functions\UnitAction\fnc_doReposition.sqf:236842:1368:54A32AA78323A4BEB9E4D9D25B8B38C8 +functions\UnitAction\fnc_doReposition.sqfc:238210:1765:22A2DC21B7400DE59B546F07B5A76848 +functions\UnitAction\fnc_doSmoke.sqf:239975:2226:47F82D03E3DA24CB5AEF3803B93210BD +functions\UnitAction\fnc_doSmoke.sqfc:242201:2656:EA2BAB3E6CB4B82ACD2A44E0058C32BD +functions\UnitAction\fnc_doSuppress.sqf:244857:2244:695ECCFF70796AC572575AA1E163EA8A +functions\UnitAction\fnc_doSuppress.sqfc:247101:3117:722752D116A44FBC1087447650D60687 +functions\UnitAction\fnc_doUGL.sqf:250218:3142:A6C9FBAFDC41733635ACE857895ABF88 +functions\UnitAction\fnc_doUGL.sqfc:253360:3671:81911EF929103D3C7E6027068079D2AA +functions\UnitAction\script_component.hpp:257031:54:6CCE9C47B1794B440210AFE451B227F3 +functions\VehicleAction\fnc_doSelectWarhead.sqf:257085:4039:64EA81D906E765B8AFCEE02713017B6B +functions\VehicleAction\fnc_doSelectWarhead.sqfc:261124:3494:BE58CFBA00C79277A671C4F5DC9A0AB6 +functions\VehicleAction\fnc_doVehicleAssault.sqf:264618:3495:18D4D9D0A10C013A166F5BD1A5FE562B +functions\VehicleAction\fnc_doVehicleAssault.sqfc:268113:4140:47B79F46199F9611AF3CC34D6236E708 +functions\VehicleAction\fnc_doVehicleJink.sqf:272253:2293:0BA3960FFDDE107DA2A5AE06A337D40F +functions\VehicleAction\fnc_doVehicleJink.sqfc:274546:2472:F8341A0476DF266CA5B339FE6A727E5D +functions\VehicleAction\fnc_doVehicleRotate.sqf:277018:2357:BB43FDB71D82D79058CD52E5C4B73395 +functions\VehicleAction\fnc_doVehicleRotate.sqfc:279375:2845:FCABA725FDDE768D00E1FE52192D1C78 +functions\VehicleAction\fnc_doVehicleSuppress.sqf:282220:2438:B73A0B01C0F4BCA8685DE49A256FB298 +functions\VehicleAction\fnc_doVehicleSuppress.sqfc:284658:3165:C4E8CB8F10CFE0123F0D2A2FEB7FBA1A +functions\VehicleAction\script_component.hpp:287823:54:6CCE9C47B1794B440210AFE451B227F3 +LICENSE:287877:19533:D67DDD8E5A6674D3CF4BABA8690B0435 +script_component.hpp:307410:378:81FEE3003FE33ED7B0E0CCA7048067AD +script_macros.hpp:307788:1649:7078125D1AEA75121B5FE440AC108CAD +script_mod.hpp:309437:464:8ABEFB1F30A495F7EF62ED7AE0BEA495 +script_version.hpp:309901:67:A04B40744F097331C408ED808AC38BCE +settings.sqf:309968:7510:4F7F7E23F223ED95B493EDE1F5BBE761 +stringtable.xml:317478:28491:5C86CE7E82ECF695986D9F5F088298EA +XEH_postInit.sqf:345969:4220:7B525F1EEEFB0F1AC2EDD7CC6E3790EA +XEH_postInit.sqfc:350189:4212:C073B30ABF69600462F6A76ED28E5A45 +XEH_preInit.sqf:354401:387:07835FE399E6BF4323E3A5605F6E2618 +XEH_preInit.sqfc:354788:8026:ED73E43F18AC018B9EC33EFC756C1280 +XEH_PREP.hpp:362814:1702:E12572F50CF65AD5CB7C252A7CCFEFBD +XEH_preStart.sqf:364516:58:5B7DDBAB47A5596BA09F89A83B6AD057 +XEH_preStart.sqfc:364574:3528:58479A84F9713922EE54D5C5D35CDD40 +$$END$$:368102:21:A2604E54F372A00E34F8234DA4C2401A