├── .github ├── dependabot.yml └── workflows │ ├── coverage.yml │ ├── crates.yml │ └── rust.yml ├── .gitignore ├── 1620228832249.jpg ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── Cross.toml ├── DOCS.md ├── LICENSE ├── Makefile.toml ├── README.md ├── Scripts ├── Linux │ ├── Check.sh │ └── Create.sh ├── README.md └── Windows │ ├── Check.bat │ └── Create.bat ├── rustfmt.toml ├── src ├── algorithms.rs ├── error.rs ├── hashing │ ├── blake2b.rs │ ├── blake2s.rs │ ├── blake3.rs │ ├── crc32.rs │ ├── md5.rs │ ├── mod.rs │ ├── sha1.rs │ ├── sha2_224.rs │ ├── sha2_256.rs │ ├── sha2_384.rs │ ├── sha2_512.rs │ ├── sha3_224.rs │ ├── sha3_256.rs │ ├── sha3_384.rs │ ├── sha3_512.rs │ ├── whirlpool.rs │ ├── xxh3.rs │ ├── xxh32.rs │ └── xxh64.rs ├── lib.rs ├── main.rs ├── operations │ ├── compare.rs │ ├── mod.rs │ └── write.rs ├── options.rs └── utilities.rs └── tests └── algo.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | jobs: 8 | test: 9 | name: "coverage" 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | build: [stable] 14 | include: 15 | - build: stable 16 | os: ubuntu-latest 17 | rust: nightly 18 | steps: 19 | - name: "Checkout repository" 20 | uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 1 23 | 24 | - name: "Enable caching" 25 | uses: Swatinem/rust-cache@v2 26 | 27 | - name: Push to codecov.io 28 | env: 29 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 30 | run: | 31 | cargo install cargo-tarpaulin 32 | cargo tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml 33 | bash <(curl -s https://codecov.io/bash) -X gcov -t $CODECOV_TOKEN 34 | -------------------------------------------------------------------------------- /.github/workflows/crates.yml: -------------------------------------------------------------------------------- 1 | name: Publish on crates.io 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | jobs: 11 | test: 12 | name: "publish" 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | build: [linux] 17 | include: 18 | - build: linux 19 | os: ubuntu-latest 20 | rust: stable 21 | steps: 22 | - name: "Checkout repository" 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 1 26 | 27 | - name: "Install Rust" 28 | uses: actions-rs/toolchain@v1 29 | with: 30 | toolchain: ${{ matrix.rust }} 31 | override: true 32 | profile: default 33 | 34 | - name: "Enable caching" 35 | uses: Swatinem/rust-cache@v2 36 | 37 | - name: "Publish on crates.io" 38 | uses: actions-rs/cargo@v1 39 | with: 40 | command: publish 41 | args: --verbose --all-features --token ${{ secrets.CARGO_REGISTRY_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Build release binaries (and publish them if this is a tag) 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | binaries: 7 | name: ${{ matrix.os }} for ${{ matrix.target }} 8 | runs-on: ${{ matrix.os }} 9 | timeout-minutes: 30 10 | strategy: 11 | matrix: 12 | target: 13 | - x86_64-unknown-linux-musl 14 | - x86_64-unknown-linux-gnu 15 | - aarch64-unknown-linux-musl 16 | - armv7-unknown-linux-musleabihf 17 | - arm-unknown-linux-musleabihf 18 | - x86_64-pc-windows-msvc 19 | - x86_64-apple-darwin 20 | include: 21 | - os: ubuntu-latest 22 | target: x86_64-unknown-linux-gnu 23 | artifact_name: target/x86_64-unknown-linux-gnu/release/quickdash 24 | release_name: x86_64-unknown-linux-gnu 25 | cross: false 26 | strip: true 27 | compress: true 28 | - os: ubuntu-latest 29 | target: x86_64-unknown-linux-musl 30 | artifact_name: target/x86_64-unknown-linux-musl/release/quickdash 31 | release_name: x86_64-unknown-linux-musl 32 | cross: true 33 | strip: true 34 | compress: true 35 | - os: ubuntu-latest 36 | target: aarch64-unknown-linux-musl 37 | artifact_name: target/aarch64-unknown-linux-musl/release/quickdash 38 | release_name: aarch64-unknown-linux-musl 39 | cross: true 40 | strip: false 41 | compress: true 42 | - os: ubuntu-latest 43 | target: armv7-unknown-linux-musleabihf 44 | artifact_name: target/armv7-unknown-linux-musleabihf/release/quickdash 45 | release_name: armv7-unknown-linux-musleabihf 46 | cross: true 47 | strip: false 48 | compress: true 49 | cargo_flags: "" 50 | - os: ubuntu-latest 51 | target: arm-unknown-linux-musleabihf 52 | artifact_name: target/arm-unknown-linux-musleabihf/release/quickdash 53 | release_name: arm-unknown-linux-musleabihf 54 | cross: true 55 | strip: false 56 | compress: true 57 | - os: windows-latest 58 | target: x86_64-pc-windows-msvc 59 | artifact_name: target/x86_64-pc-windows-msvc/release/quickdash.exe 60 | release_name: x86_64-pc-windows-msvc.exe 61 | cross: false 62 | strip: true 63 | compress: true 64 | cargo_flags: "" 65 | - os: macos-latest 66 | target: x86_64-apple-darwin 67 | artifact_name: target/x86_64-apple-darwin/release/quickdash 68 | release_name: x86_64-apple-darwin 69 | cross: false 70 | strip: true 71 | compress: true 72 | 73 | steps: 74 | - name: Checkout code 75 | uses: actions/checkout@v4 76 | 77 | - name: Setup Rust toolchain 78 | uses: dtolnay/rust-toolchain@stable 79 | 80 | - name: "Enable caching" 81 | uses: Swatinem/rust-cache@v2 82 | 83 | - name: cargo build 84 | uses: houseabsolute/actions-rust-cross@v0 85 | with: 86 | command: build 87 | args: --all-features --release --target=${{ matrix.target }} 88 | 89 | - name: Compress binaries 90 | uses: svenstaro/upx-action@v2 91 | with: 92 | file: ${{ matrix.artifact_name }} 93 | args: --lzma 94 | strip: ${{ matrix.strip }} 95 | if: ${{ matrix.compress }} 96 | 97 | - name: Upload artifact 98 | uses: actions/upload-artifact@v4 99 | with: 100 | name: ${{ matrix.target }} 101 | path: ${{ matrix.artifact_name }} 102 | 103 | ### 104 | # Below this line, steps will only be ran if a tag was pushed. 105 | ### 106 | 107 | - name: Get tag name 108 | id: tag_name 109 | run: | 110 | echo ::set-output name=current_version::${GITHUB_REF#refs/tags/v} 111 | shell: bash 112 | if: startsWith(github.ref, 'refs/tags/v') 113 | 114 | - name: Publish 115 | uses: svenstaro/upload-release-action@v2 116 | with: 117 | repo_token: ${{ secrets.GITHUB_TOKEN }} 118 | file: ${{ matrix.artifact_name }} 119 | tag: ${{ github.ref }} 120 | asset_name: quickdash-$tag-${{ matrix.release_name }} 121 | body: ${{ steps.changelog_reader.outputs.log_entry }} 122 | if: startsWith(github.ref, 'refs/tags/v') 123 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | TEST.* -------------------------------------------------------------------------------- /1620228832249.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamtakingithard/QuickDash/9c80d4da23328aa2d158bdfcc5b64c45ae528aa1/1620228832249.jpg -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG (dates are in fromat YYYY-MM-DD) 2 | 3 | ## 0.5.4 and 0.5.5 (2021-06-25) 4 | 5 | These releases were intended to fix the problems with crates.io 6 | 7 | ## 0.5.3 (2021-06-25) 8 | 9 | Added XXH32 10 | Windows builds are compressed with UPX 11 | Added code coverage, and wrote some tests 12 | From this version all builds are built via GitHub actions. 13 | 14 | ## 0.5.1 (2021-06-21) 15 | 16 | Ability to read both `hash - filename` and vice versa 17 | 18 | ## 0.5.0 (2021-06-19) 19 | 20 | Switching to Futures 0.3 21 | 22 | ## 0.4.0 and 0.4.1 (2021-06-08) 23 | 24 | Adding support for xxHash. 25 | 26 | ## 0.3.8 (2021-06-04) 27 | 28 | Switched all crates to RustCrypto maintained ones, also switched to Rust maintained sha1 crate. 29 | Added SHA-2,SHA-3, Whirlpool support. 30 | 31 | ## 0.3.6 (2021-06-01) 32 | 33 | Switched CRC crate, to crc32fast, which removed the support of crc64 but made crc32 far more faster 34 | 35 | ## 0.3.5 (2020-05-31) 36 | 37 | Program, can read lowercase hashes. 38 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | This project adheres to No Code of Conduct. We are all adults. We accept anyone's contributions. Nothing else matters. 4 | 5 | For more information please visit the [No Code of Conduct](https://nocodeofconduct.com/) homepage. 6 | 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "quickdash" 3 | description = "A modern alternative to QuickSFV using Rust." 4 | repository = "https://github.com/iamtakingithard/QuickDash" 5 | readme = "README.md" 6 | keywords = ["cli", "hash", "verify"] 7 | categories = ["authentication", "filesystem", "command-line-utilities"] 8 | license = "Apache-2.0" 9 | version = "0.7.0" 10 | authors = ["cerda", "b1tzxd", "mjc", "taskylizard"] 11 | edition = "2024" 12 | 13 | [dependencies] 14 | blake2 = "0.10.4" 15 | blake3 = "1.3.1" 16 | clap = { version = "4.4.10", features = ["derive"] } 17 | crc32fast = "1.3.2" 18 | indicatif = { version = "0.17.11", features = ["rayon"] } 19 | md-5 = "0.10.1" 20 | num_cpus = "1.13.1" 21 | once_cell = "1.10.0" 22 | rayon = "1.5.1" 23 | regex = "1.5.5" 24 | sha-1 = "0.10.0" 25 | sha2 = "0.10.2" 26 | sha3 = "0.10.1" 27 | tabwriter = "1.2.1" 28 | walkdir = "2.3.2" 29 | whirlpool = "0.10.1" 30 | xxhash-rust = { version = "0.8.4", features = ["xxh3", "xxh32", "xxh64"] } 31 | 32 | [profile.release] 33 | codegen-units = 1 34 | debug = false 35 | lto = true 36 | # panic = 'abort' 37 | 38 | [[bin]] 39 | doc = false 40 | name = "quickdash" 41 | path = "src/main.rs" 42 | test = false 43 | 44 | [lib] 45 | name = "quickdash" 46 | path = "src/lib.rs" 47 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.i686-pc-windows-msvc] 2 | xargo = true 3 | 4 | -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 | ``` 2 | QuickDash 0.6.0 3 | A modern alternative to QuickSFV using Rust. Made with <3 by Cerda. 4 | 5 | USAGE: 6 | quickdash [OPTIONS] 7 | 8 | OPTIONS: 9 | -a, --algorithm 10 | Hashing algorithm to use 11 | 12 | [default: blake3] 13 | [possible values: sha1, sha2224, sha2256, sha2384, sha2512, sha3224, sha3256, sha3384, 14 | sha3512, xxh32, xxh64, xxh3, crc32, md5, whirl-pool, blake2b, blake2s, blake3] 15 | 16 | -d, --depth 17 | Max recursion depth. Infinite if None. Default: `0` 18 | 19 | --follow-symlinks 20 | Whether to recurse down symlinks. Default: `true` 21 | 22 | -h, --help 23 | Print help information 24 | 25 | -i, --ignored-files 26 | Files/directories to ignore. Default: none 27 | 28 | -j, --jobs 29 | # of threads used for hashing 30 | 31 | [default: 0] 32 | 33 | -V, --version 34 | Print version information 35 | 36 | SUBCOMMANDS: 37 | create 38 | 39 | help 40 | Print this message or the help of the given subcommand(s) 41 | verify 42 | ``` 43 | 44 | ``` 45 | quickdash-create 46 | 47 | USAGE: 48 | quickdash create [OPTIONS] [PATH] 49 | 50 | ARGS: 51 | Directory to hash. Default: current directory [default: .] 52 | 53 | OPTIONS: 54 | -f, --force 55 | --file Output filename. Default: `directory_name.hash"` 56 | -h, --help Print help information 57 | ``` 58 | 59 | ``` 60 | quickdash-verify 61 | 62 | USAGE: 63 | quickdash verify [OPTIONS] [PATH] 64 | 65 | ARGS: 66 | Directory to verify. Default: current directory [default: .] 67 | 68 | OPTIONS: 69 | --file Input filename. Default: `directory_name.hash"` 70 | -h, --help Print help information 71 | ``` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Andre Vuillemot 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | [env] 2 | CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true 3 | 4 | [tasks.format] 5 | install_crate = "rustfmt" 6 | command = "cargo" 7 | args = ["fmt", "--all", "--", "--check"] 8 | 9 | [tasks.clippy] 10 | command = "cargo" 11 | args = ["clippy", "--all", "--", "-D", "warnings"] 12 | 13 | [tasks.upgrade] 14 | install_crate = "cargo-edit" 15 | command = "cargo" 16 | args = ["upgrade", "--workspace"] 17 | 18 | [tasks.release] 19 | dependencies = [ 20 | "upgrade", 21 | "format", 22 | "clippy", 23 | # "build-release", 24 | ] 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![The origins](https://raw.githubusercontent.com/iamtakingithard/QuickDash/main/1620228832249.jpg) 2 | 3 | 4 | # QuickDash [![Rust](https://github.com/iamtakingithard/QuickDash/actions/workflows/rust.yml/badge.svg)](https://github.com/iamtakingithard/QuickDash/actions/workflows/rust.yml) [![](https://meritbadge.herokuapp.com/quickdash)](https://crates.io/crates/quickdash) [![codecov](https://codecov.io/gh/iamtakingithard/QuickDash/branch/main/graph/badge.svg?token=YA4NPM8NPJ)](https://codecov.io/gh/iamtakingithard/QuickDash) 5 | A modern alternative to QuickSFV using Rust. It's supports BLAKE3 and BLAKE2 hashes, CRC32, MD5, SHA1, SHA2, SHA3, xxHash 6 | 7 | Note: the old name `quick_dash` is no longer in use, if anyone wants it feel free to take it on crates.io 8 | 9 | ## Benchmarks 10 | Benchmarks were performed under Windows 10 with Ryzen 5 1600. 11 | For benchmarking the program [`hyperfine`](https://github.com/sharkdp/hyperfine) was used. 12 | It was checking the hashed the source code of the QuickDash. 13 | 14 | ``` 15 | Benchmark #1: quickdash.exe -a CRC32 --verify -f TEST.sfv 16 | Time (mean ± σ): 10.7 ms ± 2.9 ms [User: 12.8 ms, System: 3.2 ms] 17 | Range (min … max): 9.5 ms … 23.3 ms 233 runs 18 | 19 | Benchmark #2: quicksfv.exe TEST.sfv 20 | Time (mean ± σ): 83.7 ms ± 16.0 ms [User: 30.9 ms, System: 28.0 ms] 21 | Range (min … max): 63.8 ms … 117.4 ms 31 runs 22 | ``` 23 | 24 | ## Install 25 | There are two ways of doing that. You can 26 | A) Get a binary from crates.io with command `cargo install quickdash` 27 | B) Get a already compiled binary from GitHub, which features Windows, Mac, Linux builds. 28 | 29 | ## Building 30 | Well, just download the source code, then go to the cloned repo, and write `cargo build --release` 31 | 32 | ## License 33 | This program is licensed under [Apache License 2.0](https://choosealicense.com/licenses/apache-2.0/) license. 34 | 35 | ## Thanks 36 | I would like to say thanks to the [Timo](https://github.com/timokoesters) and all future contributors to this project. 37 | -------------------------------------------------------------------------------- /Scripts/Linux/Check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## btw if you are using MD5 then change .sfv to .md5 and algo to MD$ too! 4 | ./quickdash -a BLAKE3 --force --verify -f TEST.blake3 5 | 6 | -------------------------------------------------------------------------------- /Scripts/Linux/Create.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ## btw if you are using MD5 then change .sfv to .md5 and algo to MD% too! 4 | ./quickdash --algorithm BLAKE3 --create --force -f TEST.blake3 5 | -------------------------------------------------------------------------------- /Scripts/README.md: -------------------------------------------------------------------------------- 1 | So these are simple scripts to simplify your life. 2 | -------------------------------------------------------------------------------- /Scripts/Windows/Check.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | quickdash.exe -a BLAKE3 --verify --force -f TEST.blake3 3 | pause 4 | 5 | -------------------------------------------------------------------------------- /Scripts/Windows/Create.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | quickdash.exe --algorithm BLAKE3 --create --force -f TEST.blake3 3 | 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Core 2 | hard_tabs = true 3 | newline_style = "Unix" 4 | 5 | # Miscellaneous 6 | condense_wildcard_suffixes = true 7 | format_code_in_doc_comments = true 8 | format_macro_bodies = true 9 | format_macro_matchers = true 10 | format_strings = true 11 | reorder_modules = true 12 | use_field_init_shorthand = true 13 | wrap_comments = true 14 | 15 | # Imports 16 | group_imports = "StdExternalCrate" 17 | imports_granularity = "Crate" 18 | imports_indent = "Block" 19 | imports_layout = "HorizontalVertical" 20 | reorder_imports = true 21 | 22 | # Reports 23 | report_fixme = "Unnumbered" 24 | report_todo = "Unnumbered" 25 | -------------------------------------------------------------------------------- /src/algorithms.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use std::str::FromStr; 17 | 18 | use clap::ValueEnum; 19 | 20 | /// A hashing algorithm. 21 | /// 22 | /// # Examples 23 | /// 24 | /// ``` 25 | /// # use std::str::FromStr; 26 | /// assert_eq!( 27 | /// quickdash::Algorithm::from_str("BLAKE3"), 28 | /// Ok(quickdash::Algorithm::BLAKE3) 29 | /// ); 30 | /// assert_eq!( 31 | /// quickdash::Algorithm::from_str("MD5"), 32 | /// Ok(quickdash::Algorithm::MD5) 33 | /// ); 34 | /// ``` 35 | 36 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] 37 | pub enum Algorithm { 38 | SHA1, 39 | SHA2224, 40 | SHA2256, 41 | SHA2384, 42 | SHA2512, 43 | SHA3224, 44 | SHA3256, 45 | SHA3384, 46 | SHA3512, 47 | XXH32, 48 | XXH64, 49 | XXH3, 50 | CRC32, 51 | MD5, 52 | WhirlPool, 53 | BLAKE2B, 54 | BLAKE2S, 55 | BLAKE3, 56 | } 57 | 58 | impl Algorithm { 59 | /// Length, in bytes, of the algorithm's output hex string 60 | pub fn hexlen(&self) -> usize { 61 | match *self { 62 | Algorithm::CRC32 | Algorithm::XXH32 => 8, 63 | Algorithm::XXH3 | Algorithm::XXH64 => 16, 64 | Algorithm::MD5 => 32, 65 | Algorithm::SHA3256 | Algorithm::SHA2256 | Algorithm::BLAKE2S | Algorithm::BLAKE3 => 64, 66 | Algorithm::SHA1 => 40, 67 | Algorithm::SHA2224 | Algorithm::SHA3224 => 56, 68 | Algorithm::SHA2384 | Algorithm::SHA3384 => 96, 69 | Algorithm::BLAKE2B | Algorithm::SHA3512 | Algorithm::SHA2512 | Algorithm::WhirlPool => { 70 | 128 71 | } 72 | } 73 | } 74 | } 75 | 76 | impl FromStr for Algorithm { 77 | type Err = String; 78 | 79 | fn from_str(s: &str) -> Result { 80 | match &s.replace('_', "-").to_lowercase()[..] { 81 | "sha-1" | "sha1" => Ok(Algorithm::SHA1), 82 | "sha2224" | "sha-224" | "sha-2-224" => Ok(Algorithm::SHA2224), 83 | "sha2256" | "sha-256" | "sha-2-256" => Ok(Algorithm::SHA2256), 84 | "sha2384" | "sha-384" | "sha-2-384" => Ok(Algorithm::SHA2384), 85 | "sha2512" | "sha-512" | "sha-2-512" => Ok(Algorithm::SHA2512), 86 | "sha3224" | "sha3-224" | "sha-3-224" => Ok(Algorithm::SHA3224), 87 | "sha3256" | "sha3-256" | "sha-3-256" => Ok(Algorithm::SHA3256), 88 | "sha3384" | "sha3-384" | "sha-3-384" => Ok(Algorithm::SHA3384), 89 | "sha3512" | "sha3-512" | "sha-3-512" => Ok(Algorithm::SHA3512), 90 | "crc32" => Ok(Algorithm::CRC32), 91 | "xxhash64" | "xxh64" => Ok(Algorithm::XXH64), 92 | "xxhash32" | "xxh32" => Ok(Algorithm::XXH32), 93 | "xxhash3" | "xxh3" => Ok(Algorithm::XXH3), 94 | "md5" => Ok(Algorithm::MD5), 95 | "blake2b" => Ok(Algorithm::BLAKE2B), 96 | "blake2s" => Ok(Algorithm::BLAKE2S), 97 | "blake3" => Ok(Algorithm::BLAKE3), 98 | "whirlpool" => Ok(Algorithm::WhirlPool), 99 | _ => Err(format!("\"{}\" is not a recognised hashing algorithm", s)), 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /// Enum representing each way the appication can fail. 17 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] 18 | pub enum Error { 19 | /// No errors occured, everything executed correctly. 20 | NoError, 21 | /// Parsing of command-line options failed. 22 | OptionParsingError, 23 | /// Selected and saved hash lengths differ. 24 | HashLengthDiffers, 25 | /// Parsing the hashes file failed. 26 | HashesFileParsingFailure, 27 | /// The specified amount of files do not match. 28 | NFilesDiffer(i32), 29 | } 30 | 31 | impl Error { 32 | /// Get the executable exit value from an `Error` instance. 33 | pub fn exit_value(&self) -> i32 { 34 | match *self { 35 | Error::NoError => 0, 36 | Error::OptionParsingError => 1, 37 | Error::HashLengthDiffers => 2, 38 | Error::HashesFileParsingFailure => 3, 39 | Error::NFilesDiffer(i) => i + 3, 40 | } 41 | } 42 | } 43 | 44 | impl From for Error { 45 | fn from(i: i32) -> Self { 46 | match i { 47 | 0 => Error::NoError, 48 | 1 => Error::OptionParsingError, 49 | 2 => Error::HashLengthDiffers, 50 | 3 => Error::HashesFileParsingFailure, 51 | i => Error::NFilesDiffer(i - 3), 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/hashing/blake2b.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use blake2::{Blake2b512, Digest}; 17 | 18 | use crate::hash_string; 19 | 20 | hash_func!( 21 | Blake2b512::new(), 22 | |blake: &mut Blake2b512, buffer: &[u8]| blake.update(buffer), 23 | |blake: Blake2b512| { hash_string(&blake.finalize()) } 24 | ); 25 | -------------------------------------------------------------------------------- /src/hashing/blake2s.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use blake2::{Blake2s256, Digest}; 17 | 18 | use crate::hash_string; 19 | 20 | hash_func!( 21 | Blake2s256::new(), 22 | |blake: &mut Blake2s256, buffer: &[u8]| blake.update(buffer), 23 | |blake: Blake2s256| { hash_string(&blake.finalize()) } 24 | ); 25 | -------------------------------------------------------------------------------- /src/hashing/blake3.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | use crate::hash_string; 16 | 17 | hash_func!( 18 | blake3::Hasher::new(), 19 | |blake: &mut blake3::Hasher, buffer: &[u8]| { 20 | blake.update(buffer); 21 | }, 22 | |blake: blake3::Hasher| hash_string(blake.finalize().as_bytes()) 23 | ); 24 | -------------------------------------------------------------------------------- /src/hashing/crc32.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | hash_func!( 17 | crc32fast::Hasher::new(), 18 | |crc: &mut crc32fast::Hasher, buffer: &[u8]| crc.update(buffer), 19 | |crc: crc32fast::Hasher| format!("{:08X}", (&crc.finalize())) 20 | ); 21 | -------------------------------------------------------------------------------- /src/hashing/md5.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use md5::{Digest, Md5}; 17 | 18 | use crate::hash_string; 19 | 20 | hash_func_write!(Md5::new(), |ctx: Md5| hash_string(&*ctx.finalize())); 21 | -------------------------------------------------------------------------------- /src/hashing/mod.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | macro_rules! hash_func { 17 | ($ctx:expr, $update:expr, $convert:expr) => { 18 | use std::io::Read; 19 | 20 | pub fn hash(reader: &mut R) -> String { 21 | let mut buffer = vec![0; 4096]; 22 | 23 | let mut ctx = $ctx; 24 | loop { 25 | let read = reader.read(&mut buffer[..]).unwrap(); 26 | 27 | if read == 0 { 28 | break; 29 | } 30 | 31 | $update(&mut ctx, &buffer[..read]); 32 | } 33 | 34 | $convert(ctx) 35 | } 36 | }; 37 | } 38 | 39 | macro_rules! hash_func_write { 40 | ($ctx:expr, $convert:expr) => { 41 | use std::io::{self, Read}; 42 | 43 | pub fn hash(reader: &mut R) -> String { 44 | let mut ctx = $ctx; 45 | io::copy(reader, &mut ctx).unwrap(); 46 | $convert(ctx) 47 | } 48 | }; 49 | } 50 | 51 | use std::{fmt::Write, fs::File, io::Read, path::Path}; 52 | 53 | use super::Algorithm; 54 | 55 | mod blake2b; 56 | mod blake2s; 57 | mod blake3; 58 | mod crc32; 59 | mod md5; 60 | mod sha1; 61 | mod sha2_224; 62 | mod sha2_256; 63 | mod sha2_384; 64 | mod sha2_512; 65 | mod sha3_224; 66 | mod sha3_256; 67 | mod sha3_384; 68 | mod sha3_512; 69 | mod whirlpool; 70 | mod xxh3; 71 | mod xxh32; 72 | mod xxh64; 73 | 74 | /// Hash the specified file using the specified hashing algorithm. 75 | pub fn hash_file(algo: Algorithm, path: &Path) -> String { 76 | hash_reader(algo, &mut File::open(path).unwrap()) 77 | } 78 | 79 | /// Hash the specified byte stream using the specified hashing algorithm. 80 | pub fn hash_reader(algo: Algorithm, data: &mut R) -> String { 81 | match algo { 82 | Algorithm::CRC32 => crc32::hash(data), 83 | Algorithm::SHA1 => sha1::hash(data), 84 | Algorithm::SHA2224 => sha2_224::hash(data), 85 | Algorithm::SHA2256 => sha2_256::hash(data), 86 | Algorithm::SHA2384 => sha2_384::hash(data), 87 | Algorithm::SHA2512 => sha2_512::hash(data), 88 | Algorithm::SHA3224 => sha3_224::hash(data), 89 | Algorithm::SHA3256 => sha3_256::hash(data), 90 | Algorithm::SHA3384 => sha3_384::hash(data), 91 | Algorithm::SHA3512 => sha3_512::hash(data), 92 | Algorithm::MD5 => md5::hash(data), 93 | Algorithm::XXH64 => xxh64::hash(data), 94 | Algorithm::XXH32 => xxh32::hash(data), 95 | Algorithm::XXH3 => xxh3::hash(data), 96 | Algorithm::BLAKE2B => blake2b::hash(data), 97 | Algorithm::BLAKE2S => blake2s::hash(data), 98 | Algorithm::BLAKE3 => blake3::hash(data), 99 | Algorithm::WhirlPool => whirlpool::hash(data), 100 | } 101 | } 102 | 103 | /// Create a hash string out of its raw bytes. 104 | /// 105 | /// # Examples 106 | /// 107 | /// ``` 108 | /// assert_eq!( 109 | /// quickdash::hash_string(&[0x99, 0xAA, 0xBB, 0xCC]), 110 | /// "99AABBCC".to_string() 111 | /// ); 112 | /// assert_eq!(quickdash::hash_string(&[0x09, 0x0A]), "090A".to_string()); 113 | /// ``` 114 | pub fn hash_string(bytes: &[u8]) -> String { 115 | let mut result = String::with_capacity(bytes.len() * 2); 116 | for b in bytes { 117 | write!(result, "{:02X}", b).unwrap(); 118 | } 119 | result 120 | } 121 | -------------------------------------------------------------------------------- /src/hashing/sha1.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use sha1::{Digest, Sha1}; 17 | 18 | use crate::hash_string; 19 | 20 | hash_func!( 21 | Sha1::new(), 22 | |sha1: &mut Sha1, buffer: &[u8]| sha1.update(buffer), 23 | |sha1: Sha1| hash_string(&sha1.finalize()) 24 | ); 25 | -------------------------------------------------------------------------------- /src/hashing/sha2_224.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use sha2::{Digest, Sha224}; 17 | 18 | use crate::hash_string; 19 | 20 | hash_func!( 21 | Sha224::new(), 22 | |sha224: &mut Sha224, buffer: &[u8]| sha224.update(buffer), 23 | |sha224: Sha224| { hash_string(&sha224.finalize()) } 24 | ); 25 | -------------------------------------------------------------------------------- /src/hashing/sha2_256.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use sha2::{Digest, Sha256}; 17 | 18 | use crate::hash_string; 19 | 20 | hash_func!( 21 | Sha256::new(), 22 | |sha256: &mut Sha256, buffer: &[u8]| sha256.update(buffer), 23 | |sha256: Sha256| { hash_string(&sha256.finalize()) } 24 | ); 25 | -------------------------------------------------------------------------------- /src/hashing/sha2_384.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use sha2::{Digest, Sha384}; 17 | 18 | use crate::hash_string; 19 | 20 | hash_func!( 21 | Sha384::new(), 22 | |sha348: &mut Sha384, buffer: &[u8]| sha348.update(buffer), 23 | |sha348: Sha384| { hash_string(&sha348.finalize()) } 24 | ); 25 | -------------------------------------------------------------------------------- /src/hashing/sha2_512.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use sha2::{Digest, Sha512}; 17 | 18 | use crate::hash_string; 19 | 20 | hash_func!( 21 | Sha512::new(), 22 | |sha512: &mut Sha512, buffer: &[u8]| sha512.update(buffer), 23 | |sha512: Sha512| { hash_string(&sha512.finalize()) } 24 | ); 25 | -------------------------------------------------------------------------------- /src/hashing/sha3_224.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use sha3::{Digest, Sha3_224}; 17 | 18 | use crate::hash_string; 19 | 20 | hash_func!( 21 | Sha3_224::new(), 22 | |sha3224: &mut Sha3_224, buffer: &[u8]| sha3224.update(buffer), 23 | |sha3224: Sha3_224| hash_string(&sha3224.finalize()) 24 | ); 25 | -------------------------------------------------------------------------------- /src/hashing/sha3_256.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use sha3::{Digest, Sha3_256}; 17 | 18 | use crate::hash_string; 19 | 20 | hash_func!( 21 | Sha3_256::new(), 22 | |sha3256: &mut Sha3_256, buffer: &[u8]| sha3256.update(buffer), 23 | |sha3256: Sha3_256| hash_string(&sha3256.finalize()) 24 | ); 25 | -------------------------------------------------------------------------------- /src/hashing/sha3_384.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use sha3::{Digest, Sha3_384}; 17 | 18 | use crate::hash_string; 19 | 20 | hash_func!( 21 | Sha3_384::new(), 22 | |sha3384: &mut Sha3_384, buffer: &[u8]| sha3384.update(buffer), 23 | |sha3384: Sha3_384| hash_string(&sha3384.finalize()) 24 | ); 25 | -------------------------------------------------------------------------------- /src/hashing/sha3_512.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use sha3::{Digest, Sha3_512}; 17 | 18 | use crate::hash_string; 19 | 20 | hash_func!( 21 | Sha3_512::new(), 22 | |sha3512: &mut Sha3_512, buffer: &[u8]| sha3512.update(buffer), 23 | |sha3512: Sha3_512| hash_string(&sha3512.finalize()) 24 | ); 25 | -------------------------------------------------------------------------------- /src/hashing/whirlpool.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use whirlpool::{Digest, Whirlpool}; 17 | 18 | use crate::hash_string; 19 | 20 | hash_func!( 21 | Whirlpool::new(), 22 | |whirlpool: &mut Whirlpool, buffer: &[u8]| whirlpool.update(buffer), 23 | |whirlpool: Whirlpool| hash_string(&whirlpool.finalize()) 24 | ); 25 | -------------------------------------------------------------------------------- /src/hashing/xxh3.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | hash_func!( 17 | xxhash_rust::xxh3::Xxh3::new(), 18 | |xxh3: &mut xxhash_rust::xxh3::Xxh3, buffer: &[u8]| xxh3.update(buffer), 19 | |xxh3: xxhash_rust::xxh3::Xxh3| format!("{:08X}", (&xxh3.digest())) 20 | ); 21 | -------------------------------------------------------------------------------- /src/hashing/xxh32.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | hash_func!( 17 | xxhash_rust::xxh32::Xxh32::new(1234), 18 | |xxh32: &mut xxhash_rust::xxh32::Xxh32, buffer: &[u8]| xxh32.update(buffer), 19 | |xxh32: xxhash_rust::xxh32::Xxh32| format!("{:08X}", (&xxh32.digest())) 20 | ); 21 | -------------------------------------------------------------------------------- /src/hashing/xxh64.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | hash_func!( 17 | xxhash_rust::xxh64::Xxh64::new(1234), 18 | |xxh: &mut xxhash_rust::xxh64::Xxh64, buffer: &[u8]| xxh.update(buffer), 19 | |xxh: xxhash_rust::xxh64::Xxh64| format!("{:08X}", (&xxh.digest())) 20 | ); 21 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | //! Tool for making/verifying hashes. 17 | //! 18 | //! # Library doc 19 | //! 20 | //! The library it's self is used in this project as well due it contains all 21 | //! needed functions. 22 | //! 23 | //! ## Data flow 24 | //! 25 | //! Hash verification 26 | //! 27 | //! ```plaintext 28 | //! Options 29 | //! |> create_hashes() 30 | //! |> load_hashes() 31 | //! |> compare_hashes() 32 | //! |> write_hash_comparison_results() 33 | //! ``` 34 | //! 35 | //! Hash creation 36 | //! 37 | //! ```plaintext 38 | //! Options 39 | //! |> create_hashes() 40 | //! |> write_hashes() 41 | //! ``` 42 | //! 43 | //! # Executable manpage 44 | //! 45 | //! Exit values and possible errors: 46 | //! 47 | //! ```text 48 | //! 1 - option parsing error 49 | //! 2 - hash lengths differ between selected and saved 50 | //! 3 - failed to parse hashes file 51 | //! N+3 - N files didn't match 52 | //! ``` 53 | //! 54 | //! ## SYNOPSIS 55 | //! 56 | //! [`QuickDash`](https://github.com/AndreVuillemot160/QuickDash) [OPTIONS] [DIRECTORY] 57 | //! 58 | //! ## DESCRIPTION 59 | //! 60 | //! This is a utility for making/checking hashes with blazing-fast speed. All 61 | //! most well-known hash functions are supported, like MD5, SHA1, SHA2 etc. It's 62 | //! licensed under Apache-2.0 License. 63 | //! 64 | //! ## OPTIONS 65 | //! 66 | //! -a --algorithm <algorithm> 67 | //! 68 | //! ```text 69 | //! Quite simple, select the hash you want. Case-insensitive. 70 | //! 71 | //! Supported algorithms: SHA{1,2-,3-{224,256,384,512}, CRC32, MD5, BLAKE{2B,2S,3}, XXH3, XXHASH64 72 | //! ``` 73 | //! 74 | //! -c --create 75 | //! 76 | //! ```text 77 | //! A very simple command. What it does, it creates hashes. 78 | //! If user didn't specified the name it will use, the name of folder with `.hash` extension. 79 | //! 80 | //! And will also fail if the output file exists already and the command `--force` is not presented. 81 | //! 82 | //! Only with `--verify`. Overrides `--verify`. 83 | //! ``` 84 | //! 85 | //! -v --verify 86 | //! 87 | //! ```text 88 | //! Verify directory hashes. Used by default. 89 | //! 90 | //! Only with `--create`. Overrides `--create`. 91 | //! ``` 92 | //! 93 | //! -d --depth <depth> 94 | //! 95 | //! ```text 96 | //! Set max recursion depth to `depth`. Default: 0. 97 | //! 98 | //! Only with `--recursive`. Overrides `--recursive`. 99 | //! ``` 100 | //! 101 | //! -r --recursive 102 | //! 103 | //! ```text 104 | //! Set max recursion depth to infinity. By default it doesn't recurse. 105 | //! 106 | //! Only with `--depth`. Overrides `--depth`. 107 | //! ``` 108 | //! 109 | //! --follow-symlinks 110 | //! 111 | //! ```text 112 | //! Recurse down symlinks. Default. 113 | //! ``` 114 | //! 115 | //! --no-follow-symlinks 116 | //! 117 | //! ```text 118 | //! Don't recurse down symlinks. 119 | //! ``` 120 | //! 121 | //! -i --ignore <filename[,filename2][,filename3][,filenameN]...>... 122 | //! 123 | //! ```text 124 | //! Add filename(s) to ignored files list. Default: not used. 125 | //! 126 | //! The program marks the files that will be ignored. The specified ignored files will not come to the output file. 127 | //! 128 | //! Can be used multiple times. 129 | //! ``` 130 | //! 131 | //! --force 132 | //! 133 | //! ```text 134 | //! Rewrite the output file in `--create` mode. 135 | //! ``` 136 | //! 137 | //! -j --jobs [jobs] 138 | //! 139 | //! ```text 140 | //! Amount of threads used for hashing. Default: # of CPU threads 141 | //! 142 | //! One thread can hash one file at a time, potentially speeding up hashing 143 | //! up to `jobs` times. 144 | //! 145 | //! No/empty value: # of CPU threads. value = 0: maximum, of u8 (255) 146 | //! 147 | //! ``` 148 | //! 149 | //! [DIRECTORY] 150 | //! 151 | //! ```text 152 | //! Directory to create/verify hash for. By default is current workdir. 153 | //! ``` 154 | //! 155 | //! ## EXAMPLES 156 | //! 157 | //! `quickdash` [`-v`] [`-f` *infile*] 158 | //! 159 | //! ```text 160 | //! Verify the current directory using the saved hashes. 161 | //! 162 | //! `-v` is not necessary as it's the default. 163 | //! 164 | //! *infile* defaults to "`DIRECTORY`.hash" 165 | //! 166 | //! Example output: 167 | //! File added: "file_that_hasnt_been_before" 168 | //! File removed: "file_that_was_originally_here_before_but_not_now" 169 | //! File ignored: "file_specified_with_ignore_now_or_during_creation" 170 | //! 171 | //! File "file_that_did_not_change" matches 172 | //! File "changed_file" doesn't match 173 | //! Was: foo 174 | //! Is : bar 175 | //! ``` 176 | //! 177 | //! `examples` `-c` [`-f` *outfile*] [`--force`] 178 | //! 179 | //! ```text 180 | //! Create hashes of the current directory tree for later verification. 181 | //! 182 | //! *outfile* defaults to "`DIRECTORY`.hash". 183 | //! 184 | //! Use `--force` to override *outfile*. 185 | //! 186 | //! *outfile* contents: 187 | //! F013BF0B163785CBB3BE52DE981E069E2B64E1CAC863815AC7BEED63E1734BAE Cargo.toml 188 | //! E84E380AEBDA3D98E96267201D61784C3D6FFB128C4D669E6C1D994C7D7BF32B Cross.toml 189 | //! ``` 190 | 191 | #![deny(unsafe_code)] 192 | #![allow(clippy::tabs_in_doc_comments)] 193 | 194 | mod algorithms; 195 | mod error; 196 | mod hashing; 197 | mod options; 198 | 199 | pub mod operations; 200 | pub mod utilities; 201 | 202 | pub use crate::{ 203 | algorithms::Algorithm, 204 | error::Error, 205 | hashing::*, 206 | options::{Commands, Mode}, 207 | }; 208 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use std::{ 17 | fs::remove_file, 18 | io::{stderr, stdout}, 19 | path::{Path, PathBuf}, 20 | process::exit, 21 | }; 22 | 23 | use clap::Parser; 24 | use quickdash::{Commands, Mode}; 25 | 26 | const BANNER: [&str; 13] = [ 27 | "", 28 | // PS I know it's kinda awful and all. But still cool. 29 | " █████ █ ██ ██▓ ▄████▄ ██ ▄█▀▓█████▄ ▄▄▄ ██████ ██░ ██ ", 30 | "▒██▓ ██▒ ██ ▓██▒▓██▒▒██▀ ▀█ ██▄█▒ ▒██▀ ██▌▒████▄ ▒██ ▒ ▓██░ ██▒", 31 | "▒██▒ ██░▓██ ▒██░▒██▒▒▓█ ▄ ▓███▄░ ░██ █▌▒██ ▀█▄ ░ ▓██▄ ▒██▀▀██░", 32 | "░██ █▀ ░▓▓█ ░██░░██░▒▓▓▄ ▄██▒▓██ █▄ ░▓█▄ ▌░██▄▄▄▄██ ▒ ██▒░▓█ ░██ ", 33 | "░▒███▒█▄ ▒▒█████▓ ░██░▒ ▓███▀ ░▒██▒ █▄░▒████▓ ▓█ ▓██▒▒██████▒▒░▓█▒░██▓", 34 | "░░ ▒▒░ ▒ ░▒▓▒ ▒ ▒ ░▓ ░ ░▒ ▒ ░▒ ▒▒ ▓▒ ▒▒▓ ▒ ▒▒ ▓▒█░▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒", 35 | " ░ ▒░ ░ ░░▒░ ░ ░ ▒ ░ ░ ▒ ░ ░▒ ▒░ ░ ▒ ▒ ▒ ▒▒ ░░ ░▒ ░ ░ ▒ ░▒░ ░", 36 | " ░ ░ ░░░ ░ ░ ▒ ░░ ░ ░░ ░ ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░", 37 | " ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░", 38 | "", 39 | "Made with <3 by Cerda. Repo: https://github.com/iamtakingithard/QuickDash", 40 | "", 41 | ]; 42 | 43 | fn main() { 44 | let result = actual_main(); 45 | exit(result); 46 | } 47 | 48 | fn actual_main() -> i32 { 49 | let opts = Commands::parse(); 50 | 51 | BANNER.iter().for_each(|line| println!("{}", line)); 52 | 53 | match opts.command { 54 | Mode::Create { path, file, force } => { 55 | let file = file.unwrap_or_else(|| default_file(&path)); 56 | match (force, Path::new(&file).exists()) { 57 | (true, _) | (_, false) => { 58 | // if this fails, it probably didn't exist 59 | let _ = remove_file(&file); 60 | 61 | let hashes = quickdash::operations::create_hashes( 62 | &path, 63 | opts.ignored_files, 64 | opts.algorithm, 65 | opts.depth, 66 | opts.follow_symlinks, 67 | opts.jobs, 68 | ); 69 | quickdash::operations::write_hashes(&file, opts.algorithm, hashes) 70 | } 71 | (false, true) => { 72 | eprintln!("File already exists. Use --force to overwrite."); 73 | 1 74 | } 75 | } 76 | } 77 | Mode::Verify { path, file } => { 78 | let hashes = quickdash::operations::create_hashes( 79 | &path, 80 | opts.ignored_files, 81 | opts.algorithm, 82 | opts.depth, 83 | opts.follow_symlinks, 84 | opts.jobs, 85 | ); 86 | let file = file.unwrap_or_else(|| default_file(&path)); 87 | match quickdash::operations::read_hashes(&file) { 88 | Ok(loaded_hashes) => { 89 | let compare_result = 90 | quickdash::operations::compare_hashes(&file, hashes, loaded_hashes); 91 | quickdash::operations::write_hash_comparison_results( 92 | &mut stdout(), 93 | &mut stderr(), 94 | compare_result, 95 | ) 96 | } 97 | Err(rval) => rval, 98 | } 99 | .exit_value() 100 | } 101 | } 102 | } 103 | 104 | fn default_file(path: &Path) -> PathBuf { 105 | let parent = path.file_stem().expect("Could not get directory name"); 106 | path.join(parent).with_extension("hash") 107 | } 108 | -------------------------------------------------------------------------------- /src/operations/compare.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use std::{collections::BTreeMap, path::Path}; 17 | 18 | use crate::utilities::{mul_str, vec_merge}; 19 | 20 | #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 21 | pub enum CompareResult { 22 | FileAdded(String), 23 | FileRemoved(String), 24 | FileIgnored(String), 25 | } 26 | 27 | #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 28 | pub enum CompareFileResult { 29 | FileMatches(String), 30 | FileDiffers { 31 | file: String, 32 | was_hash: String, 33 | new_hash: String, 34 | }, 35 | } 36 | 37 | #[derive(Debug, Clone, Hash, PartialEq, Eq, Copy)] 38 | pub enum CompareError { 39 | HashLengthDiffers { 40 | previous_len: usize, 41 | current_len: usize, 42 | }, 43 | } 44 | 45 | /// Compare two provided hashes 46 | pub fn compare_hashes( 47 | out_file: &Path, 48 | mut current_hashes: BTreeMap, 49 | mut loaded_hashes: BTreeMap, 50 | ) -> Result<(Vec, Vec), CompareError> { 51 | let current_hashes_value_len = current_hashes.iter().next().unwrap().1.len(); 52 | let loaded_hashes_value_len = loaded_hashes.iter().next().unwrap().1.len(); 53 | if current_hashes_value_len != loaded_hashes_value_len { 54 | return Err(CompareError::HashLengthDiffers { 55 | previous_len: loaded_hashes_value_len, 56 | current_len: current_hashes_value_len, 57 | }); 58 | } 59 | let placeholder_value = mul_str("-", current_hashes_value_len); 60 | let mut file_compare_results = Vec::new(); 61 | 62 | let key = out_file.to_string_lossy().to_string(); 63 | current_hashes.remove(&key); 64 | loaded_hashes.remove(&key); 65 | 66 | let remove_results = process_ignores( 67 | |key, _, other| !other.contains_key(key), 68 | CompareResult::FileAdded, 69 | CompareResult::FileRemoved, 70 | &mut current_hashes, 71 | &mut loaded_hashes, 72 | ); 73 | let ignore_results = process_ignores( 74 | |_, value, _| *value == placeholder_value, 75 | CompareResult::FileIgnored, 76 | CompareResult::FileIgnored, 77 | &mut current_hashes, 78 | &mut loaded_hashes, 79 | ); 80 | 81 | // By this point both hashes have the same keysets 82 | assert_eq!(current_hashes.len(), loaded_hashes.len()); 83 | 84 | if !current_hashes.is_empty() { 85 | for (key, loaded_value) in loaded_hashes { 86 | let current_value = ¤t_hashes[&key]; 87 | if *current_value == loaded_value { 88 | file_compare_results.push(CompareFileResult::FileMatches(key)); 89 | } else { 90 | file_compare_results.push(CompareFileResult::FileDiffers { 91 | file: key, 92 | was_hash: loaded_value, 93 | new_hash: current_value.clone(), 94 | }); 95 | } 96 | } 97 | } 98 | 99 | Ok(( 100 | vec_merge(remove_results, ignore_results), 101 | file_compare_results, 102 | )) 103 | } 104 | 105 | fn process_ignores( 106 | f: F, 107 | cres: Rc, 108 | lres: Rl, 109 | ch: &mut BTreeMap, 110 | lh: &mut BTreeMap, 111 | ) -> Vec 112 | where 113 | F: Fn(&str, &str, &BTreeMap) -> bool, 114 | Rc: Fn(String) -> CompareResult, 115 | Rl: Fn(String) -> CompareResult, 116 | { 117 | let mut results = Vec::new(); 118 | let mut keys_to_remove = Vec::new(); 119 | 120 | process_ignores_iter(&f, &cres, ch, lh, &mut keys_to_remove, &mut results); 121 | process_ignores_iter(&f, &lres, lh, ch, &mut keys_to_remove, &mut results); 122 | 123 | for key in keys_to_remove { 124 | ch.remove(&key); 125 | lh.remove(&key); 126 | } 127 | 128 | results 129 | } 130 | 131 | fn process_ignores_iter( 132 | f: &F, 133 | res: &R, 134 | curr: &BTreeMap, 135 | other: &BTreeMap, 136 | keys_to_remove: &mut Vec, 137 | results: &mut Vec, 138 | ) where 139 | F: Fn(&str, &str, &BTreeMap) -> bool, 140 | R: Fn(String) -> CompareResult, 141 | { 142 | for (key, value) in curr { 143 | if f(key, value, other) { 144 | results.push(res(key.clone())); 145 | keys_to_remove.push(key.clone()); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/operations/mod.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | //! Main functions doing actual work. 17 | //! 18 | //! 19 | //! Use `create_hashes()` to prepare the hashes for a path. 20 | //! 21 | //! Then use `write_hashes()` to save it to disk, or `read_hashes()` to get the 22 | //! saved hashes, them with `compare_hashes()` and print them with 23 | //! `write_hash_comparison_results()`. 24 | 25 | mod compare; 26 | mod write; 27 | 28 | use std::{ 29 | collections::BTreeMap, 30 | fs::File, 31 | io::{BufRead, BufReader, Write}, 32 | path::Path, 33 | time::Duration, 34 | }; 35 | 36 | use indicatif::{ParallelProgressIterator, ProgressBar, ProgressStyle}; 37 | use once_cell::sync::Lazy; 38 | use rayon::{ 39 | ThreadPoolBuilder, 40 | iter::{IntoParallelRefIterator, ParallelIterator}, 41 | }; 42 | use regex::Regex; 43 | use tabwriter::TabWriter; 44 | use walkdir::{DirEntry, WalkDir}; 45 | 46 | pub use self::{compare::*, write::*}; 47 | use crate::{ 48 | Algorithm, Error, hash_file, 49 | utilities::{mul_str, relative_name}, 50 | }; 51 | 52 | static SPINNER_STRINGS: [&str; 10] = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; 53 | 54 | /// Create subpath->hash mappings for a given path using a given algorithm up to 55 | /// a given depth. 56 | pub fn create_hashes( 57 | path: &Path, 58 | ignored_files: Vec, 59 | algo: Algorithm, 60 | depth: Option, 61 | follow_symlinks: bool, 62 | jobs: usize, 63 | ) -> BTreeMap { 64 | let mut walkdir = WalkDir::new(path).follow_links(follow_symlinks); 65 | if let Some(depth) = depth { 66 | walkdir = walkdir.max_depth(depth + 1); 67 | } 68 | 69 | let pb_style = ProgressStyle::default_bar() 70 | .template("{prefix:.bold.dim} {spinner} {wide_bar} {pos:>7}/{len:7} ETA: {eta} - {msg}") 71 | .unwrap() 72 | .tick_strings(&SPINNER_STRINGS); 73 | 74 | let pb = ProgressBar::new_spinner(); 75 | pb.set_style(pb_style); 76 | 77 | ThreadPoolBuilder::new() 78 | .num_threads(jobs) 79 | .build_global() 80 | .unwrap(); 81 | 82 | let mut hashes = BTreeMap::new(); 83 | 84 | pb.enable_steady_tick(Duration::from_millis(80)); 85 | pb.set_message("Finding files to hash..."); 86 | let mut files: Vec = walkdir 87 | .into_iter() 88 | .filter_entry(|e: &walkdir::DirEntry| { 89 | let filename = relative_name(path, e.path()); 90 | match (ignored_files.contains(&filename), e.file_type().is_file()) { 91 | (true, true) => { 92 | hashes.insert(mul_str("-", algo.hexlen()), filename); 93 | false 94 | } 95 | (true, false) => false, 96 | _ => true, 97 | } 98 | }) 99 | .flatten() 100 | .filter(|e| e.file_type().is_file()) 101 | .collect(); 102 | 103 | optimize_file_order(&mut files); 104 | 105 | pb.reset(); 106 | pb.set_length(files.len() as u64); 107 | pb.set_message("Hashing files..."); 108 | 109 | let mut result: BTreeMap = files 110 | .par_iter() 111 | .progress_with(pb) 112 | .map(|e| { 113 | let value = hash_file(algo, e.path()); 114 | let filename = relative_name(path, e.path()); 115 | (filename, value) 116 | }) 117 | .collect(); 118 | hashes.append(&mut result); 119 | hashes 120 | } 121 | 122 | #[cfg(target_os = "linux")] 123 | fn optimize_file_order(dirs: &mut [DirEntry]) { 124 | use walkdir::DirEntryExt; 125 | dirs.sort_by(|a, b| { 126 | let a_inode = a.ino(); 127 | let b_inode = b.ino(); 128 | a_inode.cmp(&b_inode) 129 | }); 130 | } 131 | 132 | #[cfg(not(target_os = "linux"))] 133 | fn optimize_file_order(_dirs: &mut [DirEntry]) {} 134 | 135 | /// Serialise the specified hashes to the specified output file. 136 | pub fn write_hashes(out_file: &Path, algo: Algorithm, mut hashes: BTreeMap) -> i32 { 137 | let file = File::create(&out_file).unwrap(); 138 | let mut out = TabWriter::new(file); 139 | 140 | hashes.insert( 141 | out_file.to_string_lossy().to_string(), 142 | mul_str("-", algo.hexlen()), 143 | ); 144 | for (fname, hash) in hashes { 145 | writeln!(&mut out, "{} {}", hash, fname).unwrap(); 146 | } 147 | 148 | out.flush().expect("Failed to flush output file"); 149 | 0 150 | } 151 | 152 | /// Read uppercased hashes with `write_hashes()` from the specified path or fail 153 | /// with line numbers not matching pattern. 154 | pub fn read_hashes(file: &Path) -> Result, Error> { 155 | let mut hashes = BTreeMap::new(); 156 | 157 | let in_file = BufReader::new(File::open(&file).unwrap()); 158 | for line in in_file.lines().map(Result::unwrap) { 159 | try_contains(&line, &mut hashes)?; 160 | } 161 | 162 | Ok(hashes) 163 | } 164 | 165 | fn try_contains(line: &str, hashes: &mut BTreeMap) -> Result<(), Error> { 166 | if line.is_empty() { 167 | return Err(Error::HashesFileParsingFailure); 168 | } 169 | 170 | static LINE_RGX1: Lazy = 171 | Lazy::new(|| Regex::new(r"(?i)^([[:xdigit:]-]+)\s{2,}(.+?)$").unwrap()); 172 | 173 | static LINE_RGX2: Lazy = 174 | Lazy::new(|| Regex::new(r"(?i)^(.+?)\t{0,}\s{1,}([[:xdigit:]-]+)$").unwrap()); 175 | 176 | if let Some(captures) = LINE_RGX1.captures(line) { 177 | hashes.insert(captures[2].to_string(), captures[1].to_uppercase()); 178 | return Ok(()); 179 | } 180 | if let Some(captures) = LINE_RGX2.captures(line) { 181 | hashes.insert(captures[1].to_string(), captures[2].to_uppercase()); 182 | return Ok(()); 183 | } 184 | Err(Error::HashesFileParsingFailure) 185 | } 186 | -------------------------------------------------------------------------------- /src/operations/write.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use std::io::Write; 17 | 18 | use super::{CompareError, CompareFileResult, CompareResult}; 19 | use crate::{Error, utilities::mul_str}; 20 | 21 | /// Write hash comparison results to the output streams in a human-consumable 22 | /// format 23 | pub fn write_hash_comparison_results( 24 | output: &mut Wo, 25 | error: &mut We, 26 | results: Result<(Vec, Vec), CompareError>, 27 | ) -> Error { 28 | let result = match results { 29 | Ok((mut compare_results, mut file_compare_results)) => { 30 | compare_results.sort(); 31 | file_compare_results.sort(); 32 | 33 | for res in &compare_results { 34 | match *res { 35 | CompareResult::FileAdded(ref file) => { 36 | write_compare_result(output, "File added: ", file) 37 | } 38 | CompareResult::FileRemoved(ref file) => { 39 | write_compare_result(output, "File removed: ", file) 40 | } 41 | CompareResult::FileIgnored(ref file) => { 42 | write_compare_result(output, "File ignored, skipping: ", file) 43 | } 44 | } 45 | } 46 | 47 | if file_compare_results.is_empty() && compare_results.is_empty() { 48 | writeln!(output, "No files left to verify").expect("io err"); 49 | Error::NoError 50 | } else if file_compare_results.is_empty() { 51 | writeln!(output, "No files to verify").expect("io err"); 52 | Error::NoError 53 | } else { 54 | if !compare_results.is_empty() { 55 | writeln!(output).unwrap(); 56 | } 57 | 58 | let mut differed_n = 0; 59 | for fres in &file_compare_results { 60 | match *fres { 61 | CompareFileResult::FileMatches(ref file) => { 62 | write_file_result_match(output, file) 63 | } 64 | CompareFileResult::FileDiffers { 65 | ref file, 66 | ref was_hash, 67 | ref new_hash, 68 | } => { 69 | write_file_result_diff(output, file, was_hash, new_hash); 70 | differed_n += 1; 71 | } 72 | } 73 | } 74 | 75 | match differed_n { 76 | 0 => Error::NoError, 77 | n => Error::NFilesDiffer(n), 78 | } 79 | } 80 | } 81 | Err(CompareError::HashLengthDiffers { 82 | previous_len, 83 | current_len, 84 | }) => { 85 | let previous_len_len = format!("{}", previous_len).len(); 86 | let current_len_len = format!("{}", current_len).len(); 87 | 88 | if previous_len_len + current_len_len + 47 <= 80 { 89 | writeln!( 90 | error, 91 | "Hash lengths do not match; selected: {}, loaded: {}", 92 | current_len, previous_len 93 | ) 94 | .unwrap(); 95 | } else { 96 | writeln!(error, "Hash lengths do not match;").unwrap(); 97 | if previous_len_len + current_len_len + 20 <= 80 { 98 | writeln!(error, "selected: {}, loaded: {}", current_len, previous_len).unwrap(); 99 | } else { 100 | writeln!(error, "Selected: {}", current_len).unwrap(); 101 | writeln!(error, "Loaded : {}", previous_len).unwrap(); 102 | } 103 | } 104 | 105 | Error::HashLengthDiffers 106 | } 107 | }; 108 | 109 | output.flush().unwrap(); 110 | error.flush().unwrap(); 111 | 112 | result 113 | } 114 | 115 | fn write_compare_result(out: &mut W, pre: &str, fname: &str) { 116 | write_result(out, pre, fname, 2, true) 117 | } 118 | 119 | fn write_result(out: &mut W, pre: &str, fname: &str, fname_indent: usize, quote: bool) { 120 | if pre.len() + quote as usize + fname.len() + quote as usize <= 80 { 121 | let quote_s = if quote { "\"" } else { "" }; 122 | writeln!(out, "{}{2}{}{2}", pre, fname, quote_s).unwrap(); 123 | } else { 124 | writeln!(out, "{}", pre).unwrap(); 125 | if fname.len() <= 80 - fname_indent { 126 | writeln!(out, " {}", fname).unwrap(); 127 | } else { 128 | let indent = mul_str(" ", fname_indent); 129 | for fname_chunk in fname 130 | .chars() 131 | .collect::>() 132 | .chunks(80 - fname_indent) 133 | .map(|cc| cc.iter().cloned().collect::()) 134 | { 135 | writeln!(out, "{}{}", indent, fname_chunk).unwrap(); 136 | } 137 | } 138 | } 139 | } 140 | 141 | fn write_file_result_match(out: &mut W, fname: &str) { 142 | if 15 + fname.len() <= 80 { 143 | writeln!(out, "File \"{}\" matches", fname).unwrap(); 144 | } else { 145 | write_compare_result(out, "File matches: ", fname); 146 | } 147 | } 148 | 149 | fn write_file_result_diff(out: &mut W, fname: &str, lhash: &str, chash: &str) { 150 | if 21 + fname.len() <= 80 { 151 | writeln!(out, "File \"{}\" doesn't match", fname).unwrap(); 152 | } else { 153 | write_result(out, "File doesn't match: ", fname, 4, true); 154 | } 155 | 156 | write_result(out, " Was: ", lhash, 4, false); 157 | write_result(out, " Is : ", chash, 4, false); 158 | } 159 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | use std::path::PathBuf; 17 | 18 | use clap::{Parser, Subcommand}; 19 | 20 | use crate::Algorithm; 21 | 22 | #[derive(Parser)] 23 | #[command( 24 | name = "QuickDash", 25 | version, 26 | about, 27 | long_about = "A modern alternative to QuickSFV using Rust. Made with <3 by Cerda." 28 | )] 29 | pub struct Commands { 30 | /// Hashing algorithm to use. 31 | #[arg(value_enum, short, long, default_value = "blake3")] 32 | pub algorithm: Algorithm, 33 | /// Max recursion depth. Infinite if None. Default: `0` 34 | #[arg(short, long)] 35 | pub depth: Option, 36 | /// Whether to recurse down symlinks. Default: `true` 37 | #[arg(long)] 38 | pub follow_symlinks: bool, 39 | /// Files/directories to ignore. Default: none 40 | #[arg(short, long)] 41 | pub ignored_files: Vec, 42 | /// # of threads used for hashing. 43 | #[arg(short, long, default_value_t = 0)] 44 | pub jobs: usize, 45 | /// Whether to verify or create hashes. Default: Verify 46 | #[command(subcommand)] 47 | pub command: Mode, 48 | } 49 | 50 | #[derive(Subcommand)] 51 | pub enum Mode { 52 | /// Create a hash file 53 | Create { 54 | /// Directory to hash. Default: current directory 55 | #[arg(default_value = ".")] 56 | path: PathBuf, 57 | /// Output filename. Default: `directory_name.hash"` 58 | #[arg(long)] 59 | file: Option, 60 | #[arg(short, long)] 61 | force: bool, 62 | }, 63 | /// Verify a hash file 64 | Verify { 65 | /// Directory to verify. Default: current directory 66 | #[arg(default_value = ".")] 67 | path: PathBuf, 68 | /// Input filename. Default: `directory_name.hash` 69 | #[arg(long)] 70 | file: Option, 71 | }, 72 | } 73 | -------------------------------------------------------------------------------- /src/utilities.rs: -------------------------------------------------------------------------------- 1 | /* Copyright [2025] [Cerda] 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | //! Module containing various utility functions 17 | 18 | use std::path::Path; 19 | 20 | /// Merges two `Vec`s. 21 | /// 22 | /// # Examples 23 | /// 24 | /// ``` 25 | /// let vec1 = vec![0]; 26 | /// let vec2 = vec![1]; 27 | /// 28 | /// assert_eq!(quickdash::utilities::vec_merge(vec1, vec2), vec![0, 1]); 29 | /// ``` 30 | pub fn vec_merge(mut lhs: Vec, rhs: Vec) -> Vec { 31 | lhs.extend(rhs); 32 | lhs 33 | } 34 | 35 | /// Create a string consisting of `n` repetitions of `what`. 36 | /// 37 | /// # Examples 38 | /// 39 | /// ``` 40 | /// assert_eq!( 41 | /// quickdash::utilities::mul_str("LOL! ", 3), 42 | /// "LOL! LOL! LOL! ".to_string() 43 | /// ); 44 | /// ``` 45 | pub fn mul_str(what: &str, n: usize) -> String { 46 | what.repeat(n) 47 | } 48 | 49 | /// Create a user-usable path to `what` from `prefix`. 50 | /// 51 | /// # Examples 52 | /// 53 | /// ``` 54 | /// # use std::path::Path; 55 | /// assert_eq!( 56 | /// quickdash::utilities::relative_name(Path::new("/usr"), Path::new("/usr/bin/quickdash")), 57 | /// "bin/quickdash".to_string() 58 | /// ); 59 | /// ``` 60 | pub fn relative_name(prefix: &Path, what: &Path) -> String { 61 | what.strip_prefix(prefix) 62 | .unwrap() 63 | .to_str() 64 | .unwrap() 65 | .replace('\\', "/") 66 | } 67 | -------------------------------------------------------------------------------- /tests/algo.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use quickdash::Algorithm; 4 | 5 | #[test] 6 | fn from_str() { 7 | for a in &[ 8 | ("sha1", Algorithm::SHA1), 9 | ("sha-224", Algorithm::SHA2224), 10 | ("sha-256", Algorithm::SHA2256), 11 | ("sha-384", Algorithm::SHA2384), 12 | ("sha-512", Algorithm::SHA2512), 13 | ("sha3-224", Algorithm::SHA3224), 14 | ("sha3-256", Algorithm::SHA3256), 15 | ("sha3-384", Algorithm::SHA3384), 16 | ("sha3-512", Algorithm::SHA3512), 17 | ("blake2b", Algorithm::BLAKE2B), 18 | ("blake2s", Algorithm::BLAKE2S), 19 | ("blake3", Algorithm::BLAKE3), 20 | ("xxh3", Algorithm::XXH3), 21 | ("xxh64", Algorithm::XXH64), 22 | ("xxh32", Algorithm::XXH32), 23 | ("crc32", Algorithm::CRC32), 24 | ("md5", Algorithm::MD5), 25 | ("whirlpool", Algorithm::WhirlPool), 26 | ] { 27 | assert_eq!(Algorithm::from_str(a.0).unwrap(), a.1); 28 | } 29 | } 30 | --------------------------------------------------------------------------------