--- /dev/null
+/target
+**/*.rs.bk
\ No newline at end of file
--- /dev/null
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "aho-corasick"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "atty"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.53 (registry+https://github.com/rust-lang/crates.io-index)",
+ "termion 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "autocfg"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "bitflags"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "block"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "clap"
+version = "2.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+ "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "clipboard"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "clipboard-win 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "objc 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "objc-foundation 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "objc_id 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "x11-clipboard 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "clipboard-win"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "fuchsia-cprng"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "gcc"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "hashgood"
+version = "0.1.0"
+dependencies = [
+ "clipboard 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",
+ "structopt 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "heck"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "hex"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "lazy_static"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "libc"
+version = "0.2.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "log"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "malloc_buf"
+version = "0.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.53 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "memchr"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "numtoa"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "objc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "objc-foundation"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "objc 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "objc_id 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "objc_id"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "objc 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rand"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.53 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rand"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.53 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "rdrand"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.1.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "redox_termios"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "regex"
+version = "1.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex-syntax 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rust-crypto"
+version = "0.2.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.53 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rustc-serialize"
+version = "0.3.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "structopt"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "structopt-derive 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "structopt-derive"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro-error 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "termion"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.53 (registry+https://github.com/rust-lang/crates.io-index)",
+ "numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "thread_local"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "time"
+version = "0.1.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.53 (registry+https://github.com/rust-lang/crates.io-index)",
+ "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "ucd-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "utf8-ranges"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "vec_map"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "winapi"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
+name = "wincolor"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "x11-clipboard"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "xcb 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "xcb"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.53 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[metadata]
+"checksum aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e6f484ae0c99fec2e858eb6134949117399f222608d84cadb3f58c1f97c2364c"
+"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
+"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
+"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
+"checksum block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
+"checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4"
+"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
+"checksum clipboard 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7"
+"checksum clipboard-win 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "289da2fc09ab964a4948a63287c94fcb4698fa823c46da84c3792928c9d36110"
+"checksum crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "acec9a3b0b3559f15aee4f90746c4e5e293b701c0f7d3925d24e01645267b68c"
+"checksum crossbeam-utils 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce446db02cdc3165b94ae73111e570793400d0794e46125cc4056c81cbb039f4"
+"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
+"checksum gcc 0.3.55 (registry+https://github.com/rust-lang/crates.io-index)" = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2"
+"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
+"checksum hex 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "023b39be39e3a2da62a94feb433e91e8bcd37676fbc8bea371daf52b7a769a3e"
+"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
+"checksum libc 0.2.53 (registry+https://github.com/rust-lang/crates.io-index)" = "ec350a9417dfd244dc9a6c4a71e13895a4db6b92f0b106f07ebbc3f3bc580cee"
+"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6"
+"checksum malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
+"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
+"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
+"checksum objc 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "31d20fd2b37e07cf5125be68357b588672e8cefe9a96f8c17a9d46053b3e590d"
+"checksum objc-foundation 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
+"checksum objc_id 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
+"checksum proc-macro-error 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aeccfe4d5d8ea175d5f0e4a2ad0637e0f4121d63bd99d356fb1f39ab2e7c6097"
+"checksum proc-macro2 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9c9e470a8dc4aeae2dee2f335e8f533e2d4b347e1434e5671afc49b054592f27"
+"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe"
+"checksum rand 0.3.23 (registry+https://github.com/rust-lang/crates.io-index)" = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c"
+"checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
+"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
+"checksum rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0e7a549d590831370895ab7ba4ea0c1b6b011d106b5ff2da6eee112615e6dc0"
+"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
+"checksum redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)" = "12229c14a0f65c4f1cb046a3b52047cdd9da1f4b30f8a39c5063c8bae515e252"
+"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
+"checksum regex 1.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8f0a0bcab2fd7d1d7c54fa9eae6f43eddeb9ce2e7352f8518a814a4f65d60c58"
+"checksum regex-syntax 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dcfd8681eebe297b81d98498869d4aae052137651ad7b96822f09ceb690d0a96"
+"checksum rust-crypto 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a"
+"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
+"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+"checksum structopt 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c167b61c7d4c126927f5346a4327ce20abf8a186b8041bbeb1ce49e5db49587b"
+"checksum structopt-derive 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "519621841414165d2ad0d4c92be8f41844203f2b67e245f9345a5a12d40c69d7"
+"checksum syn 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "661641ea2aa15845cddeb97dad000d22070bb5c1fb456b96c1cba883ec691e92"
+"checksum termcolor 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "96d6098003bde162e4277c70665bd87c326f5a0c3f3fbfb285787fa482d54e6e"
+"checksum termion 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dde0593aeb8d47accea5392b39350015b5eccb12c0d98044d856983d89548dea"
+"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
+"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f"
+"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86"
+"checksum unicode-segmentation 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "aa6024fc12ddfd1c6dbc14a80fa2324d4568849869b779f6bd37e5e4c03344d1"
+"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526"
+"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
+"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737"
+"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
+"checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770"
+"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+"checksum winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7168bab6e1daee33b4557efd0e95d5ca70a03706d39fa5f3fe7a236f584b03c9"
+"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+"checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba"
+"checksum x11-clipboard 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3a77356335a1398267e15a7c1d5fa1c8d3fdb3e5ba2e381407d74482c29587d3"
+"checksum xcb 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de"
--- /dev/null
+[package]
+name = "hashgood"
+version = "0.1.0"
+authors = ["Thomas Karpiniec <tom.karpiniec@outlook.com>"]
+edition = "2018"
+
+[dependencies]
+structopt = "0.3.4"
+hex = "0.4.0"
+rust-crypto = "0.2.36"
+crossbeam-channel = "0.4.0"
+termcolor = "1.0.5"
+clipboard = "0.5.0"
+regex = "1"
--- /dev/null
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
--- /dev/null
+# hashgood
+CLI tool for easily verifying a downloaded file's checksum
--- /dev/null
+use super::Algorithm;
+use crossbeam_channel::bounded;
+use crossbeam_channel::Receiver;
+use crypto::digest::Digest;
+use crypto::md5::Md5;
+use crypto::sha1::Sha1;
+use crypto::sha2::Sha256;
+use std::error::Error;
+use std::fs::File;
+use std::io::prelude::*;
+use std::path::PathBuf;
+use std::sync::Arc;
+use std::thread;
+use std::thread::JoinHandle;
+
+pub type CalculateResult = Result<Vec<(Algorithm, Vec<u8>)>, Box<dyn Error>>;
+
+/// For a given path to the input (may be "-" for STDIN), try to obtain a reader for the data within it.
+pub fn get_input_reader(input: &PathBuf) -> Result<Box<dyn Read>, Box<dyn Error>> {
+ if input.to_str() == Some("-") {
+ // Special case: standard input
+ return Ok(Box::new(std::io::stdin()));
+ }
+ Ok(Box::new(File::open(input)?))
+}
+
+/// For the given input stream, calculate all requested digest types
+pub fn create_digests(algorithms: &[Algorithm], mut input: Box<dyn Read>) -> CalculateResult {
+ let mut senders = vec![];
+ let mut handles = vec![];
+
+ if algorithms.contains(&Algorithm::Md5) {
+ let (s, r) = bounded::<Arc<Vec<u8>>>(1);
+ senders.push(s);
+ handles.push(md5_digest(r));
+ }
+ if algorithms.contains(&Algorithm::Sha1) {
+ let (s, r) = bounded::<Arc<Vec<u8>>>(1);
+ senders.push(s);
+ handles.push(sha1_digest(r));
+ }
+ if algorithms.contains(&Algorithm::Sha256) {
+ let (s, r) = bounded::<Arc<Vec<u8>>>(1);
+ senders.push(s);
+ handles.push(sha256_digest(r));
+ }
+
+ // 64 KB chunks will be read from the input at 64 KB and supplied to all hashing threads at once
+ // Right now that could be up to three threads. If CPU-bound, the other threads will mostly block while the slowest one finishes
+ const BUF_SIZE: usize = 1024 * 64;
+ let mut buf = [0; BUF_SIZE];
+ while let Ok(size) = input.read(&mut buf) {
+ if size == 0 {
+ break;
+ } else {
+ // Create a shared read-only copy for the hashers to take as input
+ // buf is freed up for more reading
+ let chunk = Arc::new(buf[0..size].to_vec());
+ for s in &senders {
+ s.send(chunk.clone())?;
+ }
+ }
+ }
+ drop(senders);
+ // Once all data has been sent we just have to wait for the digests to fall out
+ Ok(handles.into_iter().map(|h| h.join().unwrap()).collect())
+}
+
+/// Calculate the md5 digest of some data on the given channel
+fn md5_digest(rx: Receiver<Arc<Vec<u8>>>) -> JoinHandle<(Algorithm, Vec<u8>)> {
+ thread::spawn(move || {
+ let mut md5 = Md5::new();
+ while let Ok(chunk) = rx.recv() {
+ md5.input(&chunk);
+ }
+ let mut result = [0; 16];
+ md5.result(&mut result);
+ (Algorithm::Md5, result.to_vec())
+ })
+}
+
+/// Calculate the sha1 digest of some data on the given channel
+fn sha1_digest(rx: Receiver<Arc<Vec<u8>>>) -> JoinHandle<(Algorithm, Vec<u8>)> {
+ thread::spawn(move || {
+ let mut sha1 = Sha1::new();
+ while let Ok(chunk) = rx.recv() {
+ sha1.input(&chunk);
+ }
+ let mut result = [0; 20];
+ sha1.result(&mut result);
+ (Algorithm::Sha1, result.to_vec())
+ })
+}
+
+/// Calculate the sha256 digest of some data on the given channel
+fn sha256_digest(rx: Receiver<Arc<Vec<u8>>>) -> JoinHandle<(Algorithm, Vec<u8>)> {
+ thread::spawn(move || {
+ let mut sha256 = Sha256::new();
+ while let Ok(chunk) = rx.recv() {
+ sha256.input(&chunk);
+ }
+ let mut result = [0; 32];
+ sha256.result(&mut result);
+ (Algorithm::Sha256, result.to_vec())
+ })
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::io::Cursor;
+
+ const SMALL_DATA: [u8; 10] = ['A' as u8; 10];
+ // python3 -c 'print ("A"*10, end="", flush=True)' | md5sum
+ const SMALL_DATA_MD5: &'static str = "16c52c6e8326c071da771e66dc6e9e57";
+ // python3 -c 'print ("A"*10, end="", flush=True)' | sha1sum
+ const SMALL_DATA_SHA1: &'static str = "c71613a7386fd67995708464bf0223c0d78225c4";
+ // python3 -c 'print ("A"*10, end="", flush=True)' | sha256sum
+ const SMALL_DATA_SHA256: &'static str =
+ "1d65bf29403e4fb1767522a107c827b8884d16640cf0e3b18c4c1dd107e0d49d";
+
+ const LARGE_DATA: [u8; 1_000_000] = ['B' as u8; 1_000_000];
+ // python3 -c 'print ("B"*1000000, end="", flush=True)' | md5sum
+ const LARGE_DATA_MD5: &'static str = "9171f6d67a87ca649a702434a03458a1";
+ // python3 -c 'print ("B"*1000000, end="", flush=True)' | sha1sum
+ const LARGE_DATA_SHA1: &'static str = "cfae4cebfd01884111bdede7cf983626bb249c94";
+ // python3 -c 'print ("B"*1000000, end="", flush=True)' | sha256sum
+ const LARGE_DATA_SHA256: &'static str =
+ "b9193853f7798e92e2f6b82eda336fa7d6fc0fa90fdefe665f372b0bad8cdf8c";
+
+ fn verify_digest(alg: Algorithm, data: &'static [u8], hash: &str) {
+ let reader = Cursor::new(&*data);
+ let digests = create_digests(&[alg], Box::new(reader)).unwrap();
+ assert_eq!(digests.len(), 1);
+ assert_eq!(digests[0], (alg, hex::decode(hash).unwrap()));
+ }
+
+ /// Assert that digests for all algorithms are calculated correctly for a small piece
+ /// of test data (single block).
+ #[test]
+ fn small_digests() {
+ verify_digest(Algorithm::Md5, &SMALL_DATA, &SMALL_DATA_MD5);
+ verify_digest(Algorithm::Sha1, &SMALL_DATA, &SMALL_DATA_SHA1);
+ verify_digest(Algorithm::Sha256, &SMALL_DATA, &SMALL_DATA_SHA256);
+ }
+
+ /// Assert that digests for all algorithms are calculated correctly for a large piece
+ /// of test data. For our purposes, "large" means that it spans several of the 64 KB
+ /// blocks used to break up the input processing. Using one million bytes instead of
+ /// 1 MiB means that the final block will be slightly smaller than the others.
+ #[test]
+ fn large_digests() {
+ verify_digest(Algorithm::Md5, &LARGE_DATA, &LARGE_DATA_MD5);
+ verify_digest(Algorithm::Sha1, &LARGE_DATA, &LARGE_DATA_SHA1);
+ verify_digest(Algorithm::Sha256, &LARGE_DATA, &LARGE_DATA_SHA256);
+ }
+}
--- /dev/null
+use super::{Algorithm, CandidateHash, Hash, MatchLevel, MessageLevel, VerificationSource};
+use std::borrow::Borrow;
+use std::error::Error;
+use std::io::Write;
+use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
+
+pub type PrintResult = Result<(), Box<dyn Error>>;
+
+fn filename_display(filename: &str) -> &str {
+ if filename == "-" {
+ return "standard input";
+ }
+ filename
+}
+
+fn get_stdout(no_colour: bool) -> StandardStream {
+ if no_colour {
+ StandardStream::stdout(ColorChoice::Never)
+ } else {
+ StandardStream::stdout(ColorChoice::Always)
+ }
+}
+
+fn write_filename(mut stdout: &mut StandardStream, filename: &str) -> PrintResult {
+ stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
+ write!(&mut stdout, "{}", filename_display(filename))?;
+ stdout.reset()?;
+ Ok(())
+}
+
+fn write_algorithm(mut stdout: &mut StandardStream, alg: Algorithm) -> PrintResult {
+ match alg {
+ Algorithm::Md5 => {
+ stdout.set_color(ColorSpec::new().set_fg(Some(Color::Magenta)))?;
+ write!(&mut stdout, "MD5")?;
+ }
+ Algorithm::Sha1 => {
+ stdout.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)))?;
+ write!(&mut stdout, "SHA-1")?;
+ }
+ Algorithm::Sha256 => {
+ stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
+ write!(&mut stdout, "SHA-256")?;
+ }
+ }
+ stdout.reset()?;
+ Ok(())
+}
+
+fn print_hex_compare(print: &str, against: &str, mut stdout: &mut StandardStream) -> PrintResult {
+ for (p, a) in print.chars().zip(against.chars()) {
+ if p == a {
+ stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
+ } else {
+ stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
+ }
+ write!(&mut stdout, "{}", p)?;
+ }
+ stdout.reset()?;
+ writeln!(&mut stdout)?;
+ Ok(())
+}
+
+fn write_source(
+ mut stdout: &mut StandardStream,
+ verify_source: &VerificationSource,
+ candidate_filename: &Option<String>,
+) -> PrintResult {
+ stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
+ match &verify_source {
+ VerificationSource::CommandArgument => {
+ writeln!(&mut stdout, "command line argument")?;
+ }
+ VerificationSource::Clipboard => {
+ writeln!(&mut stdout, "pasted from clipboard")?;
+ }
+ VerificationSource::RawFile(raw_path) => match raw_path.to_string_lossy().borrow() {
+ "-" => {
+ writeln!(&mut stdout, "from standard input")?;
+ }
+ path => {
+ writeln!(&mut stdout, "from file '{}' containing raw hash", path)?;
+ }
+ },
+ VerificationSource::DigestsFile(digest_path) => {
+ match digest_path.to_string_lossy().borrow() {
+ "-" => {
+ writeln!(
+ &mut stdout,
+ "'{}' from digests on standard input",
+ candidate_filename.as_ref().unwrap()
+ )?;
+ }
+ path => {
+ writeln!(
+ &mut stdout,
+ "'{}' in digests file '{}'",
+ candidate_filename.as_ref().unwrap(),
+ path
+ )?;
+ }
+ }
+ }
+ }
+ stdout.reset()?;
+ Ok(())
+}
+
+pub fn print_hash(
+ hash: &Hash,
+ verify_hash: Option<&CandidateHash>,
+ verify_source: Option<&VerificationSource>,
+ no_colour: bool,
+) -> PrintResult {
+ let mut stdout = get_stdout(no_colour);
+
+ write_filename(&mut stdout, &hash.filename)?;
+ write!(&mut stdout, " / ")?;
+ write_algorithm(&mut stdout, hash.alg)?;
+ writeln!(&mut stdout)?;
+
+ // Handle basic case first - nothing to compare it to
+ let hash_hex = hex::encode(&hash.bytes);
+ let verify_hash = match verify_hash {
+ None => {
+ write!(&mut stdout, "{}\n\n", hash_hex)?;
+ return Ok(());
+ }
+ Some(verify_hash) => verify_hash,
+ };
+ let other_hex = hex::encode(&verify_hash.bytes);
+
+ // Do a top-to-bottom comparison
+ print_hex_compare(&hash_hex, &other_hex, &mut stdout)?;
+ print_hex_compare(&other_hex, &hash_hex, &mut stdout)?;
+
+ // Show the source of our hash
+ if let Some(source) = verify_source {
+ write_source(&mut stdout, source, &verify_hash.filename)?;
+ }
+
+ writeln!(&mut stdout)?;
+ Ok(())
+}
+
+pub fn print_messages(messages: Vec<(MessageLevel, String)>, no_colour: bool) -> PrintResult {
+ let mut stdout = get_stdout(no_colour);
+
+ for (level, msg) in &messages {
+ match level {
+ MessageLevel::Error => {
+ stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
+ write!(&mut stdout, "(error) ")?;
+ }
+ MessageLevel::Warning => {
+ stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
+ write!(&mut stdout, "(warning) ")?;
+ }
+ MessageLevel::Note => {
+ stdout.set_color(ColorSpec::new().set_fg(Some(Color::Cyan)))?;
+ write!(&mut stdout, "(note) ")?;
+ }
+ }
+ stdout.reset()?;
+ writeln!(&mut stdout, "{}", msg)?;
+ }
+ if !messages.is_empty() {
+ writeln!(&mut stdout)?
+ }
+
+ Ok(())
+}
+
+pub fn print_match_level(match_level: MatchLevel, no_colour: bool) -> PrintResult {
+ let mut stdout = get_stdout(no_colour);
+ write!(&mut stdout, "Result: ")?;
+ match match_level {
+ MatchLevel::Ok => {
+ stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
+ writeln!(&mut stdout, "OK")?;
+ }
+ MatchLevel::Maybe => {
+ stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
+ writeln!(&mut stdout, "MAYBE")?;
+ }
+ MatchLevel::Fail => {
+ stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
+ writeln!(&mut stdout, "FAIL")?;
+ }
+ }
+ stdout.reset()?;
+ Ok(())
+}
--- /dev/null
+use std::error::Error;
+use std::path::PathBuf;
+use std::process;
+use structopt::StructOpt;
+
+/// Calculate digests for given input data
+mod calculate;
+
+/// Display output nicely in the terminal
+mod display;
+
+/// Collect candidate hashes based on options and match them against a calculated hash
+mod verify;
+
+#[derive(StructOpt)]
+#[structopt(name = "hashgood")]
+pub struct Opt {
+ /// Read the hash from the clipboard
+ #[structopt(short = "p", long = "paste")]
+ paste: bool,
+
+ /// Disable ANSI colours in output
+ #[structopt(short = "C", long = "no-colour")]
+ no_colour: bool,
+
+ /// A file containing the hash to verify. It can either be a raw hash or a SHASUMS-style listing. Use `-` for standard input.
+ #[structopt(short = "c", long = "check", parse(from_os_str))]
+ hash_file: Option<PathBuf>,
+
+ /// The file to be verified or `-` for standard input
+ #[structopt(name = "input", parse(from_os_str))]
+ input: PathBuf,
+
+ /// A hash to verify, supplied directly on the command line
+ #[structopt(name = "hash")]
+ hash: Option<String>,
+}
+
+/// Types of supported digest algorithm
+#[derive(Debug, PartialEq, Copy, Clone)]
+pub enum Algorithm {
+ Md5,
+ Sha1,
+ Sha256,
+}
+
+impl Algorithm {
+ /// Assume a hash type from the binary length. Fortunately the typical 3 algorithms we care about are different lengths.
+ pub fn from_len(len: usize) -> Result<Algorithm, String> {
+ match len {
+ 16 => Ok(Algorithm::Md5),
+ 20 => Ok(Algorithm::Sha1),
+ 32 => Ok(Algorithm::Sha256),
+ _ => Err(format!("Unrecognised hash length: {} bytes", len)),
+ }
+ }
+}
+
+/// The method by which one or more hashes were supplied to verify the calculated digest
+pub enum VerificationSource {
+ CommandArgument,
+ Clipboard,
+ RawFile(PathBuf),
+ DigestsFile(PathBuf),
+}
+
+/// A complete standalone hash result
+pub struct Hash {
+ alg: Algorithm,
+ bytes: Vec<u8>,
+ filename: String,
+}
+
+impl Hash {
+ pub fn new(alg: Algorithm, bytes: Vec<u8>, path: &PathBuf) -> Self {
+ // Taking the filename component should always work?
+ // If not, just fall back to the full path
+ let filename = match path.file_name() {
+ Some(filename) => filename.to_string_lossy(),
+ None => path.to_string_lossy(),
+ };
+ Self {
+ alg,
+ bytes,
+ filename: filename.to_string(),
+ }
+ }
+}
+
+/// A possible hash to match against. The algorithm is assumed.
+pub struct CandidateHash {
+ bytes: Vec<u8>,
+ filename: Option<String>,
+}
+
+/// A list of candidate hashes that our input could potentially match. At this point it is
+/// assumed that we will be verifying a digest of a particular, single algorithm.
+pub struct CandidateHashes {
+ alg: Algorithm,
+ hashes: Vec<CandidateHash>,
+ source: VerificationSource,
+}
+
+/// Summary of an atetmpt to match the calculated digest against candidates
+pub enum MatchLevel {
+ Ok,
+ Maybe,
+ Fail,
+}
+
+/// The severity of any informational messages to be printed before the final result
+pub enum MessageLevel {
+ Error,
+ Warning,
+ Note,
+}
+
+/// Overall details of an attempt to match the calculated digest against candidates
+pub struct Verification<'a> {
+ match_level: MatchLevel,
+ comparison_hash: Option<&'a CandidateHash>,
+ messages: Vec<(MessageLevel, String)>,
+}
+
+/// Entry point - run the program and handle errors ourselves cleanly.
+///
+/// At the moment there aren't really any errors that can be handled by the application. Therefore
+/// stringly-typed errors are used and they are all captured here, where the problem is printed
+/// and the application terminates with a non-zero return code.
+fn main() {
+ hashgood().unwrap_or_else(|e| {
+ eprintln!("Error: {}", e);
+ process::exit(1);
+ });
+}
+
+/// Main application logic
+fn hashgood() -> Result<(), Box<dyn Error>> {
+ let opt = get_verified_options()?;
+ let candidates = verify::get_candidate_hashes(&opt)?;
+ let input = calculate::get_input_reader(&opt.input)?;
+ if let Some(c) = candidates {
+ // If we have a candidate hash of a particular type, use that specific algorithm
+ let hashes = calculate::create_digests(&[c.alg], input)?;
+ for (alg, bytes) in hashes {
+ // Should always be true
+ if c.alg == alg {
+ let hash = Hash::new(alg, bytes, &opt.input);
+ let verification = verify::verify_hash(&hash, &c);
+ display::print_hash(
+ &hash,
+ verification.comparison_hash,
+ Some(&c.source),
+ opt.no_colour,
+ )?;
+ display::print_messages(verification.messages, opt.no_colour)?;
+ display::print_match_level(verification.match_level, opt.no_colour)?;
+ }
+ }
+ } else {
+ // If no candidate, calculate all three common digest types for output
+ let hashes = calculate::create_digests(
+ &[Algorithm::Md5, Algorithm::Sha1, Algorithm::Sha256],
+ input,
+ )?;
+ for (alg, bytes) in hashes {
+ let hash = Hash {
+ alg,
+ bytes,
+ filename: opt.input.file_name().unwrap().to_string_lossy().to_string(),
+ };
+ display::print_hash(&hash, None, None, opt.no_colour)?;
+ }
+ }
+ Ok(())
+}
+
+/// Parse the command line options and check for ambiguous or inconsistent settings
+fn get_verified_options() -> Result<Opt, String> {
+ let opt = Opt::from_args();
+ let hash_methods =
+ opt.hash.is_some() as i32 + opt.paste as i32 + opt.hash_file.is_some() as i32;
+ if hash_methods > 1 {
+ if opt.hash.is_some() {
+ eprintln!("* specified as command line argument");
+ }
+ if opt.paste {
+ eprintln!("* paste from clipboard (-p)")
+ }
+ if opt.hash_file.is_some() {
+ eprintln!("* check hash from file (-c)")
+ }
+ return Err("Error: Hashes were provided by multiple methods. Use only one.".to_owned());
+ }
+ if opt.input.to_str() == Some("-")
+ && opt.hash_file.as_ref().and_then(|h| h.to_str()) == Some("-")
+ {
+ return Err("Error: Cannot use use stdin for both hash file and input data".to_owned());
+ }
+ Ok(opt)
+}
--- /dev/null
+use super::{
+ Algorithm, CandidateHash, CandidateHashes, Hash, MatchLevel, MessageLevel, Opt, Verification,
+ VerificationSource,
+};
+use clipboard::ClipboardContext;
+use clipboard::ClipboardProvider;
+use regex::Regex;
+use std::fs::File;
+use std::io;
+use std::io::prelude::*;
+use std::io::BufReader;
+use std::path::PathBuf;
+
+/// Calculate a list of candidate hashes based on the options specified.
+/// If no hash options have been specified returns None.
+/// It is assumed to be verified previously that at most one mode has been specified.
+pub fn get_candidate_hashes(opt: &Opt) -> Result<Option<CandidateHashes>, String> {
+ if let Some(hash_string) = &opt.hash {
+ return Ok(Some(get_by_parameter(hash_string)?));
+ } else if opt.paste {
+ return Ok(Some(get_from_clipboard()?));
+ } else if let Some(hash_file) = &opt.hash_file {
+ return Ok(Some(get_from_file(hash_file)?));
+ }
+ Ok(None)
+}
+
+/// Generate a candidate hash from the provided command line parameter, or throw an error.
+fn get_by_parameter(param: &str) -> Result<CandidateHashes, String> {
+ let bytes =
+ hex::decode(¶m).map_err(|_| "Provided hash is invalid or truncated hex".to_owned())?;
+ let alg = Algorithm::from_len(bytes.len())?;
+ let candidate = CandidateHash {
+ filename: None,
+ bytes,
+ };
+ Ok(CandidateHashes {
+ alg,
+ hashes: vec![candidate],
+ source: VerificationSource::CommandArgument,
+ })
+}
+
+/// Generate a candidate hash from the system clipboard, or throw an error.
+fn get_from_clipboard() -> Result<CandidateHashes, String> {
+ let mut ctx: ClipboardContext = match ClipboardProvider::new() {
+ Ok(ctx) => ctx,
+ Err(e) => return Err(format!("Error getting system clipboard: {}", e)),
+ };
+
+ let possible_hash = match ctx.get_contents() {
+ Ok(value) => value,
+ Err(e) => format!("Error reading from clipboard: {}", e),
+ };
+
+ let bytes = hex::decode(&possible_hash)
+ .map_err(|_| "Clipboard contains invalid or truncated hex".to_owned())?;
+ let alg = Algorithm::from_len(bytes.len())?;
+ let candidate = CandidateHash {
+ filename: None,
+ bytes,
+ };
+ Ok(CandidateHashes {
+ alg,
+ hashes: vec![candidate],
+ source: VerificationSource::Clipboard,
+ })
+}
+
+/// Generate a candidate hash from the digests file specified (could be "-" for STDIN), or throw an error.
+fn get_from_file(path: &PathBuf) -> Result<CandidateHashes, String> {
+ // Get a reader for either standard input or the chosen path
+ let reader: Box<dyn Read> = if path.to_str() == Some("-") {
+ Box::new(std::io::stdin())
+ } else {
+ Box::new(File::open(path).map_err(|_| {
+ format!(
+ "Unable to open check file at path '{}'",
+ path.to_string_lossy()
+ )
+ })?)
+ };
+
+ // Read the first line, trimmed
+ let mut reader = BufReader::new(reader);
+ let mut line = String::new();
+ reader
+ .read_line(&mut line)
+ .map_err(|_| "Error reading from check file".to_owned())?;
+ let line = line.trim().to_owned();
+
+ // Does our first line look like a raw hash on its own? If so, use that
+ if let Some(candidate) = read_raw_candidate_from_file(&line, &path) {
+ return Ok(candidate);
+ }
+
+ // Maybe it's a digests file
+ // Reconstruct the full iterator by joining our already-read line with the others
+ let full_lines = vec![Ok(line)].into_iter().chain(reader.lines());
+
+ // Does the entire file look like a coreutils-style digests file? (SHA1SUMS, etc.)
+ if let Some(candidate) = read_coreutils_digests_from_file(full_lines, &path) {
+ return Ok(candidate);
+ }
+
+ // If neither of these techniques worked this is a fatal error
+ // The user requested we use this input but we couldn't
+ Err(format!(
+ "Provided check file '{}' was neither a hash nor a valid digests file",
+ path.to_string_lossy()
+ ))
+}
+
+fn read_raw_candidate_from_file(line: &str, path: &PathBuf) -> Option<CandidateHashes> {
+ // It is a little sad to use a dynamic regex in an otherwise nice Rust program
+ // These deserve to be replaced with a good old fashioned static parser
+ // But let's be honest: the impact is negligible
+ let re = Regex::new(r"^([[:xdigit:]]{32}|[[:xdigit:]]{40}|[[:xdigit:]]{64})$").unwrap();
+ if re.is_match(line) {
+ // These should both always succeed due to the matching
+ let bytes = match hex::decode(line) {
+ Ok(bytes) => bytes,
+ _ => return None,
+ };
+ let alg = match Algorithm::from_len(bytes.len()) {
+ Ok(alg) => alg,
+ _ => return None,
+ };
+ return Some(CandidateHashes {
+ alg,
+ source: VerificationSource::RawFile(path.clone()),
+ hashes: vec![CandidateHash {
+ bytes,
+ filename: None,
+ }],
+ });
+ }
+ None
+}
+
+fn read_coreutils_digests_from_file<I>(lines: I, path: &PathBuf) -> Option<CandidateHashes>
+where
+ I: Iterator<Item = io::Result<String>>,
+{
+ let re = Regex::new(
+ r"^(?P<hash>([[:xdigit:]]{32}|[[:xdigit:]]{40}|[[:xdigit:]]{64})) .(?P<filename>.+)$",
+ )
+ .unwrap();
+
+ let mut hashes = vec![];
+ let mut alg: Option<Algorithm> = None;
+ for l in lines {
+ if let Ok(l) = l {
+ let l = l.trim();
+ // Allow (ignore) blank lines
+ if l.is_empty() {
+ continue;
+ }
+ // If we can capture a valid line, use it
+ if let Some(captures) = re.captures(&l) {
+ let hash = &captures["hash"];
+ let filename = &captures["filename"];
+ // Decode the hex and algorithm for this line
+ let line_bytes = match hex::decode(hash) {
+ Ok(bytes) => bytes,
+ _ => return None,
+ };
+ let line_alg = match Algorithm::from_len(line_bytes.len()) {
+ Ok(alg) => alg,
+ _ => return None,
+ };
+ if alg.is_some() && alg != Some(line_alg) {
+ // Different algorithms in the same digest file are not supported
+ return None;
+ } else {
+ // If we are the first line, we define the overall algorithm
+ alg = Some(line_alg);
+ }
+ // So far so good - create an entry for this line
+ hashes.push(CandidateHash {
+ bytes: line_bytes,
+ filename: Some(filename.to_owned()),
+ });
+ } else {
+ // But if we have a line with content we cannot parse, this is an error
+ return None;
+ }
+ }
+ }
+
+ // It is a failure if we got zero hashes or we somehow don't know the algorithm
+ if hashes.is_empty() {
+ return None;
+ }
+ let alg = match alg {
+ Some(alg) => alg,
+ _ => return None,
+ };
+
+ // Otherwise all is well and we can return our results
+ Some(CandidateHashes {
+ alg,
+ source: VerificationSource::DigestsFile(path.clone()),
+ hashes,
+ })
+}
+
+/// Determine if the calculated hash matches any of the candidates.
+///
+/// Ok result: the hash matches, and if the candidate has a filename, that matches too
+/// Maybe result: the hash matches but the filename does not
+/// Fail result: neither of the above
+pub fn verify_hash<'a>(calculated: &Hash, candidates: &'a CandidateHashes) -> Verification<'a> {
+ let mut ok: Option<&CandidateHash> = None;
+ let mut maybe: Option<&CandidateHash> = None;
+ let mut messages = Vec::new();
+
+ for candidate in &candidates.hashes {
+ if candidate.bytes == calculated.bytes {
+ match candidate.filename {
+ None => ok = Some(candidate),
+ Some(ref candidate_filename) if candidate_filename == &calculated.filename => {
+ ok = Some(candidate)
+ }
+ Some(ref candidate_filename) => {
+ messages.push((
+ MessageLevel::Warning,
+ format!(
+ "The matched hash has filename '{}', which does not match the input.",
+ candidate_filename
+ ),
+ ));
+ maybe = Some(candidate);
+ }
+ }
+ }
+ }
+
+ // Warn that a "successful" MD5 result is not necessarily great
+ if candidates.alg == Algorithm::Md5 && (ok.is_some() || maybe.is_some()) {
+ messages.push((
+ MessageLevel::Note,
+ "MD5 can easily be forged. Use a stronger algorithm if possible.".to_owned(),
+ ))
+ }
+
+ // If we got a full match, great
+ if ok.is_some() {
+ return Verification {
+ match_level: MatchLevel::Ok,
+ comparison_hash: ok,
+ messages,
+ };
+ }
+
+ // Second priority, a "maybe" result
+ if maybe.is_some() {
+ return Verification {
+ match_level: MatchLevel::Maybe,
+ comparison_hash: maybe,
+ messages,
+ };
+ }
+
+ // Otherwise we failed
+ // If we only had one candidate hash, include it
+ let comparison = match candidates.hashes.len() {
+ 1 => Some(&candidates.hashes[0]),
+ _ => None,
+ };
+ Verification {
+ match_level: MatchLevel::Fail,
+ comparison_hash: comparison,
+ messages,
+ }
+}