Initial commit
type: added
This commit is contained in:
commit
98335f435d
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
**/*.rs.bk
|
||||
notes.md
|
6
CONTRIBUTING
Normal file
6
CONTRIBUTING
Normal file
|
@ -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.
|
558
Cargo.lock
generated
Normal file
558
Cargo.lock
generated
Normal file
|
@ -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"
|
29
Cargo.toml
Normal file
29
Cargo.toml
Normal file
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "imdl"
|
||||
version = "0.0.0"
|
||||
description = "📦 A 40' shipping container for the internet"
|
||||
authors = ["Casey Rodarmor <casey@rodarmor.com>"]
|
||||
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"]
|
121
LICENSE
Normal file
121
LICENSE
Normal file
|
@ -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.
|
1
README.md
Normal file
1
README.md
Normal file
|
@ -0,0 +1 @@
|
|||
# intermodal: a 40' shipping container for the Internet
|
12
build.rs
Normal file
12
build.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
use std::{error::Error, process::Command, str};
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
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(())
|
||||
}
|
17
justfile
Normal file
17
justfile
Normal file
|
@ -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'
|
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
tab_spaces = 2
|
||||
max_width = 100
|
381
src/bencode.rs
Normal file
381
src/bencode.rs
Normal file
|
@ -0,0 +1,381 @@
|
|||
use crate::common::*;
|
||||
|
||||
use self::Error::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum Value<'buffer> {
|
||||
Int(&'buffer str),
|
||||
List(Vec<Value<'buffer>>),
|
||||
Dict(Vec<(&'buffer [u8], Value<'buffer>)>),
|
||||
Str(&'buffer [u8]),
|
||||
}
|
||||
|
||||
impl<'buffer> Value<'buffer> {
|
||||
pub(crate) fn decode(buffer: &'buffer [u8]) -> Result<Value<'buffer>, Error> {
|
||||
Parser::parse(buffer)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
let mut buffer = Vec::new();
|
||||
self.encode_into(&mut buffer);
|
||||
buffer
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn encode_into(&self, buffer: &mut Vec<u8>) {
|
||||
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<u8>, 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<Value<'buffer>, Error> {
|
||||
let parser = Parser { index: 0, buffer };
|
||||
|
||||
Ok(parser.root()?)
|
||||
}
|
||||
|
||||
fn root(mut self) -> Result<Value<'buffer>, Error> {
|
||||
let root = self.value()?;
|
||||
|
||||
if self.index != self.buffer.len() {
|
||||
return Err(ExtraData { start: self.index });
|
||||
}
|
||||
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
fn value(&mut self) -> Result<Value<'buffer>, 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<u64, Error> {
|
||||
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<Value<'buffer>, 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<Value<'buffer>, 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<Value<'buffer>, 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<Value<'buffer>, 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<u8, Error> {
|
||||
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<bool, Error> {
|
||||
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);
|
||||
}
|
||||
}
|
42
src/common.rs
Normal file
42
src/common.rs
Normal file
|
@ -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;
|
13
src/consts.rs
Normal file
13
src/consts.rs
Normal file
|
@ -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";
|
49
src/environment.rs
Normal file
49
src/environment.rs
Normal file
|
@ -0,0 +1,49 @@
|
|||
use crate::common::*;
|
||||
|
||||
pub(crate) struct Environment {
|
||||
args: Vec<String>,
|
||||
dir: Box<dyn AsRef<Path>>,
|
||||
err: Box<dyn Write>,
|
||||
}
|
||||
|
||||
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<D, E, S, I>(dir: D, err: E, args: I) -> Environment
|
||||
where
|
||||
D: AsRef<Path> + 'static,
|
||||
E: Write + 'static,
|
||||
S: Into<String>,
|
||||
I: IntoIterator<Item = S>,
|
||||
{
|
||||
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<Path>) -> PathBuf {
|
||||
self.dir.as_ref().as_ref().join(path).clean()
|
||||
}
|
||||
}
|
61
src/error.rs
Normal file
61
src/error.rs
Normal file
|
@ -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<clap::Error> for Error {
|
||||
fn from(source: clap::Error) -> Error {
|
||||
Error::Clap { source }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_bencode::Error> for Error {
|
||||
fn from(source: serde_bencode::Error) -> Error {
|
||||
Error::Serialize { source }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SystemTimeError> for Error {
|
||||
fn from(source: SystemTimeError) -> Error {
|
||||
Error::SystemTime { source }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<walkdir::Error> 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")
|
||||
}
|
||||
}
|
||||
}
|
8
src/file_info.rs
Normal file
8
src/file_info.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use crate::common::*;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
pub struct FileInfo {
|
||||
pub length: u64,
|
||||
pub md5sum: Option<String>,
|
||||
pub path: Vec<String>,
|
||||
}
|
131
src/hasher.rs
Normal file
131
src/hasher.rs
Normal file
|
@ -0,0 +1,131 @@
|
|||
use crate::common::*;
|
||||
|
||||
pub(crate) struct Hasher {
|
||||
buffer: Vec<u8>,
|
||||
length: u64,
|
||||
md5sum: bool,
|
||||
piece_bytes_hashed: u64,
|
||||
piece_length: u64,
|
||||
pieces: Vec<u8>,
|
||||
sha1: Sha1,
|
||||
}
|
||||
|
||||
impl Hasher {
|
||||
pub(crate) fn hash(
|
||||
root: &Path,
|
||||
md5sum: bool,
|
||||
piece_length: u64,
|
||||
) -> Result<(Mode, Vec<u8>), 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<u8>), 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<Vec<FileInfo>, 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<md5::Digest>, u64), Error> {
|
||||
self
|
||||
.hash_file_io(file)
|
||||
.context(error::Filesystem { path: file })
|
||||
}
|
||||
|
||||
fn hash_file_io(&mut self, file: &Path) -> io::Result<(Option<md5::Digest>, 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))
|
||||
}
|
||||
}
|
13
src/info.rs
Normal file
13
src/info.rs
Normal file
|
@ -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<u8>,
|
||||
#[serde(flatten)]
|
||||
pub mode: Mode,
|
||||
}
|
30
src/main.rs
Normal file
30
src/main.rs
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
8
src/matches.rs
Normal file
8
src/matches.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
macro_rules! matches {
|
||||
($expression:expr, $( $pattern:pat )|+ $( if $guard: expr )?) => {
|
||||
match $expression {
|
||||
$( $pattern )|+ $( if $guard )? => true,
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
}
|
15
src/metainfo.rs
Normal file
15
src/metainfo.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use crate::common::*;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct Metainfo {
|
||||
pub announce: String,
|
||||
#[serde(rename = "announce list")]
|
||||
pub announce_list: Vec<Vec<String>>,
|
||||
pub comment: Option<String>,
|
||||
#[serde(rename = "created by")]
|
||||
pub created_by: Option<String>,
|
||||
#[serde(rename = "creation date")]
|
||||
pub creation_date: Option<u64>,
|
||||
pub encoding: String,
|
||||
pub info: Info,
|
||||
}
|
8
src/mode.rs
Normal file
8
src/mode.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
use crate::common::*;
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum Mode {
|
||||
Single { length: u64, md5sum: Option<String> },
|
||||
Multiple { files: Vec<FileInfo> },
|
||||
}
|
15
src/opt.rs
Normal file
15
src/opt.rs
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
55
src/path_ext.rs
Normal file
55
src/path_ext.rs
Normal file
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
79
src/reckoner.rs
Normal file
79
src/reckoner.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
use crate::common::*;
|
||||
|
||||
pub(crate) trait Reckoner<K> {
|
||||
fn increment_ref(&mut self, k: &K)
|
||||
where
|
||||
K: Clone;
|
||||
|
||||
fn increment(&mut self, k: K);
|
||||
|
||||
fn increment_multiple<I>(&mut self, i: I)
|
||||
where
|
||||
I: IntoIterator<Item = K>,
|
||||
{
|
||||
for k in i.into_iter() {
|
||||
self.increment(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Ord> Reckoner<K> for BTreeMap<K, u64> {
|
||||
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<K: Hash + Eq> Reckoner<K> for HashMap<K, u64> {
|
||||
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<K: Ord> Reckoner<K> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
14
src/subcommand.rs
Normal file
14
src/subcommand.rs
Normal file
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
11
src/testing.rs
Normal file
11
src/testing.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
use crate::common::*;
|
||||
|
||||
use std::{io::Cursor, iter};
|
||||
|
||||
pub(crate) fn environment(iter: impl IntoIterator<Item = impl Into<String>>) -> Environment {
|
||||
Environment::new(
|
||||
tempfile::tempdir().unwrap(),
|
||||
Cursor::new(Vec::new()),
|
||||
iter::once(String::from("imdl")).chain(iter.into_iter().map(|item| item.into())),
|
||||
)
|
||||
}
|
19
src/torrent.rs
Normal file
19
src/torrent.rs
Normal file
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
541
src/torrent/create.rs
Normal file
541
src/torrent/create.rs
Normal file
|
@ -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<PathBuf>,
|
||||
#[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<String>,
|
||||
#[structopt(name = "PIECE-LENGTH", long = "piece-length", default_value = "524288")]
|
||||
piece_length: u64,
|
||||
#[structopt(name = "ANNOUNCE", long = "announce", required(true))]
|
||||
announce: Vec<String>,
|
||||
#[structopt(name = "MD5SUM", long = "md5sum")]
|
||||
md5sum: bool,
|
||||
#[structopt(name = "NAME", long = "name")]
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
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::<Vec<String>>();
|
||||
|
||||
tier
|
||||
.iter()
|
||||
.map(|announce| announce.parse())
|
||||
.collect::<Result<Vec<Url>, 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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Vec<u8>>();
|
||||
|
||||
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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&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::<Metainfo>(&bytes).unwrap();
|
||||
assert_eq!(
|
||||
metainfo.info.pieces,
|
||||
Sha1::from("abchijxyz").digest().bytes()
|
||||
);
|
||||
assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() })
|
||||
}
|
||||
}
|
171
src/torrent/stats.rs
Normal file
171
src/torrent/stats.rs
Normal file
|
@ -0,0 +1,171 @@
|
|||
use crate::common::*;
|
||||
|
||||
#[derive(StructOpt)]
|
||||
pub(crate) struct Stats {
|
||||
#[structopt(long = "limit", short = "l")]
|
||||
limit: Option<u64>,
|
||||
#[structopt(long = "extract-pattern", short = "e")]
|
||||
extract_patterns: Vec<Regex>,
|
||||
#[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::<Vec<(String, u64)>>();
|
||||
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!("{:<width$} - {}", count, key, width = width);
|
||||
}
|
||||
for (key, count) in paths {
|
||||
if key.starts_with("info/files") {
|
||||
println!("{:<width$} - {}", count, key, width = width);
|
||||
}
|
||||
}
|
||||
|
||||
if !extractor.values.is_empty() {
|
||||
let values = extractor
|
||||
.values
|
||||
.into_iter()
|
||||
.collect::<Vec<(String, Vec<String>)>>();
|
||||
|
||||
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<String, u64>,
|
||||
regex_set: RegexSet,
|
||||
torrents: u64,
|
||||
values: HashMap<String, Vec<String>>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
2
tmp/.gitignore
vendored
Normal file
2
tmp/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
*
|
||||
!.gitignore
|
Loading…
Reference in New Issue
Block a user