From: Thomas Karpiniec Date: Tue, 21 Jan 2020 05:54:30 +0000 (+1100) Subject: First import X-Git-Tag: v0.2.0~10 X-Git-Url: https://code.octet-stream.net/hashgood/commitdiff_plain/1dec1ec82f55d639d9fad0d0933545aa509c4272?ds=sidebyside First import --- 1dec1ec82f55d639d9fad0d0933545aa509c4272 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f0e3bca --- /dev/null +++ b/.gitignore @@ -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 index 0000000..5253cfb --- /dev/null +++ b/Cargo.lock @@ -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 index 0000000..1f68f35 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "hashgood" +version = "0.1.0" +authors = ["Thomas Karpiniec "] +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 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 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 index 0000000..870115c --- /dev/null +++ b/src/calculate.rs @@ -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)>, Box>; + +/// 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> { + 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) -> CalculateResult { + let mut senders = vec![]; + let mut handles = vec![]; + + if algorithms.contains(&Algorithm::Md5) { + let (s, r) = bounded::>>(1); + senders.push(s); + handles.push(md5_digest(r)); + } + if algorithms.contains(&Algorithm::Sha1) { + let (s, r) = bounded::>>(1); + senders.push(s); + handles.push(sha1_digest(r)); + } + if algorithms.contains(&Algorithm::Sha256) { + let (s, r) = bounded::>>(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>>) -> JoinHandle<(Algorithm, Vec)> { + 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>>) -> JoinHandle<(Algorithm, Vec)> { + 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>>) -> JoinHandle<(Algorithm, Vec)> { + 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 index 0000000..a4c011c --- /dev/null +++ b/src/display.rs @@ -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>; + +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, +) -> 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 index 0000000..2f1e7f3 --- /dev/null +++ b/src/main.rs @@ -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, + + /// 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, +} + +/// 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 { + 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, + filename: String, +} + +impl Hash { + pub fn new(alg: Algorithm, bytes: Vec, 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, + filename: Option, +} + +/// 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, + 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> { + 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 { + 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 index 0000000..6ad9ea9 --- /dev/null +++ b/src/verify.rs @@ -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, 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 { + 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 { + 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 { + // Get a reader for either standard input or the chosen path + let reader: Box = 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 { + // 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(lines: I, path: &PathBuf) -> Option +where + I: Iterator>, +{ + let re = Regex::new( + r"^(?P([[:xdigit:]]{32}|[[:xdigit:]]{40}|[[:xdigit:]]{64})) .(?P.+)$", + ) + .unwrap(); + + let mut hashes = vec![]; + let mut alg: Option = 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, + } +}