From 6549850dac2eb57f7ea2e67a1a4b50bd4a35b1d4 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Fri, 14 Feb 2020 00:12:49 -0800 Subject: [PATCH] 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 --- Cargo.lock | 98 +++++++---- Cargo.toml | 43 ++--- justfile | 4 + src/bytes.rs | 108 ++++++++---- src/common.rs | 65 ++++--- src/env.rs | 7 +- src/error.rs | 7 +- src/file_info.rs | 13 +- src/file_status.rs | 114 +++++++++++++ src/hasher.rs | 25 +-- src/info.rs | 19 ++- src/inner.rs | 52 ------ src/main.rs | 5 +- src/md5_digest.rs | 53 ++++++ src/metainfo.rs | 158 +++++++++++++---- src/mode.rs | 88 +++++++++- src/opt/torrent.rs | 3 + src/opt/torrent/create.rs | 289 +++++++++++++++++--------------- src/opt/torrent/piece_length.rs | 2 +- src/opt/torrent/show.rs | 23 ++- src/opt/torrent/stats.rs | 2 +- src/opt/torrent/verify.rs | 69 ++++++++ src/piece_length_picker.rs | 10 +- src/status.rs | 53 ++++++ src/test_env.rs | 4 + src/test_env_builder.rs | 9 +- src/torrent_summary.rs | 6 +- src/verifier.rs | 79 +++++++++ 28 files changed, 1011 insertions(+), 397 deletions(-) create mode 100644 src/file_status.rs delete mode 100644 src/inner.rs create mode 100644 src/md5_digest.rs create mode 100644 src/opt/torrent/verify.rs create mode 100644 src/status.rs create mode 100644 src/verifier.rs diff --git a/Cargo.lock b/Cargo.lock index a20389b..8c8619c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,6 +27,15 @@ dependencies = [ "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]] name = "atty" version = "0.2.14" @@ -46,9 +55,9 @@ checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" [[package]] name = "backtrace" -version = "0.3.43" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f80256bc78f67e7df7e36d77366f636ed976895d91fe2ab9efa3973e8fe8c4f" +checksum = "e4036b9bf40f3cf16aba72a3d65e8a520fc4bafcdc7079aea8f848c58c5b5536" dependencies = [ "backtrace-sys", "cfg-if", @@ -69,11 +78,11 @@ dependencies = [ [[package]] name = "bendy" 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 = [ "failure", "serde", - "serde_bytes 0.11.3", + "serde_bytes", ] [[package]] @@ -278,15 +287,15 @@ dependencies = [ "atty", "bendy", "chrono", - "env_logger", "globset", "libc", "md5", "pretty_assertions", + "pretty_env_logger", "regex", "serde", - "serde_bencode", - "serde_bytes 0.11.3", + "serde-hex", + "serde_bytes", "serde_with", "sha1", "snafu", @@ -294,6 +303,7 @@ dependencies = [ "structopt", "syn", "tempfile", + "temptree", "unicode-width", "url", "walkdir", @@ -336,6 +346,12 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + [[package]] name = "md5" version = "0.7.0" @@ -344,9 +360,15 @@ checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" -version = "2.3.0" +version = "2.3.2" 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]] name = "num-integer" @@ -401,10 +423,20 @@ dependencies = [ ] [[package]] -name = "proc-macro-error" -version = "0.4.8" +name = "pretty_env_logger" +version = "0.4.0" 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 = [ "proc-macro-error-attr", "proc-macro2", @@ -415,9 +447,9 @@ dependencies = [ [[package]] name = "proc-macro-error-attr" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5717d9fa2664351a01ed73ba5ef6df09c01a521cb42cb65a061432a826f3c7a" +checksum = "d175bef481c7902e63e3165627123fff3502f06ac043d3ef42d08c1246da9253" dependencies = [ "proc-macro2", "quote", @@ -560,22 +592,14 @@ dependencies = [ ] [[package]] -name = "serde_bencode" -version = "0.2.1" +name = "serde-hex" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315c49c11b6b10acc209df75b757ee70957b911ecd0e29bcbf2b735ebd580d45" -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" +checksum = "ca37e3e4d1b39afd7ff11ee4e947efae85adfddf4841787bfa47c470e96dc26d" dependencies = [ + "array-init", "serde", + "smallvec 0.6.13", ] [[package]] @@ -625,6 +649,15 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "smallvec" version = "1.2.0" @@ -736,6 +769,15 @@ dependencies = [ "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]] name = "term_size" version = "0.3.1" @@ -801,7 +843,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5479532badd04e128284890390c1e876ef7a993d0570b3597ae43dfa1d59afa4" dependencies = [ - "smallvec", + "smallvec 1.2.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f46d854..a0bb611 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,31 +13,31 @@ edition = "2018" default-run = "imdl" [dependencies] -ansi_term = "0.12" -atty = "0.2" +ansi_term = "0.12.0" +atty = "0.2.0" chrono = "0.4.1" -env_logger = "0.7" -globset = "0.4" -libc = "0.2" -md5 = "0.7" -pretty_assertions = "0.6" -regex = "1" -serde_bencode = "0.2" -serde_bytes = "0.11" -serde_with = "1.4" -sha1 = "0.6" -snafu = "0.6" -static_assertions = "1" +globset = "0.4.0" +libc = "0.2.0" +md5 = "0.7.0" +pretty_assertions = "0.6.0" +pretty_env_logger = "0.4.0" +regex = "1.0.0" +serde-hex = "0.1.0" +serde_bytes = "0.11.0" +serde_with = "1.4.0" +sha1 = "0.6.0" +snafu = "0.6.0" +static_assertions = "1.0.0" syn = "1.0.14" -tempfile = "3" -unicode-width = "0.1" -url = "2" -walkdir = "2.1" +tempfile = "3.0.0" +unicode-width = "0.1.0" +url = "2.0.0" +walkdir = "2.1.0" [dependencies.bendy] version = "0.2.2" git = "https://github.com/casey/bendy.git" -branch = "value" +branch = "serde" features = ["serde"] [dependencies.serde] @@ -45,9 +45,12 @@ version = "1.0.103" features = ["derive"] [dependencies.structopt] -version = "0.3" +version = "0.3.0" features = ["default", "wrap_help"] +[dev-dependencies] +temptree = "0.0.0" + [workspace] members = [ # generate table of contents and table of supported BEPs in README.md diff --git a/justfile b/justfile index cf74b0a..7ad0ff2 100644 --- a/justfile +++ b/justfile @@ -2,6 +2,10 @@ default: watch 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: cargo watch --exec test diff --git a/src/bytes.rs b/src/bytes.rs index 24164c9..ef6b3f9 100644 --- a/src/bytes.rs +++ b/src/bytes.rs @@ -1,22 +1,17 @@ use crate::common::*; -const KI: u128 = 1 << 10; -const MI: u128 = KI << 10; -const GI: u128 = MI << 10; -const TI: u128 = GI << 10; -const PI: u128 = TI << 10; -const EI: u128 = PI << 10; -const ZI: u128 = EI << 10; -const YI: u128 = ZI << 10; +const KI: u64 = 1 << 10; +const MI: u64 = KI << 10; +const GI: u64 = MI << 10; +const TI: u64 = GI << 10; +const PI: u64 = TI << 10; +const EI: u64 = PI << 10; -#[derive(Debug, PartialEq, Copy, Clone, PartialOrd, Ord, Eq)] -pub(crate) struct Bytes(pub(crate) u128); +#[serde(transparent)] +#[derive(Debug, PartialEq, Copy, Clone, PartialOrd, Ord, Eq, Serialize, Deserialize, Default)] +pub(crate) struct Bytes(pub(crate) u64); 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 { Bytes::from(KI) } @@ -25,67 +20,86 @@ impl Bytes { Bytes::from(MI) } - pub(crate) fn count(self) -> u128 { + pub(crate) fn count(self) -> u64 { self.0 } + + pub(crate) fn as_piece_length(self) -> Result { + self + .count() + .try_into() + .context(error::PieceLengthTooLarge { bytes: self }) + } } -fn float_to_int(x: f64) -> u128 { +fn float_to_int(x: f64) -> u64 { #![allow( clippy::as_conversions, clippy::cast_sign_loss, 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)] x as f64 } -impl> From for Bytes { +impl> From for Bytes { fn from(n: I) -> Bytes { Bytes(n.into()) } } impl Div for Bytes { - type Output = u128; + type Output = u64; - fn div(self, rhs: Bytes) -> u128 { + fn div(self, rhs: Bytes) -> u64 { self.0 / rhs.0 } } -impl Div for Bytes { +impl Div for Bytes { type Output = Bytes; - fn div(self, rhs: u128) -> Bytes { + fn div(self, rhs: u64) -> Bytes { Bytes::from(self.0 / rhs) } } -impl DivAssign for Bytes { - fn div_assign(&mut self, rhs: u128) { +impl DivAssign for Bytes { + fn div_assign(&mut self, rhs: u64) { self.0 /= rhs; } } -impl Mul for Bytes { +impl Mul for Bytes { type Output = Bytes; - fn mul(self, rhs: u128) -> Self { + fn mul(self, rhs: u64) -> Self { Bytes::from(self.0 * rhs) } } -impl MulAssign for Bytes { - fn mul_assign(&mut self, rhs: u128) { +impl MulAssign for Bytes { + fn mul_assign(&mut self, rhs: u64) { self.0 *= rhs; } } +impl AddAssign for Bytes { + fn add_assign(&mut self, rhs: Bytes) { + self.0 += rhs.0; + } +} + +impl SubAssign for Bytes { + fn sub_assign(&mut self, rhs: u64) { + self.0 -= rhs; + } +} + impl FromStr for Bytes { type Err = Error; @@ -115,8 +129,6 @@ impl FromStr for Bytes { "tib" => TI, "pib" => PI, "eib" => EI, - "zib" => ZI, - "yib" => YI, _ => { return Err(Error::ByteSuffix { text: text.to_owned(), @@ -129,9 +141,24 @@ impl FromStr for Bytes { } } +impl Sum for Bytes { + fn sum(iter: I) -> Self + where + I: Iterator, + { + let mut sum = Bytes(0); + + for item in iter { + sum += item; + } + + sum + } +} + impl Display for Bytes { 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); @@ -164,7 +191,7 @@ mod tests { #[test] fn ok() { - const CASES: &[(&str, u128)] = &[ + const CASES: &[(&str, u64)] = &[ ("0", 0), ("0kib", 0), ("1", 1), @@ -175,7 +202,6 @@ mod tests { ("1KiB", KI), ("12kib", 12 * KI), ("1.5mib", 1 * MI + 512 * KI), - ("1yib", 1 * YI), ]; for (text, value) in CASES { @@ -216,7 +242,17 @@ mod tests { assert_eq!(Bytes(TI).to_string(), "1 TiB"); assert_eq!(Bytes(PI).to_string(), "1 PiB"); 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(), + ); } } diff --git a/src/common.rs b/src/common.rs index c5860d9..8773c80 100644 --- a/src/common.rs +++ b/src/common.rs @@ -10,8 +10,9 @@ pub(crate) use std::{ fs::{self, File}, hash::Hash, io::{self, Read, Write}, - num::ParseFloatError, - ops::{Div, DivAssign, Mul, MulAssign}, + iter::{self, Sum}, + num::{ParseFloatError, TryFromIntError}, + ops::{AddAssign, Div, DivAssign, Mul, MulAssign, SubAssign}, path::{self, Path, PathBuf}, process::{self, Command, ExitStatus}, str::{self, FromStr}, @@ -25,8 +26,9 @@ pub(crate) use chrono::{TimeZone, Utc}; pub(crate) use globset::{Glob, GlobMatcher}; pub(crate) use libc::EXIT_FAILURE; pub(crate) use regex::{Regex, RegexSet}; -pub(crate) use serde::{Deserialize, Deserializer, Serialize, Serializer}; -pub(crate) use serde_with::skip_serializing_none; +pub(crate) use serde::{Deserialize, Serialize}; +pub(crate) use serde_hex::SerHex; +pub(crate) use serde_with::rust::unwrap_or_skip; pub(crate) use sha1::Sha1; pub(crate) use snafu::{ResultExt, Snafu}; pub(crate) use static_assertions::const_assert; @@ -39,7 +41,7 @@ pub(crate) use url::Url; pub(crate) use walkdir::WalkDir; // modules -pub(crate) use crate::{consts, error, inner, use_color}; +pub(crate) use crate::{consts, error, use_color}; // traits pub(crate) use crate::{ @@ -49,29 +51,38 @@ pub(crate) use crate::{ // structs and enums pub(crate) use crate::{ - bytes::Bytes, env::Env, error::Error, file_info::FileInfo, file_path::FilePath, files::Files, - hasher::Hasher, info::Info, lint::Lint, linter::Linter, metainfo::Metainfo, mode::Mode, opt::Opt, - piece_length_picker::PieceLengthPicker, platform::Platform, style::Style, table::Table, - target::Target, torrent_summary::TorrentSummary, use_color::UseColor, walker::Walker, + bytes::Bytes, env::Env, error::Error, file_info::FileInfo, file_path::FilePath, + file_status::FileStatus, files::Files, hasher::Hasher, info::Info, lint::Lint, linter::Linter, + md5_digest::Md5Digest, metainfo::Metainfo, mode::Mode, opt::Opt, + 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 pub(crate) type Result = std::result::Result; + +#[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::*; diff --git a/src/env.rs b/src/env.rs index 3fe7659..666e500 100644 --- a/src/env.rs +++ b/src/env.rs @@ -50,12 +50,7 @@ impl Env { ansi_term::enable_ansi_support().ok(); #[cfg(not(test))] - env_logger::Builder::from_env( - env_logger::Env::new() - .filter("JUST_LOG") - .write_style("JUST_LOG_STYLE"), - ) - .init(); + pretty_env_logger::init(); let opt = Opt::from_iter_safe(&self.args)?; diff --git a/src/error.rs b/src/error.rs index ba2acc1..9e78aee 100644 --- a/src/error.rs +++ b/src/error.rs @@ -78,7 +78,10 @@ pub(crate) enum Error { bytes, 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))] PieceLengthUneven { bytes: Bytes }, #[snafu(display("Piece length must be at least 16 KiB"))] @@ -103,6 +106,8 @@ pub(crate) enum Error { Unstable { feature: &'static str }, #[snafu(display("Unknown lint: {}", text))] LintUnknown { text: String }, + #[snafu(display("Torrent verification failed: {}", status))] + Verify { status: Status }, } impl Error { diff --git a/src/file_info.rs b/src/file_info.rs index 7831c86..0629411 100644 --- a/src/file_info.rs +++ b/src/file_info.rs @@ -1,10 +1,13 @@ use crate::common::*; -#[skip_serializing_none] -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub(crate) struct FileInfo { - pub(crate) length: u64, + pub(crate) length: Bytes, pub(crate) path: FilePath, - #[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] - pub(crate) md5sum: Option, + #[serde( + skip_serializing_if = "Option::is_none", + default, + with = "unwrap_or_skip" + )] + pub(crate) md5sum: Option, } diff --git a/src/file_status.rs b/src/file_status.rs new file mode 100644 index 0000000..c2e2cba --- /dev/null +++ b/src/file_status.rs @@ -0,0 +1,114 @@ +use crate::common::*; + +#[derive(Debug)] +pub(crate) struct FileStatus { + path: PathBuf, + error: Option, + present: bool, + file: bool, + length_expected: Bytes, + length_actual: Option, + md5_expected: Option, + md5_actual: Option, +} + +impl FileStatus { + pub(crate) fn status( + path: &Path, + length_expected: Bytes, + md5_expected: Option, + ) -> 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) -> 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 + } +} diff --git a/src/hasher.rs b/src/hasher.rs index cf4246d..21a36ad 100644 --- a/src/hasher.rs +++ b/src/hasher.rs @@ -4,8 +4,8 @@ pub(crate) struct Hasher { buffer: Vec, length: u64, md5sum: bool, - piece_bytes_hashed: u64, - piece_length: u32, + piece_bytes_hashed: usize, + piece_length: usize, pieces: Vec, sha1: Sha1, } @@ -14,20 +14,20 @@ impl Hasher { pub(crate) fn hash( files: &Files, md5sum: bool, - piece_length: u32, + piece_length: usize, ) -> Result<(Mode, Vec), Error> { 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 { - buffer: vec![0; piece_length.into_usize()], + buffer: vec![0; piece_length], length: 0, piece_bytes_hashed: 0, pieces: Vec::new(), sha1: Sha1::new(), - md5sum, piece_length, + md5sum, } } @@ -40,7 +40,7 @@ impl Hasher { let (md5sum, length) = self.hash_file(files.root())?; Mode::Single { - md5sum: md5sum.map(|md5sum| format!("{:x}", md5sum)), + md5sum: md5sum.map(|md5sum| md5sum.into()), length, } }; @@ -67,7 +67,7 @@ impl Hasher { let (md5sum, length) = self.hash_file(&path)?; files.push(FileInfo { - md5sum: md5sum.map(|md5sum| format!("{:x}", md5sum)), + md5sum: md5sum.map(|md5sum| md5sum.into()), path: file_path.clone(), length, }); @@ -76,13 +76,13 @@ impl Hasher { Ok(files) } - fn hash_file(&mut self, file: &Path) -> Result<(Option, u64), Error> { + fn hash_file(&mut self, file: &Path) -> Result<(Option, Bytes), Error> { self .hash_file_io(file) .context(error::Filesystem { path: file }) } - fn hash_file_io(&mut self, file: &Path) -> io::Result<(Option, u64)> { + fn hash_file_io(&mut self, file: &Path) -> io::Result<(Option, Bytes)> { let length = file.metadata()?.len(); let mut remaining = length; @@ -100,6 +100,7 @@ impl Hasher { .min(self.buffer.len().into_u64()) .try_into() .unwrap(); + let buffer = &mut self.buffer[0..to_buffer]; file.read_exact(buffer)?; @@ -109,7 +110,7 @@ impl Hasher { 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.sha1.reset(); self.piece_bytes_hashed = 0; @@ -125,6 +126,6 @@ impl Hasher { self.length += length; - Ok((md5.map(md5::Context::compute), length)) + Ok((md5.map(md5::Context::compute), Bytes::from(length))) } } diff --git a/src/info.rs b/src/info.rs index 4bbc7ce..d464383 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,14 +1,21 @@ use crate::common::*; -#[skip_serializing_none] -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub(crate) struct Info { - #[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] - pub(crate) private: Option, + #[serde( + skip_serializing_if = "Option::is_none", + default, + with = "unwrap_or_skip" + )] + pub(crate) private: Option, #[serde(rename = "piece length")] - pub(crate) piece_length: u32, + pub(crate) piece_length: Bytes, 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, #[serde(with = "serde_bytes")] pub(crate) pieces: Vec, diff --git a/src/inner.rs b/src/inner.rs deleted file mode 100644 index 511f866..0000000 --- a/src/inner.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::common::*; - -pub(crate) fn serialize(value: &Option, serializer: S) -> Result -where - S: Serializer, - T: Serialize, -{ - value.as_ref().unwrap().serialize(serializer) -} - -pub(crate) fn deserialize<'de, T, D>(deserializer: D) -> Result, 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(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, - } - - let none = Foo { bar: None }; - case(none, b"de"); - - let some = Foo { bar: Some(1) }; - case(some, b"d3:bari1ee"); - } -} diff --git a/src/main.rs b/src/main.rs index 31fcebc..969c04a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -61,14 +61,15 @@ mod env; mod error; mod file_info; mod file_path; +mod file_status; mod files; mod hasher; mod info; -mod inner; mod into_u64; mod into_usize; mod lint; mod linter; +mod md5_digest; mod metainfo; mod mode; mod opt; @@ -77,11 +78,13 @@ mod piece_length_picker; mod platform; mod platform_interface; mod reckoner; +mod status; mod style; mod table; mod target; mod torrent_summary; mod use_color; +mod verifier; mod walker; fn main() { diff --git a/src/md5_digest.rs b/src/md5_digest.rs new file mode 100644 index 0000000..967ce7b --- /dev/null +++ b/src/md5_digest.rs @@ -0,0 +1,53 @@ +use crate::common::*; + +#[serde(transparent)] +#[derive(Deserialize, Serialize, Debug, Eq, PartialEq, Copy, Clone)] +pub(crate) struct Md5Digest { + #[serde(with = "SerHex::")] + 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 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); + } +} diff --git a/src/metainfo.rs b/src/metainfo.rs index 471fd5a..ff405be 100644 --- a/src/metainfo.rs +++ b/src/metainfo.rs @@ -1,27 +1,45 @@ use crate::common::*; -#[skip_serializing_none] -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub(crate) struct Metainfo { pub(crate) announce: String, #[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>>, - #[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, #[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, #[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, - #[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, pub(crate) info: Info, } impl Metainfo { - #[cfg(test)] pub(crate) fn load(path: impl AsRef) -> Result { let path = path.as_ref(); let bytes = fs::read(path).context(error::Filesystem { path })?; @@ -31,45 +49,113 @@ impl Metainfo { #[cfg(test)] pub(crate) fn dump(&self, path: impl AsRef) -> Result<(), Error> { let path = path.as_ref(); - let bendy = 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) - ); - } - fs::write(path, &bendy).context(error::Filesystem { path })?; + let bencode = bendy::serde::ser::to_bytes(&self).context(error::MetainfoSerialize)?; + fs::write(path, &bencode).context(error::Filesystem { path })?; Ok(()) } pub(crate) fn deserialize(path: impl AsRef, bytes: &[u8]) -> Result { let path = path.as_ref(); - let bendy = 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) + bendy::serde::de::from_bytes(&bytes).context(error::MetainfoLoad { path }) } pub(crate) fn serialize(&self) -> Result, Error> { - let bendy = 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) + bendy::serde::ser::to_bytes(&self).context(error::MetainfoSerialize) } #[cfg(test)] pub(crate) fn from_bytes(bytes: &[u8]) -> Metainfo { - let bendy = bendy::serde::de::from_bytes(bytes).unwrap(); - let serde_bencode = serde_bencode::de::from_bytes(bytes).unwrap(); - assert_eq!(bendy, serde_bencode); - bendy + bendy::serde::de::from_bytes(bytes).unwrap() + } + + pub(crate) fn files<'a>( + &'a self, + base: &'a Path, + ) -> Box)> + '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 { + 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); } } diff --git a/src/mode.rs b/src/mode.rs index 6baf045..5d26877 100644 --- a/src/mode.rs +++ b/src/mode.rs @@ -1,13 +1,16 @@ use crate::common::*; -#[skip_serializing_none] #[serde(untagged)] -#[derive(Deserialize, Serialize, Debug, PartialEq)] +#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)] pub(crate) enum Mode { Single { - length: u64, - #[serde(skip_serializing_if = "Option::is_none", default, with = "inner")] - md5sum: Option, + length: Bytes, + #[serde( + skip_serializing_if = "Option::is_none", + default, + with = "unwrap_or_skip" + )] + md5sum: Option, }, Multiple { files: Vec, @@ -17,8 +20,79 @@ pub(crate) enum Mode { impl Mode { pub(crate) fn total_size(&self) -> Bytes { match self { - Self::Single { length, .. } => Bytes::from(*length), - Self::Multiple { files } => Bytes::from(files.iter().map(|file| file.length).sum::()), + Self::Single { length, .. } => *length, + 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); + } +} diff --git a/src/opt/torrent.rs b/src/opt/torrent.rs index 6a36b26..848d5a3 100644 --- a/src/opt/torrent.rs +++ b/src/opt/torrent.rs @@ -4,6 +4,7 @@ mod create; mod piece_length; mod show; mod stats; +mod verify; #[derive(StructOpt)] #[structopt( @@ -17,6 +18,7 @@ pub(crate) enum Torrent { PieceLength(piece_length::PieceLength), Show(show::Show), Stats(stats::Stats), + Verify(verify::Verify), } impl Torrent { @@ -26,6 +28,7 @@ impl Torrent { Self::PieceLength(piece_length) => piece_length.run(env), Self::Show(show) => show.run(env), Self::Stats(stats) => stats.run(env, unstable), + Self::Verify(verify) => verify.run(env), } } } diff --git a/src/opt/torrent/create.rs b/src/opt/torrent/create.rs index e243f13..4c96556 100644 --- a/src/opt/torrent/create.rs +++ b/src/opt/torrent/create.rs @@ -161,24 +161,17 @@ impl Create { let mut linter = Linter::new(); 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 { bytes: piece_length, }); } - let piece_length: u32 = piece_length - .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 { + if linter.is_denied(Lint::SmallPieceLength) && piece_length.count() < 16 * 1024 { return Err(Error::PieceLengthSmall); } @@ -220,7 +213,7 @@ impl Create { 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 { None @@ -238,7 +231,11 @@ impl Create { 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 { source: self.source, @@ -280,7 +277,12 @@ impl Create { .and_then(|mut file| file.write_all(&bytes)) .context(error::Filesystem { path })?; + #[cfg(test)] + TorrentSummary::from_metainfo(metainfo.clone())?.write(env)?; + + #[cfg(not(test))] TorrentSummary::from_metainfo(metainfo)?.write(env)?; + if self.open { Platform::open(&path)?; } @@ -288,6 +290,15 @@ impl Create { 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(()) } } @@ -302,9 +313,30 @@ mod tests { 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] fn require_input_argument() { - let mut env = environment(&[]); + let mut env = env! { args: [], tree: {} }; assert!(matches!(env.run(), Err(Error::Clap { .. }))); } @@ -316,8 +348,12 @@ mod tests { #[test] fn torrent_file_is_bencode_dict() { - let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); - fs::write(env.resolve("foo"), "").unwrap(); + let mut env = env! { + args: ["--input", "foo", "--announce", "https://bar"], + tree: { + foo: "", + } + }; env.run().unwrap(); let torrent = env.resolve("foo.torrent"); let bytes = fs::read(torrent).unwrap(); @@ -327,54 +363,70 @@ mod tests { #[test] fn privacy_defaults_to_false() { - let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); - fs::write(env.resolve("foo"), "").unwrap(); + let mut env = env! { + args: ["--input", "foo", "--announce", "https://bar"], + tree: { + foo: "", + } + }; env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.info.private, None); } #[test] fn privacy_flag_sets_privacy() { - let mut env = environment(&["--input", "foo", "--announce", "http://bar", "--private"]); - fs::write(env.resolve("foo"), "").unwrap(); + let mut env = env! { + args: ["--input", "foo", "--announce", "https://bar", "--private"], + tree: { + foo: "", + } + }; env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); - assert_eq!(metainfo.info.private, Some(1)); + let metainfo = env.load_torrent("foo.torrent"); + assert_eq!(metainfo.info.private, Some(true)); } #[test] fn tracker_flag_must_be_url() { - let mut env = environment(&["--input", "foo", "--announce", "bar"]); - fs::write(env.resolve("foo"), "").unwrap(); + let mut env = env! { + args: ["--input", "foo", "--announce", "bar"], + tree: { + foo: "", + } + }; assert_matches!(env.run(), Err(Error::Clap { .. })); } #[test] fn announce_single() { - let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); - fs::write(env.resolve("foo"), "").unwrap(); + let mut env = env! { + args: ["--input", "foo", "--announce", "http://bar"], + tree: { + foo: "", + } + }; env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.announce, "http://bar/"); assert!(metainfo.announce_list.is_none()); } #[test] fn announce_udp() { - let mut env = environment(&[ - "--input", - "foo", - "--announce", - "udp://tracker.opentrackr.org:1337/announce", - ]); - fs::write(env.resolve("foo"), "").unwrap(); + let mut env = env! { + args: [ + "--input", + "foo", + "--announce", + "udp://tracker.opentrackr.org:1337/announce", + ], + tree: { + foo: "", + } + }; env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!( metainfo.announce, "udp://tracker.opentrackr.org:1337/announce" @@ -387,8 +439,7 @@ mod tests { let mut env = environment(&["--input", "foo", "--announce", "wss://tracker.btorrent.xyz"]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.announce, "wss://tracker.btorrent.xyz/"); assert!(metainfo.announce_list.is_none()); } @@ -405,8 +456,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.announce, "http://bar/"); assert_eq!( metainfo.announce_list, @@ -428,8 +478,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.announce, "http://bar/"); assert_eq!( metainfo.announce_list, @@ -445,8 +494,7 @@ mod tests { let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.comment, None); } @@ -462,8 +510,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.comment.unwrap(), "Hello, world!"); } @@ -472,9 +519,8 @@ mod tests { let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); - assert_eq!(metainfo.info.piece_length, 16 * 2u32.pow(10)); + let metainfo = env.load_torrent("foo.torrent"); + assert_eq!(metainfo.info.piece_length, Bytes::from(16 * 2u32.pow(10))); } #[test] @@ -489,9 +535,8 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); - assert_eq!(metainfo.info.piece_length, 64 * 1024); + let metainfo = env.load_torrent("foo.torrent"); + assert_eq!(metainfo.info.piece_length, Bytes(64 * 1024)); } #[test] @@ -506,9 +551,8 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); - assert_eq!(metainfo.info.piece_length, 512 * 1024); + let metainfo = env.load_torrent("foo.torrent"); + assert_eq!(metainfo.info.piece_length, Bytes(512 * 1024)); } #[test] @@ -523,8 +567,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.info.name, "foo"); } @@ -542,8 +585,7 @@ mod tests { fs::create_dir(&dir).unwrap(); fs::write(dir.join("bar"), "").unwrap(); env.run().unwrap(); - let torrent = dir.join("bar.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo/bar.torrent"); assert_eq!(metainfo.info.name, "bar"); } @@ -559,8 +601,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let torrent = env.resolve("x.torrent"); - Metainfo::load(torrent).unwrap(); + env.load_torrent("x.torrent"); } #[test] @@ -568,8 +609,7 @@ mod tests { let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.created_by.unwrap(), consts::CREATED_BY_DEFAULT); } @@ -584,8 +624,7 @@ mod tests { ]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.created_by, None); } @@ -594,8 +633,7 @@ mod tests { let mut env = environment(&["--input", "foo", "--announce", "http://bar"]); fs::write(env.resolve("foo"), "").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.encoding, Some("UTF-8".into())); } @@ -608,8 +646,7 @@ mod tests { .unwrap() .as_secs(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); 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(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.creation_date, None); } @@ -636,13 +672,12 @@ mod tests { let contents = "bar"; fs::write(env.resolve("foo"), contents).unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes()); assert_eq!( metainfo.info.mode, Mode::Single { - length: contents.len() as u64, + length: Bytes(contents.len() as u64), md5sum: None, } ) @@ -663,8 +698,7 @@ mod tests { let contents = "bar"; fs::write(env.resolve("foo"), contents).unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); let pieces = Sha1::from("b") .digest() .bytes() @@ -678,7 +712,7 @@ mod tests { assert_eq!( metainfo.info.mode, Mode::Single { - length: contents.len() as u64, + length: Bytes(contents.len() as u64), md5sum: None, } ) @@ -690,13 +724,12 @@ mod tests { let contents = ""; fs::write(env.resolve("foo"), contents).unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.info.pieces.len(), 0); assert_eq!( metainfo.info.mode, Mode::Single { - length: 0, + length: Bytes(0), md5sum: None, } ) @@ -708,8 +741,7 @@ mod tests { let dir = env.resolve("foo"); fs::create_dir(&dir).unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.info.pieces.len(), 0); assert_eq!(metainfo.info.mode, Mode::Multiple { files: Vec::new() }) } @@ -723,16 +755,15 @@ mod tests { let contents = "bar"; fs::write(file, contents).unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes()); match metainfo.info.mode { Mode::Multiple { files } => { assert_eq!( files, &[FileInfo { - length: 3, - md5sum: Some("37b51d194a7513e45b56f6524f2d51f2".to_owned()), + length: Bytes(3), + md5sum: Some(Md5Digest::from_hex("37b51d194a7513e45b56f6524f2d51f2")), path: FilePath::from_components(&["bar"]), },] ); @@ -750,15 +781,14 @@ mod tests { let contents = "bar"; fs::write(file, contents).unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.info.pieces, Sha1::from(contents).digest().bytes()); match metainfo.info.mode { Mode::Multiple { files } => { assert_eq!( files, &[FileInfo { - length: 3, + length: Bytes(3), md5sum: None, path: FilePath::from_components(&["bar"]), },] @@ -777,8 +807,7 @@ mod tests { fs::write(dir.join("x"), "xyz").unwrap(); fs::write(dir.join("h"), "hij").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!( metainfo.info.pieces, Sha1::from("abchijxyz").digest().bytes() @@ -789,18 +818,18 @@ mod tests { files, &[ FileInfo { - length: 3, - md5sum: Some("900150983cd24fb0d6963f7d28e17f72".to_owned()), + length: Bytes(3), + md5sum: Some(Md5Digest::from_hex("900150983cd24fb0d6963f7d28e17f72")), path: FilePath::from_components(&["a"]), }, FileInfo { - length: 3, - md5sum: Some("857c4402ad934005eae4638a93812bf7".to_owned()), + length: Bytes(3), + md5sum: Some(Md5Digest::from_hex("857c4402ad934005eae4638a93812bf7")), path: FilePath::from_components(&["h"]), }, FileInfo { - length: 3, - md5sum: Some("d16fb36f0911f878998c136191af705e".to_owned()), + length: Bytes(3), + md5sum: Some(Md5Digest::from_hex("d16fb36f0911f878998c136191af705e")), path: FilePath::from_components(&["x"]), }, ] @@ -895,6 +924,7 @@ mod tests { let dir = env.resolve("foo"); fs::create_dir(&dir).unwrap(); env.run().unwrap(); + env.load_torrent("foo.torrent"); } #[test] @@ -942,6 +972,7 @@ mod tests { let dir = env.resolve("foo"); fs::create_dir(&dir).unwrap(); env.run().unwrap(); + env.load_torrent("foo.torrent"); } #[test] @@ -1022,10 +1053,7 @@ Content Size 9 bytes fs::write(env.resolve("foo"), "").unwrap(); fs::write(env.resolve("foo.torrent"), "foo").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let bytes = fs::read(torrent).unwrap(); - let value = Value::from_bencode(&bytes).unwrap(); - assert!(matches!(value, Value::Dict(_))); + env.load_torrent("foo.torrent"); } #[test] @@ -1036,8 +1064,7 @@ Content Size 9 bytes fs::write(dir.join("Thumbs.db"), "abc").unwrap(); fs::write(dir.join("Desktop.ini"), "abc").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_matches!( metainfo.info.mode, 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("Desktop.ini"), "abc").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 2 @@ -1095,8 +1121,7 @@ Content Size 9 bytes .unwrap(); } env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 0 @@ -1117,8 +1142,7 @@ Content Size 9 bytes fs::create_dir(&dir).unwrap(); fs::write(dir.join(".hidden"), "abc").unwrap(); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_matches!( metainfo.info.mode, 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"]); populate_symlinks(&env); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.is_empty() @@ -1183,8 +1206,7 @@ Content Size 9 bytes ]); populate_symlinks(&env); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_eq!(metainfo.info.pieces, Sha1::from("barbaz").digest().bytes()); match metainfo.info.mode { Mode::Multiple { files } => { @@ -1192,13 +1214,13 @@ Content Size 9 bytes files, &[ FileInfo { - length: 3, - md5sum: Some("37b51d194a7513e45b56f6524f2d51f2".to_owned()), + length: Bytes(3), + md5sum: Some(Md5Digest::from_hex("37b51d194a7513e45b56f6524f2d51f2")), path: FilePath::from_components(&["bar"]), }, FileInfo { - length: 3, - md5sum: Some("73feffa4b7f6bb68e44cf984c85f6e88".to_owned()), + length: Bytes(3), + md5sum: Some(Md5Digest::from_hex("73feffa4b7f6bb68e44cf984c85f6e88")), path: FilePath::from_components(&["dir", "baz"]), }, ] @@ -1231,8 +1253,7 @@ Content Size 9 bytes env.create_dir("foo/.bar"); env.create_file("foo/.bar/baz", "baz"); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.is_empty() @@ -1265,8 +1286,7 @@ Content Size 9 bytes .unwrap(); } env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_matches!( metainfo.info.mode, 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/c", "c"); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_matches!( metainfo.info.mode, 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/c", "c"); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_matches!( metainfo.info.mode, 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/c", "c"); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_matches!( metainfo.info.mode, 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/c", "c"); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_matches!( metainfo.info.mode, 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/c", "c"); env.run().unwrap(); - let torrent = env.resolve("foo.torrent"); - let metainfo = Metainfo::load(torrent).unwrap(); + let metainfo = env.load_torrent("foo.torrent"); assert_matches!( metainfo.info.mode, Mode::Multiple { files } if files.len() == 1 diff --git a/src/opt/torrent/piece_length.rs b/src/opt/torrent/piece_length.rs index aee8f28..f8700ff 100644 --- a/src/opt/torrent/piece_length.rs +++ b/src/opt/torrent/piece_length.rs @@ -21,7 +21,7 @@ impl PieceLength { )); 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); diff --git a/src/opt/torrent/show.rs b/src/opt/torrent/show.rs index a88913d..b26e152 100644 --- a/src/opt/torrent/show.rs +++ b/src/opt/torrent/show.rs @@ -18,10 +18,9 @@ pub(crate) struct Show { impl Show { 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)?; - Ok(()) } } @@ -42,15 +41,15 @@ mod tests { creation_date: Some(1), encoding: Some("UTF-8".into()), info: Info { - private: Some(1), - piece_length: 16 * 1024, + 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: 20, + length: Bytes(20), md5sum: None, }, }, @@ -133,15 +132,15 @@ Files\tfoo creation_date: Some(1), encoding: Some("UTF-8".into()), info: Info { - private: Some(1), - piece_length: 16 * 1024, + 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: 20, + length: Bytes(20), md5sum: None, }, }, @@ -225,15 +224,15 @@ Files\tfoo creation_date: Some(1), encoding: Some("UTF-8".into()), info: Info { - private: Some(1), - piece_length: 16 * 1024, + 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: 20, + length: Bytes(20), md5sum: None, }, }, diff --git a/src/opt/torrent/stats.rs b/src/opt/torrent/stats.rs index 6104633..16f3261 100644 --- a/src/opt/torrent/stats.rs +++ b/src/opt/torrent/stats.rs @@ -160,7 +160,7 @@ impl Extractor { return; }; - if let Ok(value) = Value::from_bencode(&contents) { + if let Ok(value) = bendy::serde::de::from_bytes::(&contents) { self.extract(&value); if self.print { eprintln!("{}:\n{}", path.display(), Self::pretty_print(&value)); diff --git a/src/opt/torrent/verify.rs b/src/opt/torrent/verify.rs new file mode 100644 index 0000000..738eb93 --- /dev/null +++ b/src/opt/torrent/verify.rs @@ -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, +} + +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 { .. }))); + } +} diff --git a/src/piece_length_picker.rs b/src/piece_length_picker.rs index 22e8134..397db79 100644 --- a/src/piece_length_picker.rs +++ b/src/piece_length_picker.rs @@ -29,14 +29,14 @@ impl PieceLengthPicker { clippy::cast_precision_loss, clippy::cast_possible_truncation )] - let exponent = (content_size.count() as f64).log2().ceil() as u128; - Bytes::from(1u128 << (exponent / 2 + 4)) + let exponent = (content_size.count().max(1) as f64).log2().ceil() as u64; + Bytes::from(1u64 << (exponent / 2 + 4)) .max(Bytes::kib() * 16) .min(Bytes::mib() * 16) } - pub(crate) fn piece_count(content_size: Bytes, piece_length: Bytes) -> u128 { - if content_size == Bytes::from(0u128) { + pub(crate) fn piece_count(content_size: Bytes, piece_length: Bytes) -> u64 { + if content_size == Bytes::from(0u64) { 0 } else { (content_size / piece_length).max(1) @@ -44,7 +44,7 @@ impl PieceLengthPicker { } 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) } } diff --git a/src/status.rs b/src/status.rs new file mode 100644 index 0000000..fab01ef --- /dev/null +++ b/src/status.rs @@ -0,0 +1,53 @@ +use crate::common::*; + +#[derive(Debug)] +pub(crate) struct Status { + pieces: bool, + files: Vec, +} + +impl Status { + pub(crate) fn new(pieces: bool, files: Vec) -> 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(()) + } +} diff --git a/src/test_env.rs b/src/test_env.rs index 5ca6fad..81895af 100644 --- a/src/test_env.rs +++ b/src/test_env.rs @@ -30,6 +30,10 @@ impl TestEnv { pub(crate) fn create_file(&self, path: impl AsRef, bytes: impl AsRef<[u8]>) { fs::write(self.env.resolve(path), bytes.as_ref()).unwrap(); } + + pub(crate) fn load_torrent(&self, filename: impl AsRef) -> Metainfo { + Metainfo::load(self.env.resolve(filename.as_ref())).unwrap() + } } impl Deref for TestEnv { diff --git a/src/test_env_builder.rs b/src/test_env_builder.rs index b359cb3..2ec1344 100644 --- a/src/test_env_builder.rs +++ b/src/test_env_builder.rs @@ -4,6 +4,7 @@ pub(crate) struct TestEnvBuilder { args: Vec, out_is_term: bool, use_color: bool, + tempdir: Option, } impl TestEnvBuilder { @@ -12,6 +13,7 @@ impl TestEnvBuilder { args: Vec::new(), out_is_term: false, use_color: false, + tempdir: None, } } @@ -39,12 +41,17 @@ impl TestEnvBuilder { self } + pub(crate) fn tempdir(mut self, tempdir: TempDir) -> Self { + self.tempdir = Some(tempdir); + self + } + pub(crate) fn build(self) -> TestEnv { let err = Capture::new(); let out = Capture::new(); let env = Env::new( - tempfile::tempdir().unwrap(), + self.tempdir.unwrap_or_else(|| tempfile::tempdir().unwrap()), out.clone(), if self.use_color && self.out_is_term { Style::active() diff --git a/src/torrent_summary.rs b/src/torrent_summary.rs index 40c0047..23f9691 100644 --- a/src/torrent_summary.rs +++ b/src/torrent_summary.rs @@ -13,7 +13,7 @@ impl TorrentSummary { let infohash = if let Value::Dict(items) = value { let info = items .iter() - .find(|pair: &(&Vec, &Value)| pair.0 == b"info") + .find(|pair: &(&Cow<[u8]>, &Value)| pair.0.as_ref() == b"info") .unwrap() .1 .to_bencode() @@ -99,7 +99,7 @@ impl TorrentSummary { table.row( "Private", - if self.metainfo.info.private.unwrap_or(0) == 1 { + if self.metainfo.info.private.unwrap_or(false) { "yes" } else { "no" @@ -142,7 +142,7 @@ impl TorrentSummary { 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); diff --git a/src/verifier.rs b/src/verifier.rs new file mode 100644 index 0000000..c19ca34 --- /dev/null +++ b/src/verifier.rs @@ -0,0 +1,79 @@ +use crate::common::*; + +pub(crate) struct Verifier { + buffer: Vec, + piece_length: usize, + pieces: Vec, + 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 { + 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(()) + } +}