commit 98335f435de2bc0cc80abafdea9536dc945fb11c Author: Casey Rodarmor Date: Fri May 24 01:25:55 2019 -0700 Initial commit type: added diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8346370 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +notes.md diff --git a/CONTRIBUTING b/CONTRIBUTING new file mode 100644 index 0000000..92bc1fe --- /dev/null +++ b/CONTRIBUTING @@ -0,0 +1,6 @@ +Contributing +============ + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you shall be licensed as in +LICENSE, without any additional terms or conditions. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..2457e83 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,558 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "aho-corasick" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 2.2.1 (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.66 (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 = "bitflags" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "c2-chacha" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +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 = "doc-comment" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "getrandom" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "wasi 0.7.0 (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.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "idna" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "imdl" +version = "0.0.0" +dependencies = [ + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "md5 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_bencode 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_bytes 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", + "sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "snafu 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "structopt 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "2.2.1" +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 = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ppv-lite86" +version = "0.2.6" +source = "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.11 (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.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_chacha" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.5.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.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "remove_dir_all" +version = "0.5.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 = "same-file" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-util 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_bencode" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_bytes 0.10.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_bytes" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_bytes" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive" +version = "1.0.104" +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.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "smallvec" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "snafu" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "doc-comment 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "snafu-derive 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "snafu-derive" +version = "0.6.0" +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.11 (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.5" +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.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "structopt-derive" +version = "0.3.5" +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.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "1.0.11" +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 = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)", + "remove_dir_all 0.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 = "termion" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.66 (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.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "smallvec 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-segmentation" +version = "1.3.0" +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 = "url" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 2.1.0 (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 = "walkdir" +version = "2.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "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 = "wasi" +version = "0.7.0" +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" + +[metadata] +"checksum aho-corasick 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)" = "58fb5e95d83b38284460a5fda7d6470aa0b8844d283a0b614b8535e880800d2d" +"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 bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" +"checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb" +"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +"checksum doc-comment 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "923dea538cea0aa3025e8685b20d6ee21ef99c4f77e954a30febbaac5ec73a97" +"checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407" +"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +"checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" +"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" +"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +"checksum md5 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" +"checksum memchr 2.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "88579771288728879b57485cc7d6b07d648c9f0141eb955f8ab7f9d45394468e" +"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" +"checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +"checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" +"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.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412" +"checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853" +"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +"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.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc220bd33bdce8f093101afe22a037b8eb0e5af33592e6a9caafff0d4cb81cbd" +"checksum regex-syntax 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "11a7e20d1cce64ef2fed88b66d347f88bd9babb82845b2b858f3edbf59a4f716" +"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" +"checksum same-file 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "585e8ddcedc187886a30fa705c47985c3fa88d06624095856b36ca0b82ff4421" +"checksum serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" +"checksum serde_bencode 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9b79ce11369638af2fea08e00390316304c5b5e1b7a53d58ace925152b657443" +"checksum serde_bytes 0.10.5 (registry+https://github.com/rust-lang/crates.io-index)" = "defbb8a83d7f34cc8380751eeb892b825944222888aff18996ea7901f24aec88" +"checksum serde_bytes 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "325a073952621257820e7a3469f55ba4726d8b28657e7e36653d1c36dc2c84ae" +"checksum serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" +"checksum sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +"checksum smallvec 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "44e59e0c9fa00817912ae6e4e6e3c4fe04455e75699d06eedc7d85917ed8e8f4" +"checksum snafu 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "41207ca11f96a62cd34e6b7fdf73d322b25ae3848eb9d38302169724bb32cf27" +"checksum snafu-derive 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4c5e338c8b0577457c9dda8e794b6ad7231c96e25b1b0dd5842d52249020c1c0" +"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +"checksum structopt 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "30b3a3e93f5ad553c38b3301c8a0a0cec829a36783f6a0c467fc4bf553a5f5bf" +"checksum structopt-derive 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea692d40005b3ceba90a9fe7a78fa8d4b82b0ce627eebbffc329aab850f3410e" +"checksum syn 1.0.11 (registry+https://github.com/rust-lang/crates.io-index)" = "dff0acdb207ae2fe6d5976617f887eb1e35a2ba52c13c7234c790960cdad9238" +"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +"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 unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +"checksum unicode-normalization 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b561e267b2326bb4cebfc0ef9e68355c7abe6c6f522aeac2f5bf95d56c59bdcf" +"checksum unicode-segmentation 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1967f4cdfc355b37fd76d2a954fb2ed3871034eb4f26d60537d88795cfc332a9" +"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 url 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "75b414f6c464c879d7f9babf951f23bc3743fb7313c081b2e6ca719067ea9d61" +"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +"checksum walkdir 2.2.9 (registry+https://github.com/rust-lang/crates.io-index)" = "9658c94fa8b940eab2250bd5a457f9c48b748420d71293b165c8cdbe2f55f71e" +"checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d" +"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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4da7f09 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "imdl" +version = "0.0.0" +description = "📦 A 40' shipping container for the internet" +authors = ["Casey Rodarmor "] +license = "CC0-1.0" +readme = "README.md" +keywords = ["p2p", "sharing", "bittorrent", "torrent"] +categories = ["command-line-utilities"] +homepage = "https://github.com/casey/intermodal" +repository = "https://github.com/casey/intermodal" +edition = "2018" + +[dependencies] +libc = "0.2" +md5 = "0.7" +regex = "1" +serde_bencode = "0.2" +serde_bytes = "0.11" +sha1 = "0.6" +snafu = "0.6" +structopt = "0.3" +tempfile = "3" +url = "2" +walkdir = "2" + +[dependencies.serde] +version = "1" +features = ["derive"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b6c047f --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# intermodal: a 40' shipping container for the Internet diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..ba50636 --- /dev/null +++ b/build.rs @@ -0,0 +1,12 @@ +use std::{error::Error, process::Command, str}; + +fn main() -> Result<(), Box> { + let stdout = Command::new("git") + .arg("rev-parse") + .arg("HEAD") + .output()? + .stdout; + let hash = str::from_utf8(&stdout)?; + println!("cargo:rustc-env=GIT_HEAD_PARTIAL_HASH={}", &hash[0..12]); + Ok(()) +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..773f475 --- /dev/null +++ b/justfile @@ -0,0 +1,17 @@ +default: watch + +# watch filesystem for changes and rerun tests +watch: + cargo watch --exec test + +# show stats about torrents at `PATH` +stats PATH: + cargo build --release + time ./target/release/imdl --unstable torrent stats --input {{PATH}} + +# retrieve large collection of torrents from the Internet Archive +get-torrents: + aria2c \ + -d dat \ + -x 10 \ + 'https://ia802701.us.archive.org/21/items/2014_torrent_archive_organized/torrent_archive_organized.zip' diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..bf1a714 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,2 @@ +tab_spaces = 2 +max_width = 100 diff --git a/src/bencode.rs b/src/bencode.rs new file mode 100644 index 0000000..e245529 --- /dev/null +++ b/src/bencode.rs @@ -0,0 +1,381 @@ +use crate::common::*; + +use self::Error::*; + +#[derive(Clone)] +pub(crate) enum Value<'buffer> { + Int(&'buffer str), + List(Vec>), + Dict(Vec<(&'buffer [u8], Value<'buffer>)>), + Str(&'buffer [u8]), +} + +impl<'buffer> Value<'buffer> { + pub(crate) fn decode(buffer: &'buffer [u8]) -> Result, Error> { + Parser::parse(buffer) + } + + #[cfg(test)] + fn encode(&self) -> Vec { + let mut buffer = Vec::new(); + self.encode_into(&mut buffer); + buffer + } + + #[cfg(test)] + fn encode_into(&self, buffer: &mut Vec) { + match self { + Self::Int(value) => { + buffer.push(b'i'); + buffer.extend_from_slice(value.as_bytes()); + buffer.push(b'e'); + } + Self::List(values) => { + buffer.push(b'l'); + for value in values { + value.encode_into(buffer); + } + buffer.push(b'e'); + } + Self::Dict(items) => { + buffer.push(b'd'); + for (key, value) in items { + Self::encode_str(buffer, key); + value.encode_into(buffer); + } + buffer.push(b'e'); + } + Self::Str(contents) => Self::encode_str(buffer, contents), + } + } + + #[cfg(test)] + fn encode_str(buffer: &mut Vec, contents: &[u8]) { + buffer.extend_from_slice(contents.len().to_string().as_bytes()); + buffer.push(b':'); + buffer.extend_from_slice(contents); + } + + fn fmt_str(f: &mut Formatter, contents: &[u8]) -> fmt::Result { + if let Ok(text) = str::from_utf8(contents) { + write!(f, "\"{}\"", text) + } else { + write!(f, "<")?; + for byte in contents { + write!(f, "{:X}", byte)?; + } + write!(f, ">") + } + } +} + +impl<'buffer> Display for Value<'buffer> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Int(value) => write!(f, "{}", value), + Self::List(values) => { + write!(f, "[")?; + for (i, value) in values.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}", value)?; + } + write!(f, "]") + } + Self::Dict(items) => { + write!(f, "{{")?; + for (i, (key, value)) in items.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + Value::fmt_str(f, key)?; + write!(f, ": {}", value)?; + } + write!(f, "}}") + } + Self::Str(contents) => Value::fmt_str(f, contents), + } + } +} + +#[derive(Debug, PartialEq)] +pub(crate) enum Error { + ExtraData { start: usize }, + UnexpectedEndOfBuffer, + UnexpectedByte { found: u8 }, + UnsortedKey, + DuplicateKey, + EmptyInteger, + NegativeZero, + LeadingZero, +} + +pub(crate) struct Parser<'buffer> { + index: usize, + buffer: &'buffer [u8], +} + +impl<'buffer> Parser<'buffer> { + pub(crate) fn parse(buffer: &'buffer [u8]) -> Result, Error> { + let parser = Parser { index: 0, buffer }; + + Ok(parser.root()?) + } + + fn root(mut self) -> Result, Error> { + let root = self.value()?; + + if self.index != self.buffer.len() { + return Err(ExtraData { start: self.index }); + } + + Ok(root) + } + + fn value(&mut self) -> Result, Error> { + match self.next()? { + b'i' => self.int(), + b'l' => self.list(), + b'd' => self.dict(), + b'0'..=b'9' => self.string(), + found => Err(UnexpectedByte { found }), + } + } + + fn extract_digits(&mut self) -> Result<&'buffer [u8], Error> { + let start = self.index; + + while let b'0'..=b'9' = self.next()? { + self.advance()?; + } + + Ok(&self.buffer[start..self.index]) + } + + fn parse_digits(&mut self, digits: &[u8]) -> Result { + if digits.is_empty() { + return Err(EmptyInteger); + } + + if digits == b"0" { + return Ok(0); + } + + let mut i = 0; + + for digit in digits { + let value: u64 = (digit - b'0').into(); + + if value == 0 && i == 0 { + return Err(LeadingZero); + } + + i = i * 10 + value; + } + + Ok(i) + } + + fn int(&mut self) -> Result, Error> { + self.expect(b'i')?; + + let start = self.index; + + let negative = self.accept(b'-')?; + + let digits = self.extract_digits()?; + + let end = self.index; + + self.expect(b'e')?; + + let value = self.parse_digits(digits)?; + + if value == 0 && negative { + return Err(NegativeZero); + } + + Ok(Value::Int( + str::from_utf8(&self.buffer[start..end]).unwrap(), + )) + } + + fn list(&mut self) -> Result, Error> { + self.expect(b'l')?; + + let mut values = Vec::new(); + + while self.next()? != b'e' { + values.push(self.value()?); + } + + self.expect(b'e')?; + + Ok(Value::List(values)) + } + + fn dict(&mut self) -> Result, Error> { + self.expect(b'd')?; + + let mut values: Vec<(&[u8], Value)> = Vec::new(); + + while self.next()? != b'e' { + let key = self.key()?; + + if let Some((last_key, _)) = values.last() { + match key.cmp(last_key) { + Ordering::Equal => return Err(DuplicateKey), + Ordering::Less => return Err(UnsortedKey), + Ordering::Greater => {} + } + } + + let value = self.value()?; + + values.push((key, value)); + } + + self.expect(b'e')?; + + Ok(Value::Dict(values)) + } + + fn string(&mut self) -> Result, Error> { + Ok(Value::Str(self.key()?)) + } + + fn key(&mut self) -> Result<&'buffer [u8], Error> { + let digits = self.extract_digits()?; + + self.expect(b':')?; + + let len = self.parse_digits(digits)?; + + let start = self.index; + for _ in 0..len { + self.advance()?; + } + + Ok(&self.buffer[start..self.index]) + } + + fn next(&self) -> Result { + self + .buffer + .get(self.index) + .cloned() + .ok_or(UnexpectedEndOfBuffer) + } + + fn advance(&mut self) -> Result<(), Error> { + if self.index == self.buffer.len() { + Err(UnexpectedEndOfBuffer) + } else { + self.index += 1; + Ok(()) + } + } + + fn expect(&mut self, expected: u8) -> Result<(), Error> { + let found = self.next()?; + + if found != expected { + return Err(UnexpectedByte { found }); + } + + self.advance()?; + + Ok(()) + } + + fn accept(&mut self, acceptable: u8) -> Result { + if self.next()? == acceptable { + self.advance()?; + Ok(true) + } else { + Ok(false) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn err(input: impl AsRef<[u8]>, expected: Error) { + let buffer = input.as_ref(); + let text = String::from_utf8_lossy(buffer); + match Parser::parse(buffer) { + Ok(_) => panic!( + "Input `{}` passed validation, expected: {:?}", + text, expected, + ), + Err(error) => assert_eq!(error, expected, "Unexpected error for input `{}`", text), + } + } + + fn ok(input: impl AsRef<[u8]>) { + let buffer = input.as_ref(); + match Value::decode(buffer) { + Err(_) => { + panic!( + "Input failed to validate: `{}`", + String::from_utf8_lossy(buffer) + ); + } + Ok(value) => { + let round_trip = value.encode(); + assert_eq!(round_trip, buffer); + } + } + } + + #[test] + fn misc() { + err("", UnexpectedEndOfBuffer); + err("i20efoo", ExtraData { start: 4 }); + err("defoo", ExtraData { start: 2 }); + err("lefoo", ExtraData { start: 2 }); + err("1:afoo", ExtraData { start: 3 }); + } + + #[test] + fn int() { + err("ie", EmptyInteger); + err("i-0e", NegativeZero); + err("i00e", LeadingZero); + err("iae", UnexpectedByte { found: b'a' }); + ok("i0e"); + ok("i-100e"); + } + + #[test] + fn list() { + ok("le"); + ok("llelelee"); + ok("li20ee"); + ok("li20edelee"); + } + + #[test] + fn dict() { + ok("de"); + ok("d0:0:e"); + err("di0elee", UnexpectedByte { found: b'i' }); + err("d0:i0ei0ei0ee", UnexpectedByte { found: b'i' }); + err("d0:e", UnexpectedByte { found: b'e' }); + err("d0:de0:dee", DuplicateKey); + err("d1:ade0:dee", UnsortedKey); + err("d1:ade0:dee", UnsortedKey); + ok("d1:ade1:bde1:cdee"); + } + + #[test] + fn string() { + ok("0:"); + ok("5:hello"); + err("1:", UnexpectedEndOfBuffer); + err("2:a", UnexpectedEndOfBuffer); + } +} diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..b8297cc --- /dev/null +++ b/src/common.rs @@ -0,0 +1,42 @@ +// stdlib types +pub(crate) use std::{ + borrow::Cow, + cmp::{Ordering, Reverse}, + collections::{BTreeMap, HashMap}, + env, + ffi::{OsStr, OsString}, + fmt::{self, Display, Formatter}, + fs::{self, File}, + hash::Hash, + io::{self, Read, Write}, + path::{Path, PathBuf}, + process, str, + time::{SystemTime, SystemTimeError}, + u64, usize, +}; + +// dependencies +pub(crate) use libc::EXIT_FAILURE; +pub(crate) use regex::{Regex, RegexSet}; +pub(crate) use serde::{Deserialize, Serialize}; +pub(crate) use sha1::Sha1; +pub(crate) use snafu::{ResultExt, Snafu}; +pub(crate) use structopt::StructOpt; +pub(crate) use url::Url; +pub(crate) use walkdir::WalkDir; + +// modules +pub(crate) use crate::{bencode, consts, error, torrent}; + +// traits +pub(crate) use crate::{path_ext::PathExt, reckoner::Reckoner}; + +// structs and enums +pub(crate) use crate::{ + environment::Environment, error::Error, file_info::FileInfo, hasher::Hasher, info::Info, + metainfo::Metainfo, mode::Mode, opt::Opt, subcommand::Subcommand, torrent::Torrent, +}; + +// test modules +#[cfg(test)] +pub(crate) use crate::testing; diff --git a/src/consts.rs b/src/consts.rs new file mode 100644 index 0000000..eb9eba8 --- /dev/null +++ b/src/consts.rs @@ -0,0 +1,13 @@ +/// Default value for `created by` torrent metainfo field. +/// +/// Example: imdl/0.0.0 (1234567890AB) +pub(crate) const CREATED_BY_DEFAULT: &str = concat!( + "imdl/", + env!("CARGO_PKG_VERSION"), + " (", + env!("GIT_HEAD_PARTIAL_HASH"), + ")" +); + +/// Value for `encoding` torrent metainfo field. +pub(crate) const ENCODING_UTF8: &str = "UTF-8"; diff --git a/src/environment.rs b/src/environment.rs new file mode 100644 index 0000000..26c579a --- /dev/null +++ b/src/environment.rs @@ -0,0 +1,49 @@ +use crate::common::*; + +pub(crate) struct Environment { + args: Vec, + dir: Box>, + err: Box, +} + +impl Environment { + pub(crate) fn main() -> Environment { + let dir = match env::current_dir() { + Ok(dir) => dir, + Err(error) => panic!("Failed to get current directory: {}", error), + }; + + Environment::new(dir, io::stderr(), env::args()) + } + + pub(crate) fn run(&self) -> Result<(), Error> { + Opt::from_iter_safe(&self.args)?.run(self) + } + + pub(crate) fn new(dir: D, err: E, args: I) -> Environment + where + D: AsRef + 'static, + E: Write + 'static, + S: Into, + I: IntoIterator, + { + Environment { + args: args.into_iter().map(|s| s.into()).collect(), + dir: Box::new(dir), + err: Box::new(err), + } + } + + pub(crate) fn status(&mut self) -> Result<(), i32> { + if let Err(error) = self.run() { + write!(&mut self.err, "error: {}", error).ok(); + Err(EXIT_FAILURE) + } else { + Ok(()) + } + } + + pub(crate) fn resolve(&self, path: impl AsRef) -> PathBuf { + self.dir.as_ref().as_ref().join(path).clean() + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..7ba16d4 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,61 @@ +use crate::common::*; + +use structopt::clap; + +#[derive(Debug, Snafu)] +#[snafu(visibility(pub(crate)))] +pub(crate) enum Error { + #[snafu(display("{}", source))] + Clap { source: clap::Error }, + #[snafu(display("I/O error at `{}`: {}", path.display(), source))] + Filesystem { source: io::Error, path: PathBuf }, + #[snafu(display("Serialization failed: {}", source))] + Serialize { source: serde_bencode::Error }, + #[snafu(display("Filename was not valid unicode: {}", filename.to_string_lossy()))] + FilenameDecode { filename: OsString }, + #[snafu(display("Path had no file name: {}", path.display()))] + FilenameExtract { path: PathBuf }, + #[snafu(display("Failed to retrieve system time: {}", source))] + SystemTime { source: SystemTimeError }, + #[snafu(display("Failed to parse announce URL: {}", source))] + AnnounceUrlParse { source: url::ParseError }, + #[snafu(display("Must provide at least one announce URL"))] + AnnounceEmpty, + #[snafu(display("Failed to decode bencode: {}", source))] + BencodeDecode { source: serde_bencode::Error }, + #[snafu(display( + "Feature `{}` cannot be used without passing the `--unstable` flag", + feature + ))] + Unstable { feature: &'static str }, +} + +impl From for Error { + fn from(source: clap::Error) -> Error { + Error::Clap { source } + } +} + +impl From for Error { + fn from(source: serde_bencode::Error) -> Error { + Error::Serialize { source } + } +} + +impl From for Error { + fn from(source: SystemTimeError) -> Error { + Error::SystemTime { source } + } +} + +impl From for Error { + fn from(walkdir_error: walkdir::Error) -> Error { + let path = walkdir_error.path().unwrap().to_owned(); + + if let Some(source) = walkdir_error.into_io_error() { + Error::Filesystem { source, path } + } else { + unreachable!("Encountered unexpected walkdir error") + } + } +} diff --git a/src/file_info.rs b/src/file_info.rs new file mode 100644 index 0000000..efd6a41 --- /dev/null +++ b/src/file_info.rs @@ -0,0 +1,8 @@ +use crate::common::*; + +#[derive(Deserialize, Serialize, Debug, PartialEq)] +pub struct FileInfo { + pub length: u64, + pub md5sum: Option, + pub path: Vec, +} diff --git a/src/hasher.rs b/src/hasher.rs new file mode 100644 index 0000000..8be5e52 --- /dev/null +++ b/src/hasher.rs @@ -0,0 +1,131 @@ +use crate::common::*; + +pub(crate) struct Hasher { + buffer: Vec, + length: u64, + md5sum: bool, + piece_bytes_hashed: u64, + piece_length: u64, + pieces: Vec, + sha1: Sha1, +} + +impl Hasher { + pub(crate) fn hash( + root: &Path, + md5sum: bool, + piece_length: u64, + ) -> Result<(Mode, Vec), Error> { + Hasher::new(md5sum, piece_length).hash_root(root) + } + + fn new(md5sum: bool, piece_length: u64) -> Hasher { + Hasher { + buffer: vec![0; piece_length as usize], + length: 0, + piece_bytes_hashed: 0, + pieces: Vec::new(), + sha1: Sha1::new(), + md5sum, + piece_length, + } + } + + fn hash_root(mut self, root: &Path) -> Result<(Mode, Vec), Error> { + let single = root + .metadata() + .context(error::Filesystem { path: root })? + .is_file(); + + if single { + let (md5sum, length) = self.hash_file(&root)?; + + if self.piece_bytes_hashed > 0 { + self.pieces.extend(&self.sha1.digest().bytes()); + self.sha1.reset(); + self.piece_bytes_hashed = 0; + } + + Ok(( + Mode::Single { + md5sum: md5sum.map(|md5sum| format!("{:x}", md5sum)), + length, + }, + self.pieces, + )) + } else { + let files = self.hash_dir(root)?; + + if self.piece_bytes_hashed > 0 { + self.pieces.extend(&self.sha1.digest().bytes()); + self.sha1.reset(); + self.piece_bytes_hashed = 0; + } + + Ok((Mode::Multiple { files }, self.pieces)) + } + } + + fn hash_dir(&mut self, dir: &Path) -> Result, Error> { + for result in WalkDir::new(dir).sort_by(|a, b| a.file_name().cmp(b.file_name())) { + let entry = result?; + + let path = entry.path(); + + if entry.metadata()?.is_file() { + let (_md5sum, _length) = self.hash_file(path)?; + } + } + + Ok(Vec::new()) + } + + fn hash_file(&mut self, file: &Path) -> Result<(Option, u64), Error> { + self + .hash_file_io(file) + .context(error::Filesystem { path: file }) + } + + fn hash_file_io(&mut self, file: &Path) -> io::Result<(Option, u64)> { + let length = file.metadata()?.len(); + + let mut remaining = length; + + let mut file = File::open(file)?; + + let mut md5 = if self.md5sum { + Some(md5::Context::new()) + } else { + None + }; + + while remaining > 0 { + let to_buffer = self.buffer.len().min(remaining as usize); + let buffer = &mut self.buffer[0..to_buffer]; + + file.read_exact(buffer)?; + + for byte in buffer.iter().cloned() { + self.sha1.update(&[byte]); + + self.piece_bytes_hashed += 1; + + if self.piece_bytes_hashed == self.piece_length { + self.pieces.extend(&self.sha1.digest().bytes()); + self.sha1.reset(); + self.piece_bytes_hashed = 0; + } + } + + if let Some(md5) = md5.as_mut() { + md5.consume(&buffer); + } + + remaining -= buffer.len() as u64; + } + + self.length += length; + + Ok((md5.map(md5::Context::compute), length)) + } +} diff --git a/src/info.rs b/src/info.rs new file mode 100644 index 0000000..826efd3 --- /dev/null +++ b/src/info.rs @@ -0,0 +1,13 @@ +use crate::common::*; + +#[derive(Deserialize, Serialize)] +pub struct Info { + pub private: u8, + #[serde(rename = "piece length")] + pub piece_length: u64, + pub name: String, + #[serde(with = "serde_bytes")] + pub pieces: Vec, + #[serde(flatten)] + pub mode: Mode, +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..738fb97 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,30 @@ +use crate::common::*; + +#[cfg(test)] +#[macro_use] +mod matches; + +#[cfg(test)] +mod testing; + +mod bencode; +mod common; +mod consts; +mod environment; +mod error; +mod file_info; +mod hasher; +mod info; +mod metainfo; +mod mode; +mod opt; +mod path_ext; +mod reckoner; +mod subcommand; +mod torrent; + +fn main() { + if let Err(code) = Environment::main().status() { + process::exit(code); + } +} diff --git a/src/matches.rs b/src/matches.rs new file mode 100644 index 0000000..7a56d8c --- /dev/null +++ b/src/matches.rs @@ -0,0 +1,8 @@ +macro_rules! matches { + ($expression:expr, $( $pattern:pat )|+ $( if $guard: expr )?) => { + match $expression { + $( $pattern )|+ $( if $guard )? => true, + _ => false + } + } +} diff --git a/src/metainfo.rs b/src/metainfo.rs new file mode 100644 index 0000000..ff0922c --- /dev/null +++ b/src/metainfo.rs @@ -0,0 +1,15 @@ +use crate::common::*; + +#[derive(Deserialize, Serialize)] +pub struct Metainfo { + pub announce: String, + #[serde(rename = "announce list")] + pub announce_list: Vec>, + pub comment: Option, + #[serde(rename = "created by")] + pub created_by: Option, + #[serde(rename = "creation date")] + pub creation_date: Option, + pub encoding: String, + pub info: Info, +} diff --git a/src/mode.rs b/src/mode.rs new file mode 100644 index 0000000..8c6c827 --- /dev/null +++ b/src/mode.rs @@ -0,0 +1,8 @@ +use crate::common::*; + +#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[serde(untagged)] +pub enum Mode { + Single { length: u64, md5sum: Option }, + Multiple { files: Vec }, +} diff --git a/src/opt.rs b/src/opt.rs new file mode 100644 index 0000000..f0be0c6 --- /dev/null +++ b/src/opt.rs @@ -0,0 +1,15 @@ +use crate::common::*; + +#[derive(StructOpt)] +pub(crate) struct Opt { + #[structopt(long = "unstable", short = "u")] + unstable: bool, + #[structopt(subcommand)] + subcommand: Subcommand, +} + +impl Opt { + pub(crate) fn run(self, env: &Environment) -> Result<(), Error> { + self.subcommand.run(env, self.unstable) + } +} diff --git a/src/path_ext.rs b/src/path_ext.rs new file mode 100644 index 0000000..02d799b --- /dev/null +++ b/src/path_ext.rs @@ -0,0 +1,55 @@ +use crate::common::*; + +use std::path::Component; + +pub(crate) trait PathExt { + fn clean(self) -> PathBuf; +} + +impl PathExt for &Path { + fn clean(self) -> PathBuf { + let mut components = Vec::new(); + + for component in self.components() { + if component == Component::ParentDir { + if let Some(Component::Normal(_)) = components.last() { + components.pop(); + } + } else { + components.push(component); + } + } + + components.into_iter().collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn clean() { + let cases = &[ + ("/", "foo", "/foo"), + ("/", ".", "/"), + ("/", "foo/./bar", "/foo/bar"), + ("/foo/./bar", ".", "/foo/bar"), + ("/bar", "/foo", "/foo"), + ("//foo", "bar//baz", "/foo/bar/baz"), + ("/", "..", "/"), + ("/", "/..", "/"), + ("/..", "", "/"), + ("/../../../..", "../../../", "/"), + ("/.", "./", "/"), + ("/foo/../", "bar", "/bar"), + ("/foo/bar", "..", "/foo"), + ("/foo/bar/", "..", "/foo"), + ]; + + for (prefix, suffix, want) in cases { + let have = Path::new(prefix).join(Path::new(suffix)).clean(); + assert_eq!(have, Path::new(want)); + } + } +} diff --git a/src/reckoner.rs b/src/reckoner.rs new file mode 100644 index 0000000..02997b7 --- /dev/null +++ b/src/reckoner.rs @@ -0,0 +1,79 @@ +use crate::common::*; + +pub(crate) trait Reckoner { + fn increment_ref(&mut self, k: &K) + where + K: Clone; + + fn increment(&mut self, k: K); + + fn increment_multiple(&mut self, i: I) + where + I: IntoIterator, + { + for k in i.into_iter() { + self.increment(k); + } + } +} + +impl Reckoner for BTreeMap { + fn increment_ref(&mut self, k: &K) + where + K: Clone, + { + if let Some(count) = self.get_mut(k) { + *count += 1; + } else { + self.insert(k.clone(), 1); + } + } + + fn increment(&mut self, k: K) { + *self.entry(k).or_insert(0) += 1; + } +} + +impl Reckoner for HashMap { + fn increment_ref(&mut self, k: &K) + where + K: Clone, + { + if let Some(count) = self.get_mut(k) { + *count += 1; + } else { + self.insert(k.clone(), 1); + } + } + + fn increment(&mut self, k: K) { + *self.entry(k).or_insert(0) += 1; + } +} + +impl Reckoner for Vec<(K, u64)> { + fn increment_ref(&mut self, k: &K) + where + K: Clone, + { + match self.binary_search_by_key(&k, |(key, _count)| key) { + Ok(i) => { + self[i].1 *= 1; + } + Err(i) => { + self.insert(i, (k.clone(), 1)); + } + } + } + + fn increment(&mut self, k: K) { + match self.binary_search_by_key(&&k, |(key, _count)| key) { + Ok(i) => { + self[i].1 *= 1; + } + Err(i) => { + self.insert(i, (k, 1)); + } + } + } +} diff --git a/src/subcommand.rs b/src/subcommand.rs new file mode 100644 index 0000000..c27f42b --- /dev/null +++ b/src/subcommand.rs @@ -0,0 +1,14 @@ +use crate::common::*; + +#[derive(StructOpt)] +pub(crate) enum Subcommand { + Torrent(Torrent), +} + +impl Subcommand { + pub(crate) fn run(self, env: &Environment, unstable: bool) -> Result<(), Error> { + match self { + Self::Torrent(torrent) => torrent.run(env, unstable), + } + } +} diff --git a/src/testing.rs b/src/testing.rs new file mode 100644 index 0000000..d12429d --- /dev/null +++ b/src/testing.rs @@ -0,0 +1,11 @@ +use crate::common::*; + +use std::{io::Cursor, iter}; + +pub(crate) fn environment(iter: impl IntoIterator>) -> Environment { + Environment::new( + tempfile::tempdir().unwrap(), + Cursor::new(Vec::new()), + iter::once(String::from("imdl")).chain(iter.into_iter().map(|item| item.into())), + ) +} diff --git a/src/torrent.rs b/src/torrent.rs new file mode 100644 index 0000000..4538437 --- /dev/null +++ b/src/torrent.rs @@ -0,0 +1,19 @@ +use crate::common::*; + +mod create; +mod stats; + +#[derive(StructOpt)] +pub(crate) enum Torrent { + Create(torrent::create::Create), + Stats(torrent::stats::Stats), +} + +impl Torrent { + pub(crate) fn run(self, env: &Environment, unstable: bool) -> Result<(), Error> { + match self { + Self::Create(create) => create.run(env), + Self::Stats(stats) => stats.run(env, unstable), + } + } +} diff --git a/src/torrent/create.rs b/src/torrent/create.rs new file mode 100644 index 0000000..6cc5598 --- /dev/null +++ b/src/torrent/create.rs @@ -0,0 +1,541 @@ +use crate::common::*; + +#[derive(StructOpt)] +pub(crate) struct Create { + #[structopt(name = "INPUT", long = "input")] + input: PathBuf, + #[structopt(name = "OUTPUT", long = "output")] + output: Option, + #[structopt(name = "PRIVATE", long = "private")] + private: bool, + #[structopt(name = "NO-CREATION-DATE", long = "no-creation-date")] + no_creation_date: bool, + #[structopt(name = "NO-CREATED-BY", long = "no-created-by")] + no_created_by: bool, + #[structopt(name = "COMMENT", long = "comment")] + comment: Option, + #[structopt(name = "PIECE-LENGTH", long = "piece-length", default_value = "524288")] + piece_length: u64, + #[structopt(name = "ANNOUNCE", long = "announce", required(true))] + announce: Vec, + #[structopt(name = "MD5SUM", long = "md5sum")] + md5sum: bool, + #[structopt(name = "NAME", long = "name")] + name: Option, +} + +impl Create { + pub(crate) fn run(self, env: &Environment) -> Result<(), Error> { + let input = env.resolve(&self.input); + + let mut announce_list = Vec::new(); + for announce in &self.announce { + let tier = announce + .split(",") + .map(str::to_string) + .collect::>(); + + tier + .iter() + .map(|announce| announce.parse()) + .collect::, url::ParseError>>() + .context(error::AnnounceUrlParse)?; + + announce_list.push(tier); + } + + let announce = if let Some(primary) = announce_list.first().and_then(|tier| tier.first()) { + primary.clone() + } else { + return Err(Error::AnnounceEmpty); + }; + + let filename = input.file_name().ok_or_else(|| Error::FilenameExtract { + path: input.clone(), + })?; + + let name = match &self.name { + Some(name) => name.clone(), + None => filename + .to_str() + .ok_or_else(|| Error::FilenameDecode { + filename: filename.to_os_string(), + })? + .to_owned(), + }; + + let output = self + .output + .as_ref() + .map(|output| env.resolve(&output)) + .unwrap_or_else(|| { + let mut torrent_name = name.to_owned(); + torrent_name.push_str(".torrent"); + + input.parent().unwrap().join(torrent_name) + }); + + let private = if self.private { 1 } else { 0 }; + + let creation_date = if self.no_creation_date { + None + } else { + Some( + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH)? + .as_secs(), + ) + }; + + let created_by = if self.no_created_by { + None + } else { + Some(String::from(consts::CREATED_BY_DEFAULT)) + }; + + let (mode, pieces) = Hasher::hash(&input, self.md5sum, self.piece_length)?; + + let info = Info { + piece_length: self.piece_length, + mode, + pieces, + name, + private, + }; + + let metainfo = Metainfo { + comment: self.comment.clone(), + encoding: consts::ENCODING_UTF8.to_string(), + announce, + announce_list, + creation_date, + created_by, + info, + }; + + let bytes = serde_bencode::ser::to_bytes(&metainfo)?; + + fs::write(&output, bytes).context(error::Filesystem { path: &output })?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn environment(args: &[&str]) -> Environment { + testing::environment(["torrent", "create"].iter().chain(args).cloned()) + } + + #[test] + fn require_input_argument() { + let env = environment(&[]); + assert!(matches!(env.run(), Err(Error::Clap { .. }))); + } + + #[test] + fn require_input_present() { + let env = environment(&["--input", "foo", "--announce", "http://bar"]); + assert!(matches!(env.run(), Err(Error::Filesystem { .. }))); + } + + #[test] + fn torrent_file_is_bencode_dict() { + let env = environment(&["--input", "foo", "--announce", "http://bar"]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let value = bencode::Value::decode(&bytes).unwrap(); + assert!(matches!(value, bencode::Value::Dict(_))); + } + + #[test] + fn privacy_defaults_to_false() { + let env = environment(&["--input", "foo", "--announce", "http://bar"]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.info.private, 0); + } + + #[test] + fn privacy_flag_sets_privacy() { + let env = environment(&["--input", "foo", "--announce", "http://bar", "--private"]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.info.private, 1); + } + + #[test] + fn tracker_flag_must_be_url() { + let env = environment(&["--input", "foo", "--announce", "bar"]); + fs::write(env.resolve("foo"), "").unwrap(); + assert!(matches!(env.run(), Err(Error::AnnounceUrlParse { .. }))); + } + + #[test] + fn announce_single() { + let env = environment(&["--input", "foo", "--announce", "http://bar"]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.announce, "http://bar"); + assert_eq!(metainfo.announce_list, vec![vec!["http://bar"]]); + } + + #[test] + fn announce_single_tier() { + let env = environment(&["--input", "foo", "--announce", "http://bar,http://baz"]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.announce, "http://bar"); + assert_eq!( + metainfo.announce_list, + vec![vec!["http://bar", "http://baz"]] + ); + } + + #[test] + fn announce_multiple_tiers() { + let env = environment(&[ + "--input", + "foo", + "--announce", + "http://bar,http://baz", + "--announce", + "http://abc,http://xyz", + ]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.announce, "http://bar"); + assert_eq!( + metainfo.announce_list, + vec![ + vec!["http://bar", "http://baz"], + vec!["http://abc", "http://xyz"], + ] + ); + } + + #[test] + fn comment_default() { + let env = environment(&["--input", "foo", "--announce", "http://bar"]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.comment, None); + } + + #[test] + fn comment_set() { + let env = environment(&[ + "--input", + "foo", + "--announce", + "http://bar", + "--comment", + "Hello, world!", + ]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.comment.unwrap(), "Hello, world!"); + } + + #[test] + fn piece_length_default() { + let env = environment(&["--input", "foo", "--announce", "http://bar"]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.info.piece_length, 512 * 2u64.pow(10)); + } + + #[test] + fn piece_length_override() { + let env = environment(&[ + "--input", + "foo", + "--announce", + "http://bar", + "--piece-length", + "1", + ]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.info.piece_length, 1); + } + + #[test] + fn name() { + let env = environment(&[ + "--input", + "foo", + "--announce", + "http://bar", + "--piece-length", + "1", + ]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.info.name, "foo"); + } + + #[test] + fn name_subdir() { + let env = environment(&[ + "--input", + "foo/bar", + "--announce", + "http://bar", + "--piece-length", + "1", + ]); + let dir = env.resolve("foo"); + fs::create_dir(&dir).unwrap(); + fs::write(dir.join("bar"), "").unwrap(); + env.run().unwrap(); + let torrent = dir.join("bar.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.info.name, "bar"); + } + + #[test] + fn destination_override() { + let env = environment(&[ + "--input", + "foo", + "--output", + "x.torrent", + "--announce", + "http://bar", + ]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("x.torrent"); + let bytes = fs::read(torrent).unwrap(); + serde_bencode::de::from_bytes::(&bytes).unwrap(); + } + + #[test] + fn created_by_default() { + let env = environment(&["--input", "foo", "--announce", "http://bar"]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.created_by.unwrap(), consts::CREATED_BY_DEFAULT); + } + + #[test] + fn created_by_unset() { + let env = environment(&[ + "--input", + "foo", + "--announce", + "http://bar", + "--no-created-by", + ]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.created_by, None); + } + + #[test] + fn encoding() { + let env = environment(&["--input", "foo", "--announce", "http://bar"]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.encoding, "UTF-8"); + } + + #[test] + fn created_date_default() { + let env = environment(&["--input", "foo", "--announce", "http://bar"]); + fs::write(env.resolve("foo"), "").unwrap(); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert!(metainfo.creation_date.unwrap() < now + 10); + assert!(metainfo.creation_date.unwrap() > now - 10); + } + + #[test] + fn created_date_unset() { + let env = environment(&[ + "--input", + "foo", + "--announce", + "http://bar", + "--no-creation-date", + ]); + fs::write(env.resolve("foo"), "").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.creation_date, None); + } + + #[test] + fn single_small() { + let env = environment(&["--input", "foo", "--announce", "http://bar"]); + let contents = "bar"; + fs::write(env.resolve("foo"), contents).unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes()); + assert_eq!( + metainfo.info.mode, + Mode::Single { + length: contents.len() as u64, + md5sum: None, + } + ) + } + + #[test] + fn single_one_byte_piece() { + let env = environment(&[ + "--input", + "foo", + "--announce", + "http://bar", + "--piece-length", + "1", + ]); + let contents = "bar"; + fs::write(env.resolve("foo"), contents).unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + let pieces = Sha1::from("b") + .digest() + .bytes() + .iter() + .chain(Sha1::from("a").digest().bytes().iter()) + .chain(Sha1::from("r").digest().bytes().iter()) + .cloned() + .collect::>(); + + assert_eq!(metainfo.info.pieces, pieces); + assert_eq!( + metainfo.info.mode, + Mode::Single { + length: contents.len() as u64, + md5sum: None, + } + ) + } + + #[test] + fn single_empty() { + let env = environment(&["--input", "foo", "--announce", "http://bar"]); + let contents = ""; + fs::write(env.resolve("foo"), contents).unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.info.pieces.len(), 0); + assert_eq!( + metainfo.info.mode, + Mode::Single { + length: 0, + md5sum: None, + } + ) + } + + #[test] + fn multiple_no_files() { + let env = environment(&["--input", "foo", "--announce", "http://bar"]); + let dir = env.resolve("foo"); + fs::create_dir(&dir).unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.info.pieces.len(), 0); + assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() }) + } + + #[test] + fn multiple_one_file() { + let env = environment(&["--input", "foo", "--announce", "http://bar"]); + let dir = env.resolve("foo"); + fs::create_dir(&dir).unwrap(); + let file = dir.join("bar"); + let contents = "bar"; + fs::write(file, contents).unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes()); + assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() }) + } + + #[test] + fn multiple_three_files() { + let env = environment(&["--input", "foo", "--announce", "http://bar"]); + let dir = env.resolve("foo"); + fs::create_dir(&dir).unwrap(); + fs::write(dir.join("a"), "abc").unwrap(); + fs::write(dir.join("x"), "xyz").unwrap(); + fs::write(dir.join("h"), "hij").unwrap(); + env.run().unwrap(); + let torrent = env.resolve("foo.torrent"); + let bytes = fs::read(torrent).unwrap(); + let metainfo = serde_bencode::de::from_bytes::(&bytes).unwrap(); + assert_eq!( + metainfo.info.pieces, + Sha1::from("abchijxyz").digest().bytes() + ); + assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() }) + } +} diff --git a/src/torrent/stats.rs b/src/torrent/stats.rs new file mode 100644 index 0000000..b81e808 --- /dev/null +++ b/src/torrent/stats.rs @@ -0,0 +1,171 @@ +use crate::common::*; + +#[derive(StructOpt)] +pub(crate) struct Stats { + #[structopt(long = "limit", short = "l")] + limit: Option, + #[structopt(long = "extract-pattern", short = "e")] + extract_patterns: Vec, + #[structopt(name = "INPUT", long = "input", short = "i")] + input: PathBuf, +} + +impl Stats { + pub(crate) fn run(self, env: &Environment, unstable: bool) -> Result<(), Error> { + if !unstable { + return Err(Error::Unstable { + feature: "torrent stats subcommand", + }); + } + + let path = env.resolve(self.input); + + let mut extractor = Extractor::new(&self.extract_patterns); + + for result in WalkDir::new(path).sort_by(|a, b| a.file_name().cmp(b.file_name())) { + if extractor.torrents >= self.limit.unwrap_or(u64::MAX) { + break; + } + + let entry = result?; + + extractor.process(entry.path()); + } + + println!("Torrents processed: {}", extractor.torrents); + println!("Read failed: {}", extractor.io_errors); + println!("Decode failed: {}", extractor.bencode_decode_errors); + + let mut paths = extractor.paths.into_iter().collect::>(); + paths.sort_by_key(|(_, count)| Reverse(*count)); + let max = paths.iter().map(|(_, count)| *count).max().unwrap_or(0); + let width = max.to_string().len(); + + println!("Keys:"); + for (key, count) in &paths { + if key.starts_with("info/files") { + continue; + } + println!("{:)>>(); + + println!("Values:"); + for (pattern, values) in values { + println!("{}: ", pattern); + for (i, value) in values.iter().enumerate() { + if i > 0 { + print!(", "); + } + print!("{}", value); + } + } + } + + Ok(()) + } +} + +struct Extractor { + bencode_decode_errors: u64, + io_errors: u64, + paths: HashMap, + regex_set: RegexSet, + torrents: u64, + values: HashMap>, + current_path: String, +} + +impl Extractor { + fn new(regexes: &[Regex]) -> Extractor { + let regex_set = RegexSet::new(regexes.iter().map(Regex::as_str)).unwrap(); + + Extractor { + bencode_decode_errors: 0, + io_errors: 0, + paths: HashMap::new(), + torrents: 0, + values: HashMap::new(), + current_path: String::new(), + regex_set, + } + } + + fn process(&mut self, path: &Path) { + if !path.is_file() || path.extension() != Some(OsStr::new("torrent")) { + return; + } + + if self.torrents % 10000 == 0 { + eprintln!("Processing torrent {}...", self.torrents); + } + + self.torrents += 1; + + let contents = match fs::read(&path) { + Ok(contents) => contents, + Err(_) => { + self.io_errors += 1; + return; + } + }; + + let value = match bencode::Value::decode(&contents) { + Ok(value) => value, + Err(_) => { + self.bencode_decode_errors += 1; + return; + } + }; + + self.extract(&value); + } + + fn extract(&mut self, value: &bencode::Value) { + use bencode::Value::*; + + let matches = self.regex_set.matches(&self.current_path); + + for i in matches.iter() { + let pattern = &self.regex_set.patterns()[i]; + if let Some(values) = self.values.get_mut(pattern) { + values.push(value.to_string()); + } else { + self.values.insert(pattern.clone(), vec![value.to_string()]); + } + } + + let starting_length = self.current_path.len(); + + if let Dict(items) = value { + for (key, value) in items { + match String::from_utf8_lossy(key) { + Cow::Borrowed(s) => self.current_path.push_str(s), + Cow::Owned(s) => self.current_path.push_str(&s), + } + self.paths.increment_ref(&self.current_path); + self.current_path.push('/'); + self.extract(value); + self.current_path.truncate(starting_length); + } + } else if let List(values) = value { + if self.current_path.pop().is_some() { + self.current_path.push('*'); + } + for value in values { + self.extract(value); + } + self.current_path.truncate(starting_length); + } + } +} diff --git a/tmp/.gitignore b/tmp/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore