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:
parent
b0f449b6ae
commit
6549850dac
98
Cargo.lock
generated
98
Cargo.lock
generated
|
@ -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]]
|
||||
|
|
43
Cargo.toml
43
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
|
||||
|
|
4
justfile
4
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
|
||||
|
|
108
src/bytes.rs
108
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<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(
|
||||
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<I: Into<u128>> From<I> for Bytes {
|
||||
impl<I: Into<u64>> From<I> for Bytes {
|
||||
fn from(n: I) -> Bytes {
|
||||
Bytes(n.into())
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl Div<u128> for Bytes {
|
||||
impl Div<u64> for Bytes {
|
||||
type Output = Bytes;
|
||||
|
||||
fn div(self, rhs: u128) -> Bytes {
|
||||
fn div(self, rhs: u64) -> Bytes {
|
||||
Bytes::from(self.0 / rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl DivAssign<u128> for Bytes {
|
||||
fn div_assign(&mut self, rhs: u128) {
|
||||
impl DivAssign<u64> for Bytes {
|
||||
fn div_assign(&mut self, rhs: u64) {
|
||||
self.0 /= rhs;
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<u128> for Bytes {
|
||||
impl Mul<u64> for Bytes {
|
||||
type Output = Bytes;
|
||||
|
||||
fn mul(self, rhs: u128) -> Self {
|
||||
fn mul(self, rhs: u64) -> Self {
|
||||
Bytes::from(self.0 * rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl MulAssign<u128> for Bytes {
|
||||
fn mul_assign(&mut self, rhs: u128) {
|
||||
impl MulAssign<u64> for Bytes {
|
||||
fn mul_assign(&mut self, rhs: u64) {
|
||||
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 {
|
||||
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<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 {
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,14 +51,20 @@ 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
|
||||
// type aliases
|
||||
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,
|
||||
|
@ -65,13 +73,16 @@ pub(crate) use std::{
|
|||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
// test dependencies
|
||||
pub(crate) use tempfile::TempDir;
|
||||
pub(crate) use temptree::temptree;
|
||||
|
||||
// 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<T, E = Error> = std::result::Result<T, E>;
|
||||
#[cfg(test)]
|
||||
pub(crate) use test::*;
|
||||
|
|
|
@ -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)?;
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<String>,
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
default,
|
||||
with = "unwrap_or_skip"
|
||||
)]
|
||||
pub(crate) md5sum: Option<Md5Digest>,
|
||||
}
|
||||
|
|
114
src/file_status.rs
Normal file
114
src/file_status.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -4,8 +4,8 @@ pub(crate) struct Hasher {
|
|||
buffer: Vec<u8>,
|
||||
length: u64,
|
||||
md5sum: bool,
|
||||
piece_bytes_hashed: u64,
|
||||
piece_length: u32,
|
||||
piece_bytes_hashed: usize,
|
||||
piece_length: usize,
|
||||
pieces: Vec<u8>,
|
||||
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<u8>), 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<md5::Digest>, u64), Error> {
|
||||
fn hash_file(&mut self, file: &Path) -> Result<(Option<md5::Digest>, Bytes), Error> {
|
||||
self
|
||||
.hash_file_io(file)
|
||||
.context(error::Filesystem { path: file })
|
||||
}
|
||||
|
||||
fn hash_file_io(&mut self, file: &Path) -> io::Result<(Option<md5::Digest>, u64)> {
|
||||
fn hash_file_io(&mut self, file: &Path) -> io::Result<(Option<md5::Digest>, 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)))
|
||||
}
|
||||
}
|
||||
|
|
19
src/info.rs
19
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<u8>,
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
default,
|
||||
with = "unwrap_or_skip"
|
||||
)]
|
||||
pub(crate) private: Option<bool>,
|
||||
#[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<String>,
|
||||
#[serde(with = "serde_bytes")]
|
||||
pub(crate) pieces: Vec<u8>,
|
||||
|
|
52
src/inner.rs
52
src/inner.rs
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
53
src/md5_digest.rs
Normal file
53
src/md5_digest.rs
Normal 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);
|
||||
}
|
||||
}
|
158
src/metainfo.rs
158
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<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>,
|
||||
#[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>,
|
||||
#[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>,
|
||||
#[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) info: Info,
|
||||
}
|
||||
|
||||
impl Metainfo {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn load(path: impl AsRef<Path>) -> Result<Metainfo, Error> {
|
||||
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<Path>) -> 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<Path>, bytes: &[u8]) -> Result<Metainfo, Error> {
|
||||
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<Vec<u8>, 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<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);
|
||||
}
|
||||
}
|
||||
|
|
88
src/mode.rs
88
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<String>,
|
||||
length: Bytes,
|
||||
#[serde(
|
||||
skip_serializing_if = "Option::is_none",
|
||||
default,
|
||||
with = "unwrap_or_skip"
|
||||
)]
|
||||
md5sum: Option<Md5Digest>,
|
||||
},
|
||||
Multiple {
|
||||
files: Vec<FileInfo>,
|
||||
|
@ -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::<u64>()),
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(&[
|
||||
let mut env = env! {
|
||||
args: [
|
||||
"--input",
|
||||
"foo",
|
||||
"--announce",
|
||||
"udp://tracker.opentrackr.org:1337/announce",
|
||||
]);
|
||||
fs::write(env.resolve("foo"), "").unwrap();
|
||||
],
|
||||
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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -160,7 +160,7 @@ impl Extractor {
|
|||
return;
|
||||
};
|
||||
|
||||
if let Ok(value) = Value::from_bencode(&contents) {
|
||||
if let Ok(value) = bendy::serde::de::from_bytes::<Value>(&contents) {
|
||||
self.extract(&value);
|
||||
if self.print {
|
||||
eprintln!("{}:\n{}", path.display(), Self::pretty_print(&value));
|
||||
|
|
69
src/opt/torrent/verify.rs
Normal file
69
src/opt/torrent/verify.rs
Normal 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 { .. })));
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
53
src/status.rs
Normal file
53
src/status.rs
Normal 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(())
|
||||
}
|
||||
}
|
|
@ -30,6 +30,10 @@ impl TestEnv {
|
|||
pub(crate) fn create_file(&self, path: impl AsRef<Path>, bytes: impl AsRef<[u8]>) {
|
||||
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 {
|
||||
|
|
|
@ -4,6 +4,7 @@ pub(crate) struct TestEnvBuilder {
|
|||
args: Vec<String>,
|
||||
out_is_term: bool,
|
||||
use_color: bool,
|
||||
tempdir: Option<TempDir>,
|
||||
}
|
||||
|
||||
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()
|
||||
|
|
|
@ -13,7 +13,7 @@ impl TorrentSummary {
|
|||
let infohash = if let Value::Dict(items) = value {
|
||||
let info = items
|
||||
.iter()
|
||||
.find(|pair: &(&Vec<u8>, &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);
|
||||
|
||||
|
|
79
src/verifier.rs
Normal file
79
src/verifier.rs
Normal 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(())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user