]> code.octet-stream.net Git - hashgood/commitdiff
First import
authorThomas Karpiniec <tom.karpiniec@outlook.com>
Tue, 21 Jan 2020 05:54:30 +0000 (16:54 +1100)
committerThomas Karpiniec <tom.karpiniec@outlook.com>
Tue, 21 Jan 2020 05:54:30 +0000 (16:54 +1100)
.gitignore [new file with mode: 0644]
Cargo.lock [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
src/calculate.rs [new file with mode: 0644]
src/display.rs [new file with mode: 0644]
src/main.rs [new file with mode: 0644]
src/verify.rs [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..f0e3bca
--- /dev/null
@@ -0,0 +1,2 @@
+/target
+**/*.rs.bk
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644 (file)
index 0000000..5253cfb
--- /dev/null
@@ -0,0 +1,542 @@
+# 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"
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..1f68f35
--- /dev/null
@@ -0,0 +1,14 @@
+[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"
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..261eeb9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 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.
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..295200d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# hashgood
+CLI tool for easily verifying a downloaded file's checksum
diff --git a/src/calculate.rs b/src/calculate.rs
new file mode 100644 (file)
index 0000000..870115c
--- /dev/null
@@ -0,0 +1,157 @@
+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);
+    }
+}
diff --git a/src/display.rs b/src/display.rs
new file mode 100644 (file)
index 0000000..a4c011c
--- /dev/null
@@ -0,0 +1,193 @@
+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(())
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644 (file)
index 0000000..2f1e7f3
--- /dev/null
@@ -0,0 +1,201 @@
+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)
+}
diff --git a/src/verify.rs b/src/verify.rs
new file mode 100644 (file)
index 0000000..6ad9ea9
--- /dev/null
@@ -0,0 +1,276 @@
+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(&param).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,
+    }
+}