Add initial implementation of imdl torrent verify

Adds the command `imdl` torrent verify` to verify the contents of torrents.

This implementation is extremely naive. It does successfully verify torrents,
but it will produce unsatisfying results when a torrent fails verification. In
particular, it won't give any information about which pieces in a file were
corrupt.

type: added
This commit is contained in:
Casey Rodarmor 2020-02-14 00:12:49 -08:00
parent b0f449b6ae
commit 6549850dac
No known key found for this signature in database
GPG key ID: 556186B153EC6FE0
28 changed files with 1011 additions and 397 deletions

98
Cargo.lock generated
View file

@ -27,6 +27,15 @@ dependencies = [
"winapi 0.3.8", "winapi 0.3.8",
] ]
[[package]]
name = "array-init"
version = "0.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23589ecb866b460d3a0f1278834750268c607e8e28a1b982c907219f3178cd72"
dependencies = [
"nodrop",
]
[[package]] [[package]]
name = "atty" name = "atty"
version = "0.2.14" version = "0.2.14"
@ -46,9 +55,9 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.43" version = "0.3.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f80256bc78f67e7df7e36d77366f636ed976895d91fe2ab9efa3973e8fe8c4f" checksum = "e4036b9bf40f3cf16aba72a3d65e8a520fc4bafcdc7079aea8f848c58c5b5536"
dependencies = [ dependencies = [
"backtrace-sys", "backtrace-sys",
"cfg-if", "cfg-if",
@ -69,11 +78,11 @@ dependencies = [
[[package]] [[package]]
name = "bendy" name = "bendy"
version = "0.2.2" version = "0.2.2"
source = "git+https://github.com/casey/bendy.git?branch=value#8009217c7c753e0c3e1b6685bd9e9f980e5d38de" source = "git+https://github.com/casey/bendy.git?branch=serde#5b20df5036bbf2dbb32d60b2b1181bc647ffbf49"
dependencies = [ dependencies = [
"failure", "failure",
"serde", "serde",
"serde_bytes 0.11.3", "serde_bytes",
] ]
[[package]] [[package]]
@ -278,15 +287,15 @@ dependencies = [
"atty", "atty",
"bendy", "bendy",
"chrono", "chrono",
"env_logger",
"globset", "globset",
"libc", "libc",
"md5", "md5",
"pretty_assertions", "pretty_assertions",
"pretty_env_logger",
"regex", "regex",
"serde", "serde",
"serde_bencode", "serde-hex",
"serde_bytes 0.11.3", "serde_bytes",
"serde_with", "serde_with",
"sha1", "sha1",
"snafu", "snafu",
@ -294,6 +303,7 @@ dependencies = [
"structopt", "structopt",
"syn", "syn",
"tempfile", "tempfile",
"temptree",
"unicode-width", "unicode-width",
"url", "url",
"walkdir", "walkdir",
@ -336,6 +346,12 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
[[package]]
name = "maybe-uninit"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]] [[package]]
name = "md5" name = "md5"
version = "0.7.0" version = "0.7.0"
@ -344,9 +360,15 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.3.0" version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3197e20c7edb283f87c071ddfc7a2cca8f8e0b888c242959846a6fce03c72223" checksum = "53445de381a1f436797497c61d851644d0e8e88e6140f22872ad33a704933978"
[[package]]
name = "nodrop"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]] [[package]]
name = "num-integer" name = "num-integer"
@ -401,10 +423,20 @@ dependencies = [
] ]
[[package]] [[package]]
name = "proc-macro-error" name = "pretty_env_logger"
version = "0.4.8" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875077759af22fa20b610ad4471d8155b321c89c3f2785526c9839b099be4e0a" checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d"
dependencies = [
"env_logger",
"log",
]
[[package]]
name = "proc-macro-error"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "052b3c9af39c7e5e94245f820530487d19eb285faedcb40e0c3275132293f242"
dependencies = [ dependencies = [
"proc-macro-error-attr", "proc-macro-error-attr",
"proc-macro2", "proc-macro2",
@ -415,9 +447,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro-error-attr" name = "proc-macro-error-attr"
version = "0.4.8" version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5717d9fa2664351a01ed73ba5ef6df09c01a521cb42cb65a061432a826f3c7a" checksum = "d175bef481c7902e63e3165627123fff3502f06ac043d3ef42d08c1246da9253"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -560,22 +592,14 @@ dependencies = [
] ]
[[package]] [[package]]
name = "serde_bencode" name = "serde-hex"
version = "0.2.1" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "315c49c11b6b10acc209df75b757ee70957b911ecd0e29bcbf2b735ebd580d45" checksum = "ca37e3e4d1b39afd7ff11ee4e947efae85adfddf4841787bfa47c470e96dc26d"
dependencies = [
"serde",
"serde_bytes 0.10.5",
]
[[package]]
name = "serde_bytes"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "defbb8a83d7f34cc8380751eeb892b825944222888aff18996ea7901f24aec88"
dependencies = [ dependencies = [
"array-init",
"serde", "serde",
"smallvec 0.6.13",
] ]
[[package]] [[package]]
@ -625,6 +649,15 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d"
[[package]]
name = "smallvec"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7b0758c52e15a8b5e3691eae6cc559f08eee9406e548a4477ba4e67770a82b6"
dependencies = [
"maybe-uninit",
]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.2.0" version = "1.2.0"
@ -736,6 +769,15 @@ dependencies = [
"winapi 0.3.8", "winapi 0.3.8",
] ]
[[package]]
name = "temptree"
version = "0.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b41283c421539cd57fda2bdae139a0e08992dba973cd4ba859765c867ad591"
dependencies = [
"tempfile",
]
[[package]] [[package]]
name = "term_size" name = "term_size"
version = "0.3.1" version = "0.3.1"
@ -801,7 +843,7 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4"
dependencies = [ dependencies = [
"smallvec", "smallvec 1.2.0",
] ]
[[package]] [[package]]

View file

@ -13,31 +13,31 @@ edition = "2018"
default-run = "imdl" default-run = "imdl"
[dependencies] [dependencies]
ansi_term = "0.12" ansi_term = "0.12.0"
atty = "0.2" atty = "0.2.0"
chrono = "0.4.1" chrono = "0.4.1"
env_logger = "0.7" globset = "0.4.0"
globset = "0.4" libc = "0.2.0"
libc = "0.2" md5 = "0.7.0"
md5 = "0.7" pretty_assertions = "0.6.0"
pretty_assertions = "0.6" pretty_env_logger = "0.4.0"
regex = "1" regex = "1.0.0"
serde_bencode = "0.2" serde-hex = "0.1.0"
serde_bytes = "0.11" serde_bytes = "0.11.0"
serde_with = "1.4" serde_with = "1.4.0"
sha1 = "0.6" sha1 = "0.6.0"
snafu = "0.6" snafu = "0.6.0"
static_assertions = "1" static_assertions = "1.0.0"
syn = "1.0.14" syn = "1.0.14"
tempfile = "3" tempfile = "3.0.0"
unicode-width = "0.1" unicode-width = "0.1.0"
url = "2" url = "2.0.0"
walkdir = "2.1" walkdir = "2.1.0"
[dependencies.bendy] [dependencies.bendy]
version = "0.2.2" version = "0.2.2"
git = "https://github.com/casey/bendy.git" git = "https://github.com/casey/bendy.git"
branch = "value" branch = "serde"
features = ["serde"] features = ["serde"]
[dependencies.serde] [dependencies.serde]
@ -45,9 +45,12 @@ version = "1.0.103"
features = ["derive"] features = ["derive"]
[dependencies.structopt] [dependencies.structopt]
version = "0.3" version = "0.3.0"
features = ["default", "wrap_help"] features = ["default", "wrap_help"]
[dev-dependencies]
temptree = "0.0.0"
[workspace] [workspace]
members = [ members = [
# generate table of contents and table of supported BEPs in README.md # generate table of contents and table of supported BEPs in README.md

View file

@ -2,6 +2,10 @@ default: watch
version := `sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/v\1/p' Cargo.toml | head -1` version := `sed -En 's/version[[:space:]]*=[[:space:]]*"([^"]+)"/v\1/p' Cargo.toml | head -1`
bt := "0"
export RUST_BACKTRACE := bt
# watch filesystem for changes and rerun tests # watch filesystem for changes and rerun tests
watch: watch:
cargo watch --exec test cargo watch --exec test

View file

@ -1,22 +1,17 @@
use crate::common::*; use crate::common::*;
const KI: u128 = 1 << 10; const KI: u64 = 1 << 10;
const MI: u128 = KI << 10; const MI: u64 = KI << 10;
const GI: u128 = MI << 10; const GI: u64 = MI << 10;
const TI: u128 = GI << 10; const TI: u64 = GI << 10;
const PI: u128 = TI << 10; const PI: u64 = TI << 10;
const EI: u128 = PI << 10; const EI: u64 = PI << 10;
const ZI: u128 = EI << 10;
const YI: u128 = ZI << 10;
#[derive(Debug, PartialEq, Copy, Clone, PartialOrd, Ord, Eq)] #[serde(transparent)]
pub(crate) struct Bytes(pub(crate) u128); #[derive(Debug, PartialEq, Copy, Clone, PartialOrd, Ord, Eq, Serialize, Deserialize, Default)]
pub(crate) struct Bytes(pub(crate) u64);
impl Bytes { impl Bytes {
pub(crate) fn is_power_of_two(self) -> bool {
self.0 == 0 || self.0 & (self.0 - 1) == 0
}
pub(crate) fn kib() -> Self { pub(crate) fn kib() -> Self {
Bytes::from(KI) Bytes::from(KI)
} }
@ -25,67 +20,86 @@ impl Bytes {
Bytes::from(MI) Bytes::from(MI)
} }
pub(crate) fn count(self) -> u128 { pub(crate) fn count(self) -> u64 {
self.0 self.0
} }
pub(crate) fn as_piece_length(self) -> Result<u32> {
self
.count()
.try_into()
.context(error::PieceLengthTooLarge { bytes: self })
}
} }
fn float_to_int(x: f64) -> u128 { fn float_to_int(x: f64) -> u64 {
#![allow( #![allow(
clippy::as_conversions, clippy::as_conversions,
clippy::cast_sign_loss, clippy::cast_sign_loss,
clippy::cast_possible_truncation clippy::cast_possible_truncation
)] )]
x as u128 x as u64
} }
fn int_to_float(x: u128) -> f64 { fn int_to_float(x: u64) -> f64 {
#![allow(clippy::as_conversions, clippy::cast_precision_loss)] #![allow(clippy::as_conversions, clippy::cast_precision_loss)]
x as f64 x as f64
} }
impl<I: Into<u128>> From<I> for Bytes { impl<I: Into<u64>> From<I> for Bytes {
fn from(n: I) -> Bytes { fn from(n: I) -> Bytes {
Bytes(n.into()) Bytes(n.into())
} }
} }
impl Div<Bytes> for Bytes { impl Div<Bytes> for Bytes {
type Output = u128; type Output = u64;
fn div(self, rhs: Bytes) -> u128 { fn div(self, rhs: Bytes) -> u64 {
self.0 / rhs.0 self.0 / rhs.0
} }
} }
impl Div<u128> for Bytes { impl Div<u64> for Bytes {
type Output = Bytes; type Output = Bytes;
fn div(self, rhs: u128) -> Bytes { fn div(self, rhs: u64) -> Bytes {
Bytes::from(self.0 / rhs) Bytes::from(self.0 / rhs)
} }
} }
impl DivAssign<u128> for Bytes { impl DivAssign<u64> for Bytes {
fn div_assign(&mut self, rhs: u128) { fn div_assign(&mut self, rhs: u64) {
self.0 /= rhs; self.0 /= rhs;
} }
} }
impl Mul<u128> for Bytes { impl Mul<u64> for Bytes {
type Output = Bytes; type Output = Bytes;
fn mul(self, rhs: u128) -> Self { fn mul(self, rhs: u64) -> Self {
Bytes::from(self.0 * rhs) Bytes::from(self.0 * rhs)
} }
} }
impl MulAssign<u128> for Bytes { impl MulAssign<u64> for Bytes {
fn mul_assign(&mut self, rhs: u128) { fn mul_assign(&mut self, rhs: u64) {
self.0 *= rhs; self.0 *= rhs;
} }
} }
impl AddAssign<Bytes> for Bytes {
fn add_assign(&mut self, rhs: Bytes) {
self.0 += rhs.0;
}
}
impl SubAssign<u64> for Bytes {
fn sub_assign(&mut self, rhs: u64) {
self.0 -= rhs;
}
}
impl FromStr for Bytes { impl FromStr for Bytes {
type Err = Error; type Err = Error;
@ -115,8 +129,6 @@ impl FromStr for Bytes {
"tib" => TI, "tib" => TI,
"pib" => PI, "pib" => PI,
"eib" => EI, "eib" => EI,
"zib" => ZI,
"yib" => YI,
_ => { _ => {
return Err(Error::ByteSuffix { return Err(Error::ByteSuffix {
text: text.to_owned(), text: text.to_owned(),
@ -129,9 +141,24 @@ impl FromStr for Bytes {
} }
} }
impl Sum for Bytes {
fn sum<I>(iter: I) -> Self
where
I: Iterator<Item = Self>,
{
let mut sum = Bytes(0);
for item in iter {
sum += item;
}
sum
}
}
impl Display for Bytes { impl Display for Bytes {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
const DISPLAY_SUFFIXES: &[&str] = &["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; const DISPLAY_SUFFIXES: &[&str] = &["KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
let mut value = int_to_float(self.0); let mut value = int_to_float(self.0);
@ -164,7 +191,7 @@ mod tests {
#[test] #[test]
fn ok() { fn ok() {
const CASES: &[(&str, u128)] = &[ const CASES: &[(&str, u64)] = &[
("0", 0), ("0", 0),
("0kib", 0), ("0kib", 0),
("1", 1), ("1", 1),
@ -175,7 +202,6 @@ mod tests {
("1KiB", KI), ("1KiB", KI),
("12kib", 12 * KI), ("12kib", 12 * KI),
("1.5mib", 1 * MI + 512 * KI), ("1.5mib", 1 * MI + 512 * KI),
("1yib", 1 * YI),
]; ];
for (text, value) in CASES { for (text, value) in CASES {
@ -216,7 +242,17 @@ mod tests {
assert_eq!(Bytes(TI).to_string(), "1 TiB"); assert_eq!(Bytes(TI).to_string(), "1 TiB");
assert_eq!(Bytes(PI).to_string(), "1 PiB"); assert_eq!(Bytes(PI).to_string(), "1 PiB");
assert_eq!(Bytes(EI).to_string(), "1 EiB"); assert_eq!(Bytes(EI).to_string(), "1 EiB");
assert_eq!(Bytes(ZI).to_string(), "1 ZiB"); }
assert_eq!(Bytes(YI).to_string(), "1 YiB");
#[test]
fn bencode() {
assert_eq!(
bendy::serde::ser::to_bytes(&Bytes::kib()).unwrap(),
b"i1024e"
);
assert_eq!(
Bytes::kib(),
bendy::serde::de::from_bytes(b"i1024e").unwrap(),
);
} }
} }

View file

@ -10,8 +10,9 @@ pub(crate) use std::{
fs::{self, File}, fs::{self, File},
hash::Hash, hash::Hash,
io::{self, Read, Write}, io::{self, Read, Write},
num::ParseFloatError, iter::{self, Sum},
ops::{Div, DivAssign, Mul, MulAssign}, num::{ParseFloatError, TryFromIntError},
ops::{AddAssign, Div, DivAssign, Mul, MulAssign, SubAssign},
path::{self, Path, PathBuf}, path::{self, Path, PathBuf},
process::{self, Command, ExitStatus}, process::{self, Command, ExitStatus},
str::{self, FromStr}, str::{self, FromStr},
@ -25,8 +26,9 @@ pub(crate) use chrono::{TimeZone, Utc};
pub(crate) use globset::{Glob, GlobMatcher}; pub(crate) use globset::{Glob, GlobMatcher};
pub(crate) use libc::EXIT_FAILURE; pub(crate) use libc::EXIT_FAILURE;
pub(crate) use regex::{Regex, RegexSet}; pub(crate) use regex::{Regex, RegexSet};
pub(crate) use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub(crate) use serde::{Deserialize, Serialize};
pub(crate) use serde_with::skip_serializing_none; pub(crate) use serde_hex::SerHex;
pub(crate) use serde_with::rust::unwrap_or_skip;
pub(crate) use sha1::Sha1; pub(crate) use sha1::Sha1;
pub(crate) use snafu::{ResultExt, Snafu}; pub(crate) use snafu::{ResultExt, Snafu};
pub(crate) use static_assertions::const_assert; pub(crate) use static_assertions::const_assert;
@ -39,7 +41,7 @@ pub(crate) use url::Url;
pub(crate) use walkdir::WalkDir; pub(crate) use walkdir::WalkDir;
// modules // modules
pub(crate) use crate::{consts, error, inner, use_color}; pub(crate) use crate::{consts, error, use_color};
// traits // traits
pub(crate) use crate::{ pub(crate) use crate::{
@ -49,29 +51,38 @@ pub(crate) use crate::{
// structs and enums // structs and enums
pub(crate) use crate::{ pub(crate) use crate::{
bytes::Bytes, env::Env, error::Error, file_info::FileInfo, file_path::FilePath, files::Files, bytes::Bytes, env::Env, error::Error, file_info::FileInfo, file_path::FilePath,
hasher::Hasher, info::Info, lint::Lint, linter::Linter, metainfo::Metainfo, mode::Mode, opt::Opt, file_status::FileStatus, files::Files, hasher::Hasher, info::Info, lint::Lint, linter::Linter,
piece_length_picker::PieceLengthPicker, platform::Platform, style::Style, table::Table, md5_digest::Md5Digest, metainfo::Metainfo, mode::Mode, opt::Opt,
target::Target, torrent_summary::TorrentSummary, use_color::UseColor, walker::Walker, piece_length_picker::PieceLengthPicker, platform::Platform, status::Status, style::Style,
table::Table, target::Target, torrent_summary::TorrentSummary, use_color::UseColor,
verifier::Verifier, walker::Walker,
}; };
// test stdlib types
#[cfg(test)]
pub(crate) use std::{
cell::RefCell,
io::Cursor,
ops::{Deref, DerefMut},
rc::Rc,
time::{Duration, Instant},
};
// test modules
#[cfg(test)]
pub(crate) use crate::testing;
// test structs and enums
#[cfg(test)]
pub(crate) use crate::{capture::Capture, test_env::TestEnv, test_env_builder::TestEnvBuilder};
// type aliases // type aliases
pub(crate) type Result<T, E = Error> = std::result::Result<T, E>; pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;
#[cfg(test)]
mod test {
// test stdlib types
pub(crate) use std::{
cell::RefCell,
io::Cursor,
ops::{Deref, DerefMut},
rc::Rc,
time::{Duration, Instant},
};
// test dependencies
pub(crate) use tempfile::TempDir;
pub(crate) use temptree::temptree;
// test modules
pub(crate) use crate::testing;
// test structs and enums
pub(crate) use crate::{capture::Capture, test_env::TestEnv, test_env_builder::TestEnvBuilder};
}
#[cfg(test)]
pub(crate) use test::*;

View file

@ -50,12 +50,7 @@ impl Env {
ansi_term::enable_ansi_support().ok(); ansi_term::enable_ansi_support().ok();
#[cfg(not(test))] #[cfg(not(test))]
env_logger::Builder::from_env( pretty_env_logger::init();
env_logger::Env::new()
.filter("JUST_LOG")
.write_style("JUST_LOG_STYLE"),
)
.init();
let opt = Opt::from_iter_safe(&self.args)?; let opt = Opt::from_iter_safe(&self.args)?;

View file

@ -78,7 +78,10 @@ pub(crate) enum Error {
bytes, bytes,
Bytes(u32::max_value().into()) Bytes(u32::max_value().into())
))] ))]
PieceLengthTooLarge { bytes: Bytes }, PieceLengthTooLarge {
bytes: Bytes,
source: TryFromIntError,
},
#[snafu(display("Piece length `{}` is not an even power of two", bytes))] #[snafu(display("Piece length `{}` is not an even power of two", bytes))]
PieceLengthUneven { bytes: Bytes }, PieceLengthUneven { bytes: Bytes },
#[snafu(display("Piece length must be at least 16 KiB"))] #[snafu(display("Piece length must be at least 16 KiB"))]
@ -103,6 +106,8 @@ pub(crate) enum Error {
Unstable { feature: &'static str }, Unstable { feature: &'static str },
#[snafu(display("Unknown lint: {}", text))] #[snafu(display("Unknown lint: {}", text))]
LintUnknown { text: String }, LintUnknown { text: String },
#[snafu(display("Torrent verification failed: {}", status))]
Verify { status: Status },
} }
impl Error { impl Error {

View file

@ -1,10 +1,13 @@
use crate::common::*; use crate::common::*;
#[skip_serializing_none] #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
#[derive(Deserialize, Serialize, Debug, PartialEq)]
pub(crate) struct FileInfo { pub(crate) struct FileInfo {
pub(crate) length: u64, pub(crate) length: Bytes,
pub(crate) path: FilePath, pub(crate) path: FilePath,
#[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] #[serde(
pub(crate) md5sum: Option<String>, skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
)]
pub(crate) md5sum: Option<Md5Digest>,
} }

114
src/file_status.rs Normal file
View file

@ -0,0 +1,114 @@
use crate::common::*;
#[derive(Debug)]
pub(crate) struct FileStatus {
path: PathBuf,
error: Option<io::Error>,
present: bool,
file: bool,
length_expected: Bytes,
length_actual: Option<Bytes>,
md5_expected: Option<Md5Digest>,
md5_actual: Option<Md5Digest>,
}
impl FileStatus {
pub(crate) fn status(
path: &Path,
length_expected: Bytes,
md5_expected: Option<Md5Digest>,
) -> Self {
let mut status = Self::new(path.to_owned(), length_expected, md5_expected);
if let Err(error) = status.verify() {
status.error = Some(error);
}
status
}
fn new(path: PathBuf, length_expected: Bytes, md5_expected: Option<Md5Digest>) -> Self {
Self {
error: None,
file: false,
md5_actual: None,
present: false,
length_actual: None,
length_expected,
md5_expected,
path,
}
}
fn verify(&mut self) -> io::Result<()> {
let metadata = self.path.metadata()?;
self.present = true;
if !metadata.is_file() {
return Ok(());
}
self.file = true;
self.length_actual = Some(metadata.len().into());
if self.md5_expected.is_some() {
let mut reader = File::open(&self.path)?;
let mut context = md5::Context::new();
io::copy(&mut reader, &mut context)?;
self.md5_actual = Some(context.compute().into());
}
Ok(())
}
pub(crate) fn icon(&self) -> char {
if self.error.is_some() {
return '!';
}
if !self.present {
return '?';
}
if !self.file {
return '¿';
}
if !self.md5() {
return 'x';
}
let length = self.length_actual.unwrap();
if length > self.length_expected {
return '+';
}
if length < self.length_expected {
return '-';
}
'♡'
}
fn md5(&self) -> bool {
match (self.md5_actual, self.md5_expected) {
(Some(actual), Some(expected)) => actual == expected,
(None, None) => true,
_ => unreachable!(),
}
}
pub(crate) fn good(&self) -> bool {
self.error.is_none() && self.present && self.file && self.md5()
}
pub(crate) fn bad(&self) -> bool {
!self.good()
}
pub(crate) fn path(&self) -> &Path {
&self.path
}
}

View file

@ -4,8 +4,8 @@ pub(crate) struct Hasher {
buffer: Vec<u8>, buffer: Vec<u8>,
length: u64, length: u64,
md5sum: bool, md5sum: bool,
piece_bytes_hashed: u64, piece_bytes_hashed: usize,
piece_length: u32, piece_length: usize,
pieces: Vec<u8>, pieces: Vec<u8>,
sha1: Sha1, sha1: Sha1,
} }
@ -14,20 +14,20 @@ impl Hasher {
pub(crate) fn hash( pub(crate) fn hash(
files: &Files, files: &Files,
md5sum: bool, md5sum: bool,
piece_length: u32, piece_length: usize,
) -> Result<(Mode, Vec<u8>), Error> { ) -> Result<(Mode, Vec<u8>), Error> {
Self::new(md5sum, piece_length).hash_files(files) Self::new(md5sum, piece_length).hash_files(files)
} }
fn new(md5sum: bool, piece_length: u32) -> Self { fn new(md5sum: bool, piece_length: usize) -> Self {
Self { Self {
buffer: vec![0; piece_length.into_usize()], buffer: vec![0; piece_length],
length: 0, length: 0,
piece_bytes_hashed: 0, piece_bytes_hashed: 0,
pieces: Vec::new(), pieces: Vec::new(),
sha1: Sha1::new(), sha1: Sha1::new(),
md5sum,
piece_length, piece_length,
md5sum,
} }
} }
@ -40,7 +40,7 @@ impl Hasher {
let (md5sum, length) = self.hash_file(files.root())?; let (md5sum, length) = self.hash_file(files.root())?;
Mode::Single { Mode::Single {
md5sum: md5sum.map(|md5sum| format!("{:x}", md5sum)), md5sum: md5sum.map(|md5sum| md5sum.into()),
length, length,
} }
}; };
@ -67,7 +67,7 @@ impl Hasher {
let (md5sum, length) = self.hash_file(&path)?; let (md5sum, length) = self.hash_file(&path)?;
files.push(FileInfo { files.push(FileInfo {
md5sum: md5sum.map(|md5sum| format!("{:x}", md5sum)), md5sum: md5sum.map(|md5sum| md5sum.into()),
path: file_path.clone(), path: file_path.clone(),
length, length,
}); });
@ -76,13 +76,13 @@ impl Hasher {
Ok(files) Ok(files)
} }
fn hash_file(&mut self, file: &Path) -> Result<(Option<md5::Digest>, u64), Error> { fn hash_file(&mut self, file: &Path) -> Result<(Option<md5::Digest>, Bytes), Error> {
self self
.hash_file_io(file) .hash_file_io(file)
.context(error::Filesystem { path: file }) .context(error::Filesystem { path: file })
} }
fn hash_file_io(&mut self, file: &Path) -> io::Result<(Option<md5::Digest>, u64)> { fn hash_file_io(&mut self, file: &Path) -> io::Result<(Option<md5::Digest>, Bytes)> {
let length = file.metadata()?.len(); let length = file.metadata()?.len();
let mut remaining = length; let mut remaining = length;
@ -100,6 +100,7 @@ impl Hasher {
.min(self.buffer.len().into_u64()) .min(self.buffer.len().into_u64())
.try_into() .try_into()
.unwrap(); .unwrap();
let buffer = &mut self.buffer[0..to_buffer]; let buffer = &mut self.buffer[0..to_buffer];
file.read_exact(buffer)?; file.read_exact(buffer)?;
@ -109,7 +110,7 @@ impl Hasher {
self.piece_bytes_hashed += 1; self.piece_bytes_hashed += 1;
if self.piece_bytes_hashed == self.piece_length.into() { if self.piece_bytes_hashed == self.piece_length {
self.pieces.extend(&self.sha1.digest().bytes()); self.pieces.extend(&self.sha1.digest().bytes());
self.sha1.reset(); self.sha1.reset();
self.piece_bytes_hashed = 0; self.piece_bytes_hashed = 0;
@ -125,6 +126,6 @@ impl Hasher {
self.length += length; self.length += length;
Ok((md5.map(md5::Context::compute), length)) Ok((md5.map(md5::Context::compute), Bytes::from(length)))
} }
} }

View file

@ -1,14 +1,21 @@
use crate::common::*; use crate::common::*;
#[skip_serializing_none] #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
#[derive(Deserialize, Serialize, Debug, PartialEq)]
pub(crate) struct Info { pub(crate) struct Info {
#[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] #[serde(
pub(crate) private: Option<u8>, skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
)]
pub(crate) private: Option<bool>,
#[serde(rename = "piece length")] #[serde(rename = "piece length")]
pub(crate) piece_length: u32, pub(crate) piece_length: Bytes,
pub(crate) name: String, pub(crate) name: String,
#[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] #[serde(
skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
)]
pub(crate) source: Option<String>, pub(crate) source: Option<String>,
#[serde(with = "serde_bytes")] #[serde(with = "serde_bytes")]
pub(crate) pieces: Vec<u8>, pub(crate) pieces: Vec<u8>,

View file

@ -1,52 +0,0 @@
use crate::common::*;
pub(crate) fn serialize<T, S>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: Serialize,
{
value.as_ref().unwrap().serialize(serializer)
}
pub(crate) fn deserialize<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
Ok(Some(T::deserialize(deserializer)?))
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::fmt::Debug;
fn case<T>(value: T, expected: impl AsRef<[u8]>)
where
T: Serialize + DeserializeOwned + PartialEq + Debug,
{
let serialized = bendy::serde::ser::to_bytes(&value).unwrap();
assert_eq!(serialized, expected.as_ref());
let deserialized = bendy::serde::de::from_bytes(&serialized).unwrap();
assert_eq!(value, deserialized);
}
#[test]
fn serialize() {
#[derive(Deserialize, Serialize, Debug, PartialEq)]
struct Foo {
#[serde(skip_serializing_if = "Option::is_none", default, with = "super")]
pub(crate) bar: Option<u8>,
}
let none = Foo { bar: None };
case(none, b"de");
let some = Foo { bar: Some(1) };
case(some, b"d3:bari1ee");
}
}

View file

@ -61,14 +61,15 @@ mod env;
mod error; mod error;
mod file_info; mod file_info;
mod file_path; mod file_path;
mod file_status;
mod files; mod files;
mod hasher; mod hasher;
mod info; mod info;
mod inner;
mod into_u64; mod into_u64;
mod into_usize; mod into_usize;
mod lint; mod lint;
mod linter; mod linter;
mod md5_digest;
mod metainfo; mod metainfo;
mod mode; mod mode;
mod opt; mod opt;
@ -77,11 +78,13 @@ mod piece_length_picker;
mod platform; mod platform;
mod platform_interface; mod platform_interface;
mod reckoner; mod reckoner;
mod status;
mod style; mod style;
mod table; mod table;
mod target; mod target;
mod torrent_summary; mod torrent_summary;
mod use_color; mod use_color;
mod verifier;
mod walker; mod walker;
fn main() { fn main() {

53
src/md5_digest.rs Normal file
View file

@ -0,0 +1,53 @@
use crate::common::*;
#[serde(transparent)]
#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Copy, Clone)]
pub(crate) struct Md5Digest {
#[serde(with = "SerHex::<serde_hex::Strict>")]
bytes: [u8; 16],
}
impl Md5Digest {
#[cfg(test)]
pub(crate) fn from_hex(hex: &str) -> Md5Digest {
assert_eq!(hex.len(), 32);
let mut bytes: [u8; 16] = [0; 16];
for n in 0..16 {
let i = n * 2;
bytes[n] = u8::from_str_radix(&hex[i..i + 2], 16).unwrap();
}
Md5Digest { bytes }
}
}
impl From<md5::Digest> for Md5Digest {
fn from(digest: md5::Digest) -> Self {
Md5Digest { bytes: digest.0 }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ser() {
let digest = Md5Digest {
bytes: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
};
let bytes = bendy::serde::ser::to_bytes(&digest).unwrap();
assert_eq!(
str::from_utf8(&bytes).unwrap(),
"32:000102030405060708090a0b0c0d0e0f"
);
let string_bytes = bendy::serde::ser::to_bytes(&"000102030405060708090a0b0c0d0e0f").unwrap();
assert_eq!(bytes, string_bytes);
}
}

View file

@ -1,27 +1,45 @@
use crate::common::*; use crate::common::*;
#[skip_serializing_none] #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
#[derive(Deserialize, Serialize, Debug, PartialEq)]
pub(crate) struct Metainfo { pub(crate) struct Metainfo {
pub(crate) announce: String, pub(crate) announce: String,
#[serde(rename = "announce-list")] #[serde(rename = "announce-list")]
#[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] #[serde(
skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
)]
pub(crate) announce_list: Option<Vec<Vec<String>>>, pub(crate) announce_list: Option<Vec<Vec<String>>>,
#[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] #[serde(
skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
)]
pub(crate) comment: Option<String>, pub(crate) comment: Option<String>,
#[serde(rename = "created by")] #[serde(rename = "created by")]
#[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] #[serde(
skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
)]
pub(crate) created_by: Option<String>, pub(crate) created_by: Option<String>,
#[serde(rename = "creation date")] #[serde(rename = "creation date")]
#[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] #[serde(
skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
)]
pub(crate) creation_date: Option<u64>, pub(crate) creation_date: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] #[serde(
skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
)]
pub(crate) encoding: Option<String>, pub(crate) encoding: Option<String>,
pub(crate) info: Info, pub(crate) info: Info,
} }
impl Metainfo { impl Metainfo {
#[cfg(test)]
pub(crate) fn load(path: impl AsRef<Path>) -> Result<Metainfo, Error> { pub(crate) fn load(path: impl AsRef<Path>) -> Result<Metainfo, Error> {
let path = path.as_ref(); let path = path.as_ref();
let bytes = fs::read(path).context(error::Filesystem { path })?; let bytes = fs::read(path).context(error::Filesystem { path })?;
@ -31,45 +49,113 @@ impl Metainfo {
#[cfg(test)] #[cfg(test)]
pub(crate) fn dump(&self, path: impl AsRef<Path>) -> Result<(), Error> { pub(crate) fn dump(&self, path: impl AsRef<Path>) -> Result<(), Error> {
let path = path.as_ref(); let path = path.as_ref();
let bendy = bendy::serde::ser::to_bytes(&self).context(error::MetainfoSerialize)?; let bencode = bendy::serde::ser::to_bytes(&self).context(error::MetainfoSerialize)?;
let serde_bencode = serde_bencode::ser::to_bytes(&self).unwrap(); fs::write(path, &bencode).context(error::Filesystem { path })?;
if bendy != serde_bencode {
panic!(
"Serialize bendy != serde_bencode:\n{}\n{}",
String::from_utf8_lossy(&bendy),
String::from_utf8_lossy(&serde_bencode)
);
}
fs::write(path, &bendy).context(error::Filesystem { path })?;
Ok(()) Ok(())
} }
pub(crate) fn deserialize(path: impl AsRef<Path>, bytes: &[u8]) -> Result<Metainfo, Error> { pub(crate) fn deserialize(path: impl AsRef<Path>, bytes: &[u8]) -> Result<Metainfo, Error> {
let path = path.as_ref(); let path = path.as_ref();
let bendy = bendy::serde::de::from_bytes(&bytes).context(error::MetainfoLoad { path })?; bendy::serde::de::from_bytes(&bytes).context(error::MetainfoLoad { path })
let serde_bencode = serde_bencode::de::from_bytes(&bytes).unwrap();
assert_eq!(bendy, serde_bencode);
Ok(bendy)
} }
pub(crate) fn serialize(&self) -> Result<Vec<u8>, Error> { pub(crate) fn serialize(&self) -> Result<Vec<u8>, Error> {
let bendy = bendy::serde::ser::to_bytes(&self).context(error::MetainfoSerialize)?; bendy::serde::ser::to_bytes(&self).context(error::MetainfoSerialize)
let serde_bencode = serde_bencode::ser::to_bytes(&self).unwrap();
if bendy != serde_bencode {
panic!(
"Serialize bendy != serde_bencode:\n{}\n{}",
String::from_utf8_lossy(&bendy),
String::from_utf8_lossy(&serde_bencode)
);
}
Ok(bendy)
} }
#[cfg(test)] #[cfg(test)]
pub(crate) fn from_bytes(bytes: &[u8]) -> Metainfo { pub(crate) fn from_bytes(bytes: &[u8]) -> Metainfo {
let bendy = bendy::serde::de::from_bytes(bytes).unwrap(); bendy::serde::de::from_bytes(bytes).unwrap()
let serde_bencode = serde_bencode::de::from_bytes(bytes).unwrap(); }
assert_eq!(bendy, serde_bencode);
bendy pub(crate) fn files<'a>(
&'a self,
base: &'a Path,
) -> Box<dyn Iterator<Item = (PathBuf, Bytes, Option<Md5Digest>)> + 'a> {
match &self.info.mode {
Mode::Single { length, md5sum } => Box::new(iter::once((base.to_owned(), *length, *md5sum))),
Mode::Multiple { files } => {
let base = base.to_owned();
Box::new(
files
.iter()
.map(move |file| (file.path.absolute(&base), file.length, file.md5sum)),
)
}
}
}
pub(crate) fn verify(&self, base: &Path) -> Result<Status> {
Verifier::verify(self, base)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_single() {
let value = Metainfo {
announce: "announce".into(),
announce_list: Some(vec![vec!["announce".into(), "b".into()], vec!["c".into()]]),
comment: Some("comment".into()),
created_by: Some("created by".into()),
creation_date: Some(1),
encoding: Some("UTF-8".into()),
info: Info {
private: Some(true),
piece_length: Bytes(16 * 1024),
source: Some("source".into()),
name: "foo".into(),
pieces: vec![
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
],
mode: Mode::Single {
length: Bytes(20),
md5sum: None,
},
},
};
let bencode = bendy::serde::ser::to_bytes(&value).unwrap();
let deserialized = bendy::serde::de::from_bytes(&bencode).unwrap();
assert_eq!(value, deserialized);
}
#[test]
fn round_trip_multiple() {
let value = Metainfo {
announce: "announce".into(),
announce_list: Some(vec![vec!["announce".into(), "b".into()], vec!["c".into()]]),
comment: Some("comment".into()),
created_by: Some("created by".into()),
creation_date: Some(1),
encoding: Some("UTF-8".into()),
info: Info {
private: Some(true),
piece_length: Bytes(16 * 1024),
source: Some("source".into()),
name: "foo".into(),
pieces: vec![
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
],
mode: Mode::Multiple {
files: vec![FileInfo {
length: Bytes(10),
path: FilePath::from_components(&["foo", "bar"]),
md5sum: Some(Md5Digest::from_hex("000102030405060708090a0b0c0d0e0f")),
}],
},
},
};
let bencode = bendy::serde::ser::to_bytes(&value).unwrap();
let deserialized = bendy::serde::de::from_bytes(&bencode).unwrap();
assert_eq!(value, deserialized);
} }
} }

View file

@ -1,13 +1,16 @@
use crate::common::*; use crate::common::*;
#[skip_serializing_none]
#[serde(untagged)] #[serde(untagged)]
#[derive(Deserialize, Serialize, Debug, PartialEq)] #[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
pub(crate) enum Mode { pub(crate) enum Mode {
Single { Single {
length: u64, length: Bytes,
#[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] #[serde(
md5sum: Option<String>, skip_serializing_if = "Option::is_none",
default,
with = "unwrap_or_skip"
)]
md5sum: Option<Md5Digest>,
}, },
Multiple { Multiple {
files: Vec<FileInfo>, files: Vec<FileInfo>,
@ -17,8 +20,79 @@ pub(crate) enum Mode {
impl Mode { impl Mode {
pub(crate) fn total_size(&self) -> Bytes { pub(crate) fn total_size(&self) -> Bytes {
match self { match self {
Self::Single { length, .. } => Bytes::from(*length), Self::Single { length, .. } => *length,
Self::Multiple { files } => Bytes::from(files.iter().map(|file| file.length).sum::<u64>()), Self::Multiple { files } => files.iter().map(|file| file.length).sum(),
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_no_md5sum() {
let input = Mode::Single {
length: Bytes(10),
md5sum: None,
};
let have = bendy::serde::ser::to_bytes(&input).unwrap();
assert_eq!(str::from_utf8(&have).unwrap(), "d6:lengthi10ee");
let output: Mode = bendy::serde::de::from_bytes(&have).unwrap();
assert_eq!(output, input);
}
#[test]
fn single_with_md5sum() {
let input = Mode::Single {
length: Bytes(10),
md5sum: Some(Md5Digest::from_hex("000102030405060708090a0b0c0d0e0f")),
};
let have = bendy::serde::ser::to_bytes(&input).unwrap();
assert_eq!(
str::from_utf8(&have).unwrap(),
"d6:lengthi10e6:md5sum32:000102030405060708090a0b0c0d0e0fe"
);
let output: Mode = bendy::serde::de::from_bytes(&have).unwrap();
assert_eq!(output, input);
}
#[test]
fn round_trip_single() {
let value = Mode::Single {
length: Bytes(10),
md5sum: Some(Md5Digest::from_hex("000102030405060708090a0b0c0d0e0f")),
};
let bencode = bendy::serde::ser::to_bytes(&value).unwrap();
let deserialized = bendy::serde::de::from_bytes(&bencode).unwrap();
assert_eq!(value, deserialized);
}
#[test]
fn round_trip_multiple() {
let value = Mode::Multiple {
files: vec![FileInfo {
length: Bytes(10),
path: FilePath::from_components(&["foo", "bar"]),
md5sum: Some(Md5Digest::from_hex("000102030405060708090a0b0c0d0e0f")),
}],
};
let bencode = bendy::serde::ser::to_bytes(&value).unwrap();
let deserialized = bendy::serde::de::from_bytes(&bencode).unwrap();
assert_eq!(value, deserialized);
}
}

View file

@ -4,6 +4,7 @@ mod create;
mod piece_length; mod piece_length;
mod show; mod show;
mod stats; mod stats;
mod verify;
#[derive(StructOpt)] #[derive(StructOpt)]
#[structopt( #[structopt(
@ -17,6 +18,7 @@ pub(crate) enum Torrent {
PieceLength(piece_length::PieceLength), PieceLength(piece_length::PieceLength),
Show(show::Show), Show(show::Show),
Stats(stats::Stats), Stats(stats::Stats),
Verify(verify::Verify),
} }
impl Torrent { impl Torrent {
@ -26,6 +28,7 @@ impl Torrent {
Self::PieceLength(piece_length) => piece_length.run(env), Self::PieceLength(piece_length) => piece_length.run(env),
Self::Show(show) => show.run(env), Self::Show(show) => show.run(env),
Self::Stats(stats) => stats.run(env, unstable), Self::Stats(stats) => stats.run(env, unstable),
Self::Verify(verify) => verify.run(env),
} }
} }
} }

View file

@ -161,24 +161,17 @@ impl Create {
let mut linter = Linter::new(); let mut linter = Linter::new();
linter.allow(self.allowed_lints.iter().cloned()); linter.allow(self.allowed_lints.iter().cloned());
if linter.is_denied(Lint::UnevenPieceLength) && !piece_length.is_power_of_two() { if piece_length.count() == 0 {
return Err(Error::PieceLengthZero);
}
if linter.is_denied(Lint::UnevenPieceLength) && !piece_length.count().is_power_of_two() {
return Err(Error::PieceLengthUneven { return Err(Error::PieceLengthUneven {
bytes: piece_length, bytes: piece_length,
}); });
} }
let piece_length: u32 = piece_length if linter.is_denied(Lint::SmallPieceLength) && piece_length.count() < 16 * 1024 {
.0
.try_into()
.map_err(|_| Error::PieceLengthTooLarge {
bytes: piece_length,
})?;
if piece_length == 0 {
return Err(Error::PieceLengthZero);
}
if linter.is_denied(Lint::SmallPieceLength) && piece_length < 16 * 1024 {
return Err(Error::PieceLengthSmall); return Err(Error::PieceLengthSmall);
} }
@ -220,7 +213,7 @@ impl Create {
Target::File(input.parent().unwrap().join(torrent_name)) Target::File(input.parent().unwrap().join(torrent_name))
}); });
let private = if self.private { Some(1) } else { None }; let private = if self.private { Some(true) } else { None };
let creation_date = if self.no_creation_date { let creation_date = if self.no_creation_date {
None None
@ -238,7 +231,11 @@ impl Create {
Some(String::from(consts::CREATED_BY_DEFAULT)) Some(String::from(consts::CREATED_BY_DEFAULT))
}; };
let (mode, pieces) = Hasher::hash(&files, self.md5sum, piece_length)?; let (mode, pieces) = Hasher::hash(
&files,
self.md5sum,
piece_length.as_piece_length()?.into_usize(),
)?;
let info = Info { let info = Info {
source: self.source, source: self.source,
@ -280,7 +277,12 @@ impl Create {
.and_then(|mut file| file.write_all(&bytes)) .and_then(|mut file| file.write_all(&bytes))
.context(error::Filesystem { path })?; .context(error::Filesystem { path })?;
#[cfg(test)]
TorrentSummary::from_metainfo(metainfo.clone())?.write(env)?;
#[cfg(not(test))]
TorrentSummary::from_metainfo(metainfo)?.write(env)?; TorrentSummary::from_metainfo(metainfo)?.write(env)?;
if self.open { if self.open {
Platform::open(&path)?; Platform::open(&path)?;
} }
@ -288,6 +290,15 @@ impl Create {
Target::Stdio => env.out.write_all(&bytes).context(error::Stdout)?, Target::Stdio => env.out.write_all(&bytes).context(error::Stdout)?,
} }
#[cfg(test)]
{
let status = metainfo.verify(&input)?;
if !status.good() {
return Err(Error::Verify { status });
}
}
Ok(()) Ok(())
} }
} }
@ -302,9 +313,30 @@ mod tests {
testing::env(["torrent", "create"].iter().chain(args).cloned()) testing::env(["torrent", "create"].iter().chain(args).cloned())
} }
fn tree_environment(args: &[&str], tempdir: TempDir) -> TestEnv {
TestEnvBuilder::new()
.args(["imdl", "torrent", "create"].iter().chain(args).cloned())
.tempdir(tempdir)
.build()
}
macro_rules! env {
{
args: [$($arg:expr),* $(,)?],
tree: {
$($tree:tt)*
} $(,)?
} => {
{
let tempdir = temptree! { $($tree)* };
tree_environment(&[$($arg),*], tempdir)
}
}
}
#[test] #[test]
fn require_input_argument() { fn require_input_argument() {
let mut env = environment(&[]); let mut env = env! { args: [], tree: {} };
assert!(matches!(env.run(), Err(Error::Clap { .. }))); assert!(matches!(env.run(), Err(Error::Clap { .. })));
} }
@ -316,8 +348,12 @@ mod tests {
#[test] #[test]
fn torrent_file_is_bencode_dict() { fn torrent_file_is_bencode_dict() {
let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); let mut env = env! {
fs::write(env.resolve("foo"), "").unwrap(); args: ["--input", "foo", "--announce", "https://bar"],
tree: {
foo: "",
}
};
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let torrent = env.resolve("foo.torrent");
let bytes = fs::read(torrent).unwrap(); let bytes = fs::read(torrent).unwrap();
@ -327,54 +363,70 @@ mod tests {
#[test] #[test]
fn privacy_defaults_to_false() { fn privacy_defaults_to_false() {
let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); let mut env = env! {
fs::write(env.resolve("foo"), "").unwrap(); args: ["--input", "foo", "--announce", "https://bar"],
tree: {
foo: "",
}
};
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.info.private, None); assert_eq!(metainfo.info.private, None);
} }
#[test] #[test]
fn privacy_flag_sets_privacy() { fn privacy_flag_sets_privacy() {
let mut env = environment(&["--input", "foo", "--announce", "http://bar", "--private"]); let mut env = env! {
fs::write(env.resolve("foo"), "").unwrap(); args: ["--input", "foo", "--announce", "https://bar", "--private"],
tree: {
foo: "",
}
};
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.private, Some(true));
assert_eq!(metainfo.info.private, Some(1));
} }
#[test] #[test]
fn tracker_flag_must_be_url() { fn tracker_flag_must_be_url() {
let mut env = environment(&["--input", "foo", "--announce", "bar"]); let mut env = env! {
fs::write(env.resolve("foo"), "").unwrap(); args: ["--input", "foo", "--announce", "bar"],
tree: {
foo: "",
}
};
assert_matches!(env.run(), Err(Error::Clap { .. })); assert_matches!(env.run(), Err(Error::Clap { .. }));
} }
#[test] #[test]
fn announce_single() { fn announce_single() {
let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); let mut env = env! {
fs::write(env.resolve("foo"), "").unwrap(); args: ["--input", "foo", "--announce", "http://bar"],
tree: {
foo: "",
}
};
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.announce, "http://bar/"); assert_eq!(metainfo.announce, "http://bar/");
assert!(metainfo.announce_list.is_none()); assert!(metainfo.announce_list.is_none());
} }
#[test] #[test]
fn announce_udp() { fn announce_udp() {
let mut env = environment(&[ let mut env = env! {
"--input", args: [
"foo", "--input",
"--announce", "foo",
"udp://tracker.opentrackr.org:1337/announce", "--announce",
]); "udp://tracker.opentrackr.org:1337/announce",
fs::write(env.resolve("foo"), "").unwrap(); ],
tree: {
foo: "",
}
};
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!( assert_eq!(
metainfo.announce, metainfo.announce,
"udp://tracker.opentrackr.org:1337/announce" "udp://tracker.opentrackr.org:1337/announce"
@ -387,8 +439,7 @@ mod tests {
let mut env = environment(&["--input", "foo", "--announce", "wss://tracker.btorrent.xyz"]); let mut env = environment(&["--input", "foo", "--announce", "wss://tracker.btorrent.xyz"]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.announce, "wss://tracker.btorrent.xyz/"); assert_eq!(metainfo.announce, "wss://tracker.btorrent.xyz/");
assert!(metainfo.announce_list.is_none()); assert!(metainfo.announce_list.is_none());
} }
@ -405,8 +456,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.announce, "http://bar/"); assert_eq!(metainfo.announce, "http://bar/");
assert_eq!( assert_eq!(
metainfo.announce_list, metainfo.announce_list,
@ -428,8 +478,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.announce, "http://bar/"); assert_eq!(metainfo.announce, "http://bar/");
assert_eq!( assert_eq!(
metainfo.announce_list, metainfo.announce_list,
@ -445,8 +494,7 @@ mod tests {
let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); let mut env = environment(&["--input", "foo", "--announce", "http://bar"]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.comment, None); assert_eq!(metainfo.comment, None);
} }
@ -462,8 +510,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.comment.unwrap(), "Hello, world!"); assert_eq!(metainfo.comment.unwrap(), "Hello, world!");
} }
@ -472,9 +519,8 @@ mod tests {
let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); let mut env = environment(&["--input", "foo", "--announce", "http://bar"]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.piece_length, Bytes::from(16 * 2u32.pow(10)));
assert_eq!(metainfo.info.piece_length, 16 * 2u32.pow(10));
} }
#[test] #[test]
@ -489,9 +535,8 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.piece_length, Bytes(64 * 1024));
assert_eq!(metainfo.info.piece_length, 64 * 1024);
} }
#[test] #[test]
@ -506,9 +551,8 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap(); assert_eq!(metainfo.info.piece_length, Bytes(512 * 1024));
assert_eq!(metainfo.info.piece_length, 512 * 1024);
} }
#[test] #[test]
@ -523,8 +567,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.info.name, "foo"); assert_eq!(metainfo.info.name, "foo");
} }
@ -542,8 +585,7 @@ mod tests {
fs::create_dir(&dir).unwrap(); fs::create_dir(&dir).unwrap();
fs::write(dir.join("bar"), "").unwrap(); fs::write(dir.join("bar"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = dir.join("bar.torrent"); let metainfo = env.load_torrent("foo/bar.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.info.name, "bar"); assert_eq!(metainfo.info.name, "bar");
} }
@ -559,8 +601,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("x.torrent"); env.load_torrent("x.torrent");
Metainfo::load(torrent).unwrap();
} }
#[test] #[test]
@ -568,8 +609,7 @@ mod tests {
let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); let mut env = environment(&["--input", "foo", "--announce", "http://bar"]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.created_by.unwrap(), consts::CREATED_BY_DEFAULT); assert_eq!(metainfo.created_by.unwrap(), consts::CREATED_BY_DEFAULT);
} }
@ -584,8 +624,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.created_by, None); assert_eq!(metainfo.created_by, None);
} }
@ -594,8 +633,7 @@ mod tests {
let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); let mut env = environment(&["--input", "foo", "--announce", "http://bar"]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.encoding, Some("UTF-8".into())); assert_eq!(metainfo.encoding, Some("UTF-8".into()));
} }
@ -608,8 +646,7 @@ mod tests {
.unwrap() .unwrap()
.as_secs(); .as_secs();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert!(metainfo.creation_date.unwrap() < now + 10); assert!(metainfo.creation_date.unwrap() < now + 10);
assert!(metainfo.creation_date.unwrap() > now - 10); assert!(metainfo.creation_date.unwrap() > now - 10);
} }
@ -625,8 +662,7 @@ mod tests {
]); ]);
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.creation_date, None); assert_eq!(metainfo.creation_date, None);
} }
@ -636,13 +672,12 @@ mod tests {
let contents = "bar"; let contents = "bar";
fs::write(env.resolve("foo"), contents).unwrap(); fs::write(env.resolve("foo"), contents).unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes()); assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes());
assert_eq!( assert_eq!(
metainfo.info.mode, metainfo.info.mode,
Mode::Single { Mode::Single {
length: contents.len() as u64, length: Bytes(contents.len() as u64),
md5sum: None, md5sum: None,
} }
) )
@ -663,8 +698,7 @@ mod tests {
let contents = "bar"; let contents = "bar";
fs::write(env.resolve("foo"), contents).unwrap(); fs::write(env.resolve("foo"), contents).unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
let pieces = Sha1::from("b") let pieces = Sha1::from("b")
.digest() .digest()
.bytes() .bytes()
@ -678,7 +712,7 @@ mod tests {
assert_eq!( assert_eq!(
metainfo.info.mode, metainfo.info.mode,
Mode::Single { Mode::Single {
length: contents.len() as u64, length: Bytes(contents.len() as u64),
md5sum: None, md5sum: None,
} }
) )
@ -690,13 +724,12 @@ mod tests {
let contents = ""; let contents = "";
fs::write(env.resolve("foo"), contents).unwrap(); fs::write(env.resolve("foo"), contents).unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.info.pieces.len(), 0); assert_eq!(metainfo.info.pieces.len(), 0);
assert_eq!( assert_eq!(
metainfo.info.mode, metainfo.info.mode,
Mode::Single { Mode::Single {
length: 0, length: Bytes(0),
md5sum: None, md5sum: None,
} }
) )
@ -708,8 +741,7 @@ mod tests {
let dir = env.resolve("foo"); let dir = env.resolve("foo");
fs::create_dir(&dir).unwrap(); fs::create_dir(&dir).unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.info.pieces.len(), 0); assert_eq!(metainfo.info.pieces.len(), 0);
assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() }) assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() })
} }
@ -723,16 +755,15 @@ mod tests {
let contents = "bar"; let contents = "bar";
fs::write(file, contents).unwrap(); fs::write(file, contents).unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes()); assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes());
match metainfo.info.mode { match metainfo.info.mode {
Mode::Multiple { files } => { Mode::Multiple { files } => {
assert_eq!( assert_eq!(
files, files,
&[FileInfo { &[FileInfo {
length: 3, length: Bytes(3),
md5sum: Some("37b51d194a7513e45b56f6524f2d51f2".to_owned()), md5sum: Some(Md5Digest::from_hex("37b51d194a7513e45b56f6524f2d51f2")),
path: FilePath::from_components(&["bar"]), path: FilePath::from_components(&["bar"]),
},] },]
); );
@ -750,15 +781,14 @@ mod tests {
let contents = "bar"; let contents = "bar";
fs::write(file, contents).unwrap(); fs::write(file, contents).unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes()); assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes());
match metainfo.info.mode { match metainfo.info.mode {
Mode::Multiple { files } => { Mode::Multiple { files } => {
assert_eq!( assert_eq!(
files, files,
&[FileInfo { &[FileInfo {
length: 3, length: Bytes(3),
md5sum: None, md5sum: None,
path: FilePath::from_components(&["bar"]), path: FilePath::from_components(&["bar"]),
},] },]
@ -777,8 +807,7 @@ mod tests {
fs::write(dir.join("x"), "xyz").unwrap(); fs::write(dir.join("x"), "xyz").unwrap();
fs::write(dir.join("h"), "hij").unwrap(); fs::write(dir.join("h"), "hij").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!( assert_eq!(
metainfo.info.pieces, metainfo.info.pieces,
Sha1::from("abchijxyz").digest().bytes() Sha1::from("abchijxyz").digest().bytes()
@ -789,18 +818,18 @@ mod tests {
files, files,
&[ &[
FileInfo { FileInfo {
length: 3, length: Bytes(3),
md5sum: Some("900150983cd24fb0d6963f7d28e17f72".to_owned()), md5sum: Some(Md5Digest::from_hex("900150983cd24fb0d6963f7d28e17f72")),
path: FilePath::from_components(&["a"]), path: FilePath::from_components(&["a"]),
}, },
FileInfo { FileInfo {
length: 3, length: Bytes(3),
md5sum: Some("857c4402ad934005eae4638a93812bf7".to_owned()), md5sum: Some(Md5Digest::from_hex("857c4402ad934005eae4638a93812bf7")),
path: FilePath::from_components(&["h"]), path: FilePath::from_components(&["h"]),
}, },
FileInfo { FileInfo {
length: 3, length: Bytes(3),
md5sum: Some("d16fb36f0911f878998c136191af705e".to_owned()), md5sum: Some(Md5Digest::from_hex("d16fb36f0911f878998c136191af705e")),
path: FilePath::from_components(&["x"]), path: FilePath::from_components(&["x"]),
}, },
] ]
@ -895,6 +924,7 @@ mod tests {
let dir = env.resolve("foo"); let dir = env.resolve("foo");
fs::create_dir(&dir).unwrap(); fs::create_dir(&dir).unwrap();
env.run().unwrap(); env.run().unwrap();
env.load_torrent("foo.torrent");
} }
#[test] #[test]
@ -942,6 +972,7 @@ mod tests {
let dir = env.resolve("foo"); let dir = env.resolve("foo");
fs::create_dir(&dir).unwrap(); fs::create_dir(&dir).unwrap();
env.run().unwrap(); env.run().unwrap();
env.load_torrent("foo.torrent");
} }
#[test] #[test]
@ -1022,10 +1053,7 @@ Content Size 9 bytes
fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo"), "").unwrap();
fs::write(env.resolve("foo.torrent"), "foo").unwrap(); fs::write(env.resolve("foo.torrent"), "foo").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); env.load_torrent("foo.torrent");
let bytes = fs::read(torrent).unwrap();
let value = Value::from_bencode(&bytes).unwrap();
assert!(matches!(value, Value::Dict(_)));
} }
#[test] #[test]
@ -1036,8 +1064,7 @@ Content Size 9 bytes
fs::write(dir.join("Thumbs.db"), "abc").unwrap(); fs::write(dir.join("Thumbs.db"), "abc").unwrap();
fs::write(dir.join("Desktop.ini"), "abc").unwrap(); fs::write(dir.join("Desktop.ini"), "abc").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.is_empty() Mode::Multiple { files } if files.is_empty()
@ -1059,8 +1086,7 @@ Content Size 9 bytes
fs::write(dir.join("Thumbs.db"), "abc").unwrap(); fs::write(dir.join("Thumbs.db"), "abc").unwrap();
fs::write(dir.join("Desktop.ini"), "abc").unwrap(); fs::write(dir.join("Desktop.ini"), "abc").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.len() == 2 Mode::Multiple { files } if files.len() == 2
@ -1095,8 +1121,7 @@ Content Size 9 bytes
.unwrap(); .unwrap();
} }
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.len() == 0 Mode::Multiple { files } if files.len() == 0
@ -1117,8 +1142,7 @@ Content Size 9 bytes
fs::create_dir(&dir).unwrap(); fs::create_dir(&dir).unwrap();
fs::write(dir.join(".hidden"), "abc").unwrap(); fs::write(dir.join(".hidden"), "abc").unwrap();
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.len() == 1 Mode::Multiple { files } if files.len() == 1
@ -1161,8 +1185,7 @@ Content Size 9 bytes
let mut env = environment(&["--input", "foo", "--announce", "http://bar", "--md5sum"]); let mut env = environment(&["--input", "foo", "--announce", "http://bar", "--md5sum"]);
populate_symlinks(&env); populate_symlinks(&env);
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.is_empty() Mode::Multiple { files } if files.is_empty()
@ -1183,8 +1206,7 @@ Content Size 9 bytes
]); ]);
populate_symlinks(&env); populate_symlinks(&env);
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_eq!(metainfo.info.pieces, Sha1::from("barbaz").digest().bytes()); assert_eq!(metainfo.info.pieces, Sha1::from("barbaz").digest().bytes());
match metainfo.info.mode { match metainfo.info.mode {
Mode::Multiple { files } => { Mode::Multiple { files } => {
@ -1192,13 +1214,13 @@ Content Size 9 bytes
files, files,
&[ &[
FileInfo { FileInfo {
length: 3, length: Bytes(3),
md5sum: Some("37b51d194a7513e45b56f6524f2d51f2".to_owned()), md5sum: Some(Md5Digest::from_hex("37b51d194a7513e45b56f6524f2d51f2")),
path: FilePath::from_components(&["bar"]), path: FilePath::from_components(&["bar"]),
}, },
FileInfo { FileInfo {
length: 3, length: Bytes(3),
md5sum: Some("73feffa4b7f6bb68e44cf984c85f6e88".to_owned()), md5sum: Some(Md5Digest::from_hex("73feffa4b7f6bb68e44cf984c85f6e88")),
path: FilePath::from_components(&["dir", "baz"]), path: FilePath::from_components(&["dir", "baz"]),
}, },
] ]
@ -1231,8 +1253,7 @@ Content Size 9 bytes
env.create_dir("foo/.bar"); env.create_dir("foo/.bar");
env.create_file("foo/.bar/baz", "baz"); env.create_file("foo/.bar/baz", "baz");
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.is_empty() Mode::Multiple { files } if files.is_empty()
@ -1265,8 +1286,7 @@ Content Size 9 bytes
.unwrap(); .unwrap();
} }
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.is_empty() Mode::Multiple { files } if files.is_empty()
@ -1282,8 +1302,7 @@ Content Size 9 bytes
env.create_file("foo/b", "b"); env.create_file("foo/b", "b");
env.create_file("foo/c", "c"); env.create_file("foo/c", "c");
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.len() == 2 Mode::Multiple { files } if files.len() == 2
@ -1299,8 +1318,7 @@ Content Size 9 bytes
env.create_file("foo/b", "b"); env.create_file("foo/b", "b");
env.create_file("foo/c", "c"); env.create_file("foo/c", "c");
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.len() == 3 Mode::Multiple { files } if files.len() == 3
@ -1323,8 +1341,7 @@ Content Size 9 bytes
env.create_file("foo/b", "b"); env.create_file("foo/b", "b");
env.create_file("foo/c", "c"); env.create_file("foo/c", "c");
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.len() == 2 Mode::Multiple { files } if files.len() == 2
@ -1340,8 +1357,7 @@ Content Size 9 bytes
env.create_file("foo/b", "b"); env.create_file("foo/b", "b");
env.create_file("foo/c", "c"); env.create_file("foo/c", "c");
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.is_empty() Mode::Multiple { files } if files.is_empty()
@ -1368,8 +1384,7 @@ Content Size 9 bytes
env.create_file("foo/b", "b"); env.create_file("foo/b", "b");
env.create_file("foo/c", "c"); env.create_file("foo/c", "c");
env.run().unwrap(); env.run().unwrap();
let torrent = env.resolve("foo.torrent"); let metainfo = env.load_torrent("foo.torrent");
let metainfo = Metainfo::load(torrent).unwrap();
assert_matches!( assert_matches!(
metainfo.info.mode, metainfo.info.mode,
Mode::Multiple { files } if files.len() == 1 Mode::Multiple { files } if files.len() == 1

View file

@ -21,7 +21,7 @@ impl PieceLength {
)); ));
for i in 14..51 { for i in 14..51 {
let content_size = Bytes::from(1u128 << i); let content_size = Bytes::from(1u64 << i);
let piece_length = PieceLengthPicker::from_content_size(content_size); let piece_length = PieceLengthPicker::from_content_size(content_size);

View file

@ -18,10 +18,9 @@ pub(crate) struct Show {
impl Show { impl Show {
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> { pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
let summary = TorrentSummary::load(&env.resolve(self.input))?; let input = env.resolve(&self.input);
let summary = TorrentSummary::load(&input)?;
summary.write(env)?; summary.write(env)?;
Ok(()) Ok(())
} }
} }
@ -42,15 +41,15 @@ mod tests {
creation_date: Some(1), creation_date: Some(1),
encoding: Some("UTF-8".into()), encoding: Some("UTF-8".into()),
info: Info { info: Info {
private: Some(1), private: Some(true),
piece_length: 16 * 1024, piece_length: Bytes(16 * 1024),
source: Some("source".into()), source: Some("source".into()),
name: "foo".into(), name: "foo".into(),
pieces: vec![ pieces: vec![
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
], ],
mode: Mode::Single { mode: Mode::Single {
length: 20, length: Bytes(20),
md5sum: None, md5sum: None,
}, },
}, },
@ -133,15 +132,15 @@ Files\tfoo
creation_date: Some(1), creation_date: Some(1),
encoding: Some("UTF-8".into()), encoding: Some("UTF-8".into()),
info: Info { info: Info {
private: Some(1), private: Some(true),
piece_length: 16 * 1024, piece_length: Bytes(16 * 1024),
source: Some("source".into()), source: Some("source".into()),
name: "foo".into(), name: "foo".into(),
pieces: vec![ pieces: vec![
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
], ],
mode: Mode::Single { mode: Mode::Single {
length: 20, length: Bytes(20),
md5sum: None, md5sum: None,
}, },
}, },
@ -225,15 +224,15 @@ Files\tfoo
creation_date: Some(1), creation_date: Some(1),
encoding: Some("UTF-8".into()), encoding: Some("UTF-8".into()),
info: Info { info: Info {
private: Some(1), private: Some(true),
piece_length: 16 * 1024, piece_length: Bytes(16 * 1024),
source: Some("source".into()), source: Some("source".into()),
name: "foo".into(), name: "foo".into(),
pieces: vec![ pieces: vec![
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,
], ],
mode: Mode::Single { mode: Mode::Single {
length: 20, length: Bytes(20),
md5sum: None, md5sum: None,
}, },
}, },

View file

@ -160,7 +160,7 @@ impl Extractor {
return; return;
}; };
if let Ok(value) = Value::from_bencode(&contents) { if let Ok(value) = bendy::serde::de::from_bytes::<Value>(&contents) {
self.extract(&value); self.extract(&value);
if self.print { if self.print {
eprintln!("{}:\n{}", path.display(), Self::pretty_print(&value)); eprintln!("{}:\n{}", path.display(), Self::pretty_print(&value));

69
src/opt/torrent/verify.rs Normal file
View file

@ -0,0 +1,69 @@
use crate::common::*;
#[derive(StructOpt)]
#[structopt(
help_message(consts::HELP_MESSAGE),
version_message(consts::VERSION_MESSAGE),
about(
"Verify files against a `.torrent` file.
files present
md5sum matches
piece hashes match
lengths are correct
"
)
)]
pub(crate) struct Verify {
#[structopt(
name = "TORRENT",
long = "metainfo",
help = "Verify input data against `TORRENT` metainfo file.",
parse(from_os_str)
)]
metainfo: PathBuf,
#[structopt(
name = "INPUT",
long = "input",
help = "Verify `INPUT`. Defaults to `info.name` field of torrent metainfo.",
parse(from_os_str)
)]
input: Option<PathBuf>,
}
impl Verify {
pub(crate) fn run(self, env: &mut Env) -> Result<(), Error> {
let metainfo_path = env.resolve(&self.metainfo);
let metainfo = Metainfo::load(&metainfo_path)?;
let base = if let Some(input) = &self.input {
env.resolve(input)
} else {
metainfo_path.parent().unwrap().join(&metainfo.info.name)
};
let status = metainfo.verify(&base)?;
status.write(env)?;
if status.good() {
Ok(())
} else {
Err(Error::Verify { status })
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn environment(args: &[&str]) -> TestEnv {
testing::env(["torrent", "create"].iter().chain(args).cloned())
}
#[test]
fn require_metainfo_argument() {
let mut env = environment(&[]);
assert!(matches!(env.run(), Err(Error::Clap { .. })));
}
}

View file

@ -29,14 +29,14 @@ impl PieceLengthPicker {
clippy::cast_precision_loss, clippy::cast_precision_loss,
clippy::cast_possible_truncation clippy::cast_possible_truncation
)] )]
let exponent = (content_size.count() as f64).log2().ceil() as u128; let exponent = (content_size.count().max(1) as f64).log2().ceil() as u64;
Bytes::from(1u128 << (exponent / 2 + 4)) Bytes::from(1u64 << (exponent / 2 + 4))
.max(Bytes::kib() * 16) .max(Bytes::kib() * 16)
.min(Bytes::mib() * 16) .min(Bytes::mib() * 16)
} }
pub(crate) fn piece_count(content_size: Bytes, piece_length: Bytes) -> u128 { pub(crate) fn piece_count(content_size: Bytes, piece_length: Bytes) -> u64 {
if content_size == Bytes::from(0u128) { if content_size == Bytes::from(0u64) {
0 0
} else { } else {
(content_size / piece_length).max(1) (content_size / piece_length).max(1)
@ -44,7 +44,7 @@ impl PieceLengthPicker {
} }
pub(crate) fn metainfo_size(content_size: Bytes, piece_length: Bytes) -> Bytes { pub(crate) fn metainfo_size(content_size: Bytes, piece_length: Bytes) -> Bytes {
let digest_length: u128 = sha1::DIGEST_LENGTH.into_u64().into(); let digest_length: u64 = sha1::DIGEST_LENGTH.into_u64();
Bytes::from(Self::piece_count(content_size, piece_length) * digest_length) Bytes::from(Self::piece_count(content_size, piece_length) * digest_length)
} }
} }

53
src/status.rs Normal file
View file

@ -0,0 +1,53 @@
use crate::common::*;
#[derive(Debug)]
pub(crate) struct Status {
pieces: bool,
files: Vec<FileStatus>,
}
impl Status {
pub(crate) fn new(pieces: bool, files: Vec<FileStatus>) -> Status {
Status { pieces, files }
}
pub(crate) fn pieces(&self) -> bool {
self.pieces
}
pub(crate) fn good(&self) -> bool {
self.pieces && self.files.iter().all(FileStatus::good)
}
pub(crate) fn write(&self, out: &mut Env) -> Result<()> {
for file in &self.files {
errln!(out, "{} {}", file.icon(), file.path().display());
}
if !self.pieces() {
errln!(out, "Piece hashes incorrect");
}
Ok(())
}
}
impl Display for Status {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let bad = self.files.iter().filter(|status| status.bad()).count();
if bad != 0 {
write!(f, "{} of {} files corrupted", bad, self.files.len())?;
return Ok(());
}
if !self.pieces() {
write!(f, "pieces corrupted")?;
return Ok(());
}
write!(f, "ok")?;
Ok(())
}
}

View file

@ -30,6 +30,10 @@ impl TestEnv {
pub(crate) fn create_file(&self, path: impl AsRef<Path>, bytes: impl AsRef<[u8]>) { pub(crate) fn create_file(&self, path: impl AsRef<Path>, bytes: impl AsRef<[u8]>) {
fs::write(self.env.resolve(path), bytes.as_ref()).unwrap(); fs::write(self.env.resolve(path), bytes.as_ref()).unwrap();
} }
pub(crate) fn load_torrent(&self, filename: impl AsRef<Path>) -> Metainfo {
Metainfo::load(self.env.resolve(filename.as_ref())).unwrap()
}
} }
impl Deref for TestEnv { impl Deref for TestEnv {

View file

@ -4,6 +4,7 @@ pub(crate) struct TestEnvBuilder {
args: Vec<String>, args: Vec<String>,
out_is_term: bool, out_is_term: bool,
use_color: bool, use_color: bool,
tempdir: Option<TempDir>,
} }
impl TestEnvBuilder { impl TestEnvBuilder {
@ -12,6 +13,7 @@ impl TestEnvBuilder {
args: Vec::new(), args: Vec::new(),
out_is_term: false, out_is_term: false,
use_color: false, use_color: false,
tempdir: None,
} }
} }
@ -39,12 +41,17 @@ impl TestEnvBuilder {
self self
} }
pub(crate) fn tempdir(mut self, tempdir: TempDir) -> Self {
self.tempdir = Some(tempdir);
self
}
pub(crate) fn build(self) -> TestEnv { pub(crate) fn build(self) -> TestEnv {
let err = Capture::new(); let err = Capture::new();
let out = Capture::new(); let out = Capture::new();
let env = Env::new( let env = Env::new(
tempfile::tempdir().unwrap(), self.tempdir.unwrap_or_else(|| tempfile::tempdir().unwrap()),
out.clone(), out.clone(),
if self.use_color && self.out_is_term { if self.use_color && self.out_is_term {
Style::active() Style::active()

View file

@ -13,7 +13,7 @@ impl TorrentSummary {
let infohash = if let Value::Dict(items) = value { let infohash = if let Value::Dict(items) = value {
let info = items let info = items
.iter() .iter()
.find(|pair: &(&Vec<u8>, &Value)| pair.0 == b"info") .find(|pair: &(&Cow<[u8]>, &Value)| pair.0.as_ref() == b"info")
.unwrap() .unwrap()
.1 .1
.to_bencode() .to_bencode()
@ -99,7 +99,7 @@ impl TorrentSummary {
table.row( table.row(
"Private", "Private",
if self.metainfo.info.private.unwrap_or(0) == 1 { if self.metainfo.info.private.unwrap_or(false) {
"yes" "yes"
} else { } else {
"no" "no"
@ -142,7 +142,7 @@ impl TorrentSummary {
None => table.row("Tracker", &self.metainfo.announce), None => table.row("Tracker", &self.metainfo.announce),
} }
table.size("Piece Size", Bytes::from(self.metainfo.info.piece_length)); table.size("Piece Size", self.metainfo.info.piece_length);
table.row("Piece Count", self.metainfo.info.pieces.len() / 20); table.row("Piece Count", self.metainfo.info.pieces.len() / 20);

79
src/verifier.rs Normal file
View file

@ -0,0 +1,79 @@
use crate::common::*;
pub(crate) struct Verifier {
buffer: Vec<u8>,
piece_length: usize,
pieces: Vec<u8>,
sha1: Sha1,
piece_bytes_hashed: usize,
}
impl Verifier {
pub(crate) fn new(piece_length: usize) -> Verifier {
Verifier {
buffer: vec![0; piece_length],
piece_bytes_hashed: 0,
sha1: Sha1::new(),
pieces: Vec::new(),
piece_length,
}
}
pub(crate) fn verify(metainfo: &Metainfo, base: &Path) -> Result<Status> {
let piece_length = metainfo.info.piece_length.as_piece_length()?;
let piece_length = piece_length.into_usize();
let mut status = Vec::new();
let mut hasher = Self::new(piece_length);
for (path, len, md5sum) in metainfo.files(&base) {
status.push(FileStatus::status(&path, len, md5sum));
hasher.hash(&path).ok();
}
if hasher.piece_bytes_hashed > 0 {
hasher.pieces.extend(&hasher.sha1.digest().bytes());
hasher.sha1.reset();
hasher.piece_bytes_hashed = 0;
}
let pieces = hasher.pieces == metainfo.info.pieces;
Ok(Status::new(pieces, status))
}
pub(crate) fn hash(&mut self, path: &Path) -> io::Result<()> {
let mut file = File::open(path)?;
let mut remaining = path.metadata()?.len();
while remaining > 0 {
let to_buffer: usize = remaining
.min(self.buffer.len().into_u64())
.try_into()
.unwrap();
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;
}
}
remaining -= buffer.len().into_u64();
}
Ok(())
}
}