Print individual file torrent verification errors
If torrent verification fails, print all errors with individual files. type: changed
This commit is contained in:
parent
f8e3fd594b
commit
1532113782
16
src/bytes.rs
16
src/bytes.rs
|
@ -30,6 +30,14 @@ impl Bytes {
|
||||||
.try_into()
|
.try_into()
|
||||||
.context(error::PieceLengthTooLarge { bytes: self })
|
.context(error::PieceLengthTooLarge { bytes: self })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn absolute_difference(self, other: Bytes) -> Bytes {
|
||||||
|
if self > other {
|
||||||
|
self - other
|
||||||
|
} else {
|
||||||
|
other - self
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn float_to_int(x: f64) -> u64 {
|
fn float_to_int(x: f64) -> u64 {
|
||||||
|
@ -101,6 +109,14 @@ impl Div<Bytes> for Bytes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Sub<Bytes> for Bytes {
|
||||||
|
type Output = Bytes;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Bytes) -> Bytes {
|
||||||
|
Bytes(self.count() - rhs.count())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Div<u64> for Bytes {
|
impl Div<u64> for Bytes {
|
||||||
type Output = Bytes;
|
type Output = Bytes;
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,9 @@ pub(crate) use std::{
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
iter::{self, Sum},
|
iter::Sum,
|
||||||
num::{ParseFloatError, ParseIntError, TryFromIntError},
|
num::{ParseFloatError, ParseIntError, TryFromIntError},
|
||||||
ops::{AddAssign, Div, DivAssign, Mul, MulAssign, SubAssign},
|
ops::{AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign},
|
||||||
path::{self, Path, PathBuf},
|
path::{self, Path, PathBuf},
|
||||||
process::{self, Command, ExitStatus},
|
process::{self, Command, ExitStatus},
|
||||||
str::{self, FromStr},
|
str::{self, FromStr},
|
||||||
|
@ -48,18 +48,18 @@ pub(crate) use crate::{consts, error};
|
||||||
// traits
|
// traits
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
into_u64::IntoU64, into_usize::IntoUsize, path_ext::PathExt,
|
into_u64::IntoU64, into_usize::IntoUsize, path_ext::PathExt,
|
||||||
platform_interface::PlatformInterface, reckoner::Reckoner, step::Step,
|
platform_interface::PlatformInterface, print::Print, reckoner::Reckoner, step::Step,
|
||||||
};
|
};
|
||||||
|
|
||||||
// structs and enums
|
// structs and enums
|
||||||
pub(crate) use crate::{
|
pub(crate) use crate::{
|
||||||
arguments::Arguments, bytes::Bytes, env::Env, error::Error, file_info::FileInfo,
|
arguments::Arguments, bytes::Bytes, env::Env, error::Error, file_error::FileError,
|
||||||
file_path::FilePath, file_status::FileStatus, files::Files, hasher::Hasher, info::Info,
|
file_info::FileInfo, file_path::FilePath, file_status::FileStatus, files::Files, hasher::Hasher,
|
||||||
lint::Lint, linter::Linter, md5_digest::Md5Digest, metainfo::Metainfo, mode::Mode, node::Node,
|
info::Info, lint::Lint, linter::Linter, md5_digest::Md5Digest, metainfo::Metainfo, mode::Mode,
|
||||||
options::Options, output_target::OutputTarget, piece_length_picker::PieceLengthPicker,
|
node::Node, options::Options, output_stream::OutputStream, output_target::OutputTarget,
|
||||||
piece_list::PieceList, platform::Platform, sha1_digest::Sha1Digest, status::Status, style::Style,
|
piece_length_picker::PieceLengthPicker, piece_list::PieceList, platform::Platform,
|
||||||
subcommand::Subcommand, table::Table, torrent_summary::TorrentSummary, use_color::UseColor,
|
sha1_digest::Sha1Digest, status::Status, style::Style, subcommand::Subcommand, table::Table,
|
||||||
verifier::Verifier, walker::Walker,
|
torrent_summary::TorrentSummary, use_color::UseColor, verifier::Verifier, walker::Walker,
|
||||||
};
|
};
|
||||||
|
|
||||||
// type aliases
|
// type aliases
|
||||||
|
|
95
src/env.rs
95
src/env.rs
|
@ -3,12 +3,8 @@ use crate::common::*;
|
||||||
pub(crate) struct Env {
|
pub(crate) struct Env {
|
||||||
args: Vec<OsString>,
|
args: Vec<OsString>,
|
||||||
dir: PathBuf,
|
dir: PathBuf,
|
||||||
pub(crate) err: Box<dyn Write>,
|
err: OutputStream,
|
||||||
pub(crate) out: Box<dyn Write>,
|
out: OutputStream,
|
||||||
err_style: Style,
|
|
||||||
out_style: Style,
|
|
||||||
out_is_term: bool,
|
|
||||||
err_is_term: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Env {
|
impl Env {
|
||||||
|
@ -18,34 +14,13 @@ impl Env {
|
||||||
Err(error) => panic!("Failed to get current directory: {}", error),
|
Err(error) => panic!("Failed to get current directory: {}", error),
|
||||||
};
|
};
|
||||||
|
|
||||||
let no_color = env::var_os("NO_COLOR").is_some()
|
let style = env::var_os("NO_COLOR").is_none()
|
||||||
|| env::var_os("TERM").as_deref() == Some(OsStr::new("dumb"));
|
&& env::var_os("TERM").as_deref() != Some(OsStr::new("dumb"));
|
||||||
|
|
||||||
let err_style = if no_color || !atty::is(atty::Stream::Stderr) {
|
let out_stream = OutputStream::stdout(style);
|
||||||
Style::inactive()
|
let err_stream = OutputStream::stderr(style);
|
||||||
} else {
|
|
||||||
Style::active()
|
|
||||||
};
|
|
||||||
|
|
||||||
let out_style = if no_color || !atty::is(atty::Stream::Stdout) {
|
Self::new(dir, env::args(), out_stream, err_stream)
|
||||||
Style::inactive()
|
|
||||||
} else {
|
|
||||||
Style::active()
|
|
||||||
};
|
|
||||||
|
|
||||||
let out_is_term = atty::is(atty::Stream::Stdout);
|
|
||||||
let err_is_term = atty::is(atty::Stream::Stderr);
|
|
||||||
|
|
||||||
Self::new(
|
|
||||||
dir,
|
|
||||||
io::stdout(),
|
|
||||||
out_style,
|
|
||||||
out_is_term,
|
|
||||||
io::stderr(),
|
|
||||||
err_style,
|
|
||||||
err_is_term,
|
|
||||||
env::args(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run(&mut self) -> Result<(), Error> {
|
pub(crate) fn run(&mut self) -> Result<(), Error> {
|
||||||
|
@ -57,40 +32,23 @@ impl Env {
|
||||||
|
|
||||||
let args = Arguments::from_iter_safe(&self.args)?;
|
let args = Arguments::from_iter_safe(&self.args)?;
|
||||||
|
|
||||||
match args.options().use_color {
|
let use_color = args.options().use_color;
|
||||||
UseColor::Always => self.err_style = Style::active(),
|
self.err.set_use_color(use_color);
|
||||||
UseColor::Auto => {}
|
self.out.set_use_color(use_color);
|
||||||
UseColor::Never => self.err_style = Style::inactive(),
|
|
||||||
}
|
|
||||||
|
|
||||||
args.run(self)
|
args.run(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new<O, E, S, I>(
|
pub(crate) fn new<S, I>(dir: PathBuf, args: I, out: OutputStream, err: OutputStream) -> Self
|
||||||
dir: PathBuf,
|
|
||||||
out: O,
|
|
||||||
out_style: Style,
|
|
||||||
out_is_term: bool,
|
|
||||||
err: E,
|
|
||||||
err_style: Style,
|
|
||||||
err_is_term: bool,
|
|
||||||
args: I,
|
|
||||||
) -> Self
|
|
||||||
where
|
where
|
||||||
O: Write + 'static,
|
|
||||||
E: Write + 'static,
|
|
||||||
S: Into<OsString>,
|
S: Into<OsString>,
|
||||||
I: IntoIterator<Item = S>,
|
I: IntoIterator<Item = S>,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
args: args.into_iter().map(Into::into).collect(),
|
args: args.into_iter().map(Into::into).collect(),
|
||||||
err: Box::new(err),
|
|
||||||
out: Box::new(out),
|
|
||||||
dir,
|
dir,
|
||||||
out_style,
|
out,
|
||||||
out_is_term,
|
err,
|
||||||
err_style,
|
|
||||||
err_is_term,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,21 +67,24 @@ impl Env {
|
||||||
_ => Err(EXIT_FAILURE),
|
_ => Err(EXIT_FAILURE),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
let style = self.err.style();
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut self.err,
|
&mut self.err,
|
||||||
"{}{}: {}{}",
|
"{}{}: {}{}",
|
||||||
self.err_style.error().paint("error"),
|
style.error().paint("error"),
|
||||||
self.err_style.message().prefix(),
|
style.message().prefix(),
|
||||||
error,
|
error,
|
||||||
self.err_style.message().suffix(),
|
style.message().suffix(),
|
||||||
)
|
)
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
|
error.print_body(self).ok();
|
||||||
|
|
||||||
if let Some(lint) = error.lint() {
|
if let Some(lint) = error.lint() {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut self.err,
|
&mut self.err,
|
||||||
"{}: This check can be disabled with `--allow {}`.",
|
"{}: This check can be disabled with `--allow {}`.",
|
||||||
self.err_style.message().paint("note"),
|
style.message().paint("note"),
|
||||||
lint.name()
|
lint.name()
|
||||||
)
|
)
|
||||||
.ok();
|
.ok();
|
||||||
|
@ -144,20 +105,20 @@ impl Env {
|
||||||
self.dir().join(path).clean()
|
self.dir().join(path).clean()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn out_is_term(&self) -> bool {
|
pub(crate) fn err(&self) -> &OutputStream {
|
||||||
self.out_is_term
|
&self.err
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn out_style(&self) -> Style {
|
pub(crate) fn err_mut(&mut self) -> &mut OutputStream {
|
||||||
self.out_style
|
&mut self.err
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn err_is_term(&self) -> bool {
|
pub(crate) fn out(&self) -> &OutputStream {
|
||||||
self.err_is_term
|
&self.out
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn err_style(&self) -> Style {
|
pub(crate) fn out_mut(&mut self) -> &mut OutputStream {
|
||||||
self.err_style
|
&mut self.out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
macro_rules! err {
|
macro_rules! err {
|
||||||
($env:expr, $fmt:expr) => {
|
($env:expr, $fmt:expr) => {
|
||||||
write!($env.err, $fmt).context(crate::error::Stderr)
|
write!($env.err_mut(), $fmt).context(crate::error::Stderr)
|
||||||
};
|
};
|
||||||
($env:expr, $fmt:expr, $($arg:tt)*) => {
|
($env:expr, $fmt:expr, $($arg:tt)*) => {
|
||||||
write!($env.err, $fmt, $($arg)*).context(crate::error::Stderr)
|
write!($env.err_mut(), $fmt, $($arg)*).context(crate::error::Stderr)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
macro_rules! errln {
|
macro_rules! errln {
|
||||||
($env:expr) => {
|
($env:expr) => {
|
||||||
writeln!($env.err, "").context(crate::error::Stderr)
|
writeln!($env.err_mut(), "").context(crate::error::Stderr)
|
||||||
};
|
};
|
||||||
($env:expr, $fmt:expr) => {
|
($env:expr, $fmt:expr) => {
|
||||||
writeln!($env.err, $fmt).context(crate::error::Stderr)
|
writeln!($env.err_mut(), $fmt).context(crate::error::Stderr)
|
||||||
};
|
};
|
||||||
($env:expr, $fmt:expr, $($arg:tt)*) => {
|
($env:expr, $fmt:expr, $($arg:tt)*) => {
|
||||||
writeln!($env.err, $fmt, $($arg)*).context(crate::error::Stderr)
|
writeln!($env.err_mut(), $fmt, $($arg)*).context(crate::error::Stderr)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
12
src/error.rs
12
src/error.rs
|
@ -119,7 +119,7 @@ pub(crate) enum Error {
|
||||||
feature
|
feature
|
||||||
))]
|
))]
|
||||||
Unstable { feature: &'static str },
|
Unstable { feature: &'static str },
|
||||||
#[snafu(display("Torrent verification failed: {}", status))]
|
#[snafu(display("Torrent verification failed."))]
|
||||||
Verify { status: Status },
|
Verify { status: Status },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,10 +133,18 @@ impl Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn internal(message: impl Into<String>) -> Error {
|
pub(crate) fn internal(message: impl Into<String>) -> Error {
|
||||||
Error::Internal {
|
Self::Internal {
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn print_body(&self, env: &mut Env) -> Result<()> {
|
||||||
|
if let Self::Verify { status } = self {
|
||||||
|
status.print_body(env)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<clap::Error> for Error {
|
impl From<clap::Error> for Error {
|
||||||
|
|
98
src/file_error.rs
Normal file
98
src/file_error.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) enum FileError {
|
||||||
|
Io(io::Error),
|
||||||
|
Missing,
|
||||||
|
Directory,
|
||||||
|
Surfeit(Bytes),
|
||||||
|
Dearth(Bytes),
|
||||||
|
Md5 {
|
||||||
|
expected: Md5Digest,
|
||||||
|
actual: Md5Digest,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileError {
|
||||||
|
pub(crate) fn verify(
|
||||||
|
path: &Path,
|
||||||
|
expected_length: Bytes,
|
||||||
|
expected_md5: Option<Md5Digest>,
|
||||||
|
) -> Result<(), FileError> {
|
||||||
|
let metadata = match path.metadata() {
|
||||||
|
Ok(metadata) => metadata,
|
||||||
|
Err(error) => {
|
||||||
|
if error.kind() == io::ErrorKind::NotFound {
|
||||||
|
return Err(FileError::Missing);
|
||||||
|
} else {
|
||||||
|
return Err(FileError::Io(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if metadata.is_dir() {
|
||||||
|
return Err(FileError::Directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
let actual = Bytes(metadata.len());
|
||||||
|
|
||||||
|
let difference = actual.absolute_difference(expected_length);
|
||||||
|
|
||||||
|
if actual > expected_length {
|
||||||
|
return Err(FileError::Surfeit(difference));
|
||||||
|
}
|
||||||
|
|
||||||
|
if actual < expected_length {
|
||||||
|
return Err(FileError::Dearth(difference));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(expected) = expected_md5 {
|
||||||
|
let mut reader = File::open(path)?;
|
||||||
|
let mut context = md5::Context::new();
|
||||||
|
io::copy(&mut reader, &mut context)?;
|
||||||
|
let actual = context.compute().into();
|
||||||
|
|
||||||
|
if actual != expected {
|
||||||
|
return Err(FileError::Md5 { actual, expected });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for FileError {
|
||||||
|
fn from(io_error: io::Error) -> Self {
|
||||||
|
Self::Io(io_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for FileError {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Io(io_error) => write!(f, "{}", io_error),
|
||||||
|
Self::Missing => write!(f, "File missing"),
|
||||||
|
Self::Directory => write!(f, "Expected file but found directory"),
|
||||||
|
Self::Surfeit(difference) => write!(f, "Extra bytes: {}", difference),
|
||||||
|
Self::Dearth(difference) => write!(f, "Missing bytes: {}", difference),
|
||||||
|
Self::Md5 { actual, expected } => write!(
|
||||||
|
f,
|
||||||
|
"MD5 checksum mismatch: {} (expected {})",
|
||||||
|
actual, expected
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Print for FileError {
|
||||||
|
fn print(&self, stream: &mut OutputStream) -> io::Result<()> {
|
||||||
|
let style = stream.style();
|
||||||
|
write!(
|
||||||
|
stream,
|
||||||
|
"{}{}{}",
|
||||||
|
style.error().prefix(),
|
||||||
|
self,
|
||||||
|
style.error().suffix(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,80 +2,35 @@ use crate::common::*;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct FileStatus {
|
pub(crate) struct FileStatus {
|
||||||
path: PathBuf,
|
path: FilePath,
|
||||||
error: Option<io::Error>,
|
error: Option<FileError>,
|
||||||
present: bool,
|
|
||||||
file: bool,
|
|
||||||
length_expected: Bytes,
|
|
||||||
length_actual: Option<Bytes>,
|
|
||||||
md5_expected: Option<Md5Digest>,
|
|
||||||
md5_actual: Option<Md5Digest>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileStatus {
|
impl FileStatus {
|
||||||
pub(crate) fn status(
|
pub(crate) fn status(
|
||||||
path: &Path,
|
absolute: &Path,
|
||||||
length_expected: Bytes,
|
path: FilePath,
|
||||||
md5_expected: Option<Md5Digest>,
|
length: Bytes,
|
||||||
|
md5: Option<Md5Digest>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut status = Self::new(path.to_owned(), length_expected, md5_expected);
|
let error = FileError::verify(absolute, length, md5).err();
|
||||||
|
|
||||||
if let Err(error) = status.verify() {
|
FileStatus { path, error }
|
||||||
status.error = Some(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
status
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(path: PathBuf, length_expected: Bytes, md5_expected: Option<Md5Digest>) -> Self {
|
pub(crate) fn is_good(&self) -> bool {
|
||||||
Self {
|
self.error.is_none()
|
||||||
error: None,
|
|
||||||
file: false,
|
|
||||||
md5_actual: None,
|
|
||||||
present: false,
|
|
||||||
length_actual: None,
|
|
||||||
length_expected,
|
|
||||||
md5_expected,
|
|
||||||
path,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify(&mut self) -> io::Result<()> {
|
pub(crate) fn is_bad(&self) -> bool {
|
||||||
let metadata = self.path.metadata()?;
|
!self.is_good()
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn md5(&self) -> bool {
|
pub(crate) fn error(&self) -> Option<&FileError> {
|
||||||
match (self.md5_actual, self.md5_expected) {
|
self.error.as_ref()
|
||||||
(Some(actual), Some(expected)) => actual == expected,
|
|
||||||
(None, None) => true,
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn good(&self) -> bool {
|
pub(crate) fn path(&self) -> &FilePath {
|
||||||
self.error.is_none() && self.present && self.file && self.md5()
|
&self.path
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn bad(&self) -> bool {
|
|
||||||
!self.good()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ mod common;
|
||||||
mod consts;
|
mod consts;
|
||||||
mod env;
|
mod env;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod file_error;
|
||||||
mod file_info;
|
mod file_info;
|
||||||
mod file_path;
|
mod file_path;
|
||||||
mod file_status;
|
mod file_status;
|
||||||
|
@ -70,12 +71,14 @@ mod metainfo;
|
||||||
mod mode;
|
mod mode;
|
||||||
mod node;
|
mod node;
|
||||||
mod options;
|
mod options;
|
||||||
|
mod output_stream;
|
||||||
mod output_target;
|
mod output_target;
|
||||||
mod path_ext;
|
mod path_ext;
|
||||||
mod piece_length_picker;
|
mod piece_length_picker;
|
||||||
mod piece_list;
|
mod piece_list;
|
||||||
mod platform;
|
mod platform;
|
||||||
mod platform_interface;
|
mod platform_interface;
|
||||||
|
mod print;
|
||||||
mod reckoner;
|
mod reckoner;
|
||||||
mod sha1_digest;
|
mod sha1_digest;
|
||||||
mod status;
|
mod status;
|
||||||
|
|
|
@ -34,6 +34,16 @@ impl From<md5::Digest> for Md5Digest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for Md5Digest {
|
||||||
|
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||||
|
for byte in &self.bytes {
|
||||||
|
write!(f, "{:x}", byte)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
@ -75,23 +75,6 @@ impl Metainfo {
|
||||||
Self::deserialize("<TEST>", bytes).unwrap()
|
Self::deserialize("<TEST>", 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, progress_bar: Option<ProgressBar>) -> Result<Status> {
|
pub(crate) fn verify(&self, base: &Path, progress_bar: Option<ProgressBar>) -> Result<Status> {
|
||||||
Verifier::verify(self, base, progress_bar)
|
Verifier::verify(self, base, progress_bar)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
macro_rules! outln {
|
macro_rules! outln {
|
||||||
($env:expr) => {
|
($env:expr) => {
|
||||||
writeln!($env.out, "").context(crate::error::Stderr)
|
writeln!($env.out_mut(), "").context(crate::error::Stdout)
|
||||||
};
|
};
|
||||||
($env:expr, $fmt:expr) => {
|
($env:expr, $fmt:expr) => {
|
||||||
writeln!($env.out, $fmt).context(crate::error::Stderr)
|
writeln!($env.out_mut(), $fmt).context(crate::error::Stdout)
|
||||||
};
|
};
|
||||||
($env:expr, $fmt:expr, $($arg:tt)*) => {
|
($env:expr, $fmt:expr, $($arg:tt)*) => {
|
||||||
writeln!($env.out, $fmt, $($arg)*).context(crate::error::Stderr)
|
writeln!($env.out_mut(), $fmt, $($arg)*).context(crate::error::Stdout)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
65
src/output_stream.rs
Normal file
65
src/output_stream.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
pub(crate) struct OutputStream {
|
||||||
|
stream: Box<dyn Write>,
|
||||||
|
style: bool,
|
||||||
|
term: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputStream {
|
||||||
|
pub(crate) fn stdout(style: bool) -> OutputStream {
|
||||||
|
let term = atty::is(atty::Stream::Stdout);
|
||||||
|
Self {
|
||||||
|
stream: Box::new(io::stdout()),
|
||||||
|
style: style && term,
|
||||||
|
term,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stderr(style: bool) -> OutputStream {
|
||||||
|
Self {
|
||||||
|
term: style && atty::is(atty::Stream::Stderr),
|
||||||
|
stream: Box::new(io::stderr()),
|
||||||
|
style,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn new(stream: Box<dyn Write>, style: bool, term: bool) -> OutputStream {
|
||||||
|
Self {
|
||||||
|
stream,
|
||||||
|
style,
|
||||||
|
term,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_use_color(&mut self, use_color: UseColor) {
|
||||||
|
match use_color {
|
||||||
|
UseColor::Always => self.style = true,
|
||||||
|
UseColor::Auto => {}
|
||||||
|
UseColor::Never => self.style = false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_term(&self) -> bool {
|
||||||
|
self.term
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_styled(&self) -> bool {
|
||||||
|
self.style
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn style(&self) -> Style {
|
||||||
|
Style::from_active(self.style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for OutputStream {
|
||||||
|
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
|
||||||
|
self.stream.write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.stream.flush()
|
||||||
|
}
|
||||||
|
}
|
11
src/print.rs
Normal file
11
src/print.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
use crate::common::*;
|
||||||
|
|
||||||
|
pub(crate) trait Print {
|
||||||
|
fn print(&self, stream: &mut OutputStream) -> io::Result<()>;
|
||||||
|
|
||||||
|
fn println(&self, stream: &mut OutputStream) -> io::Result<()> {
|
||||||
|
self.print(stream)?;
|
||||||
|
writeln!(stream)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,46 +1,89 @@
|
||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct Status {
|
pub(crate) enum Status {
|
||||||
pieces: bool,
|
Single {
|
||||||
files: Vec<FileStatus>,
|
pieces: bool,
|
||||||
|
error: Option<FileError>,
|
||||||
|
},
|
||||||
|
Multiple {
|
||||||
|
pieces: bool,
|
||||||
|
files: Vec<FileStatus>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Status {
|
impl Status {
|
||||||
pub(crate) fn new(pieces: bool, files: Vec<FileStatus>) -> Self {
|
pub(crate) fn single(pieces: bool, error: Option<FileError>) -> Self {
|
||||||
Self { pieces, files }
|
Status::Single { pieces, error }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn multiple(pieces: bool, files: Vec<FileStatus>) -> Self {
|
||||||
|
Status::Multiple { pieces, files }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn pieces(&self) -> bool {
|
pub(crate) fn pieces(&self) -> bool {
|
||||||
self.pieces
|
match self {
|
||||||
}
|
Self::Single { pieces, .. } | Self::Multiple { pieces, .. } => *pieces,
|
||||||
|
}
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn files(&self) -> &[FileStatus] {
|
|
||||||
&self.files
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn good(&self) -> bool {
|
pub(crate) fn good(&self) -> bool {
|
||||||
self.pieces && self.files.iter().all(FileStatus::good)
|
self.pieces()
|
||||||
|
&& match self {
|
||||||
|
Self::Single { error, .. } => error.is_none(),
|
||||||
|
Self::Multiple { files, .. } => files.iter().all(FileStatus::is_good),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for Status {
|
#[cfg(test)]
|
||||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
pub(crate) fn count_bad(&self) -> usize {
|
||||||
let bad = self.files.iter().filter(|status| status.bad()).count();
|
match self {
|
||||||
|
Self::Single { error, .. } => {
|
||||||
|
if error.is_some() {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Multiple { files, .. } => files.iter().filter(|file| file.is_bad()).count(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if bad != 0 {
|
pub(crate) fn print_body(&self, env: &mut Env) -> Result<()> {
|
||||||
write!(f, "{} of {} files corrupted", bad, self.files.len())?;
|
match self {
|
||||||
return Ok(());
|
Self::Single { error, .. } => {
|
||||||
|
if let Some(error) = error {
|
||||||
|
error.println(env.err_mut()).context(error::Stderr)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::Multiple { files, .. } => {
|
||||||
|
for file in files {
|
||||||
|
if let Some(error) = file.error() {
|
||||||
|
let style = env.err().style();
|
||||||
|
err!(
|
||||||
|
env,
|
||||||
|
"{}{}:{} ",
|
||||||
|
style.message().prefix(),
|
||||||
|
file.path(),
|
||||||
|
style.message().suffix(),
|
||||||
|
)?;
|
||||||
|
error.println(env.err_mut()).context(error::Stderr)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errln!(
|
||||||
|
env,
|
||||||
|
"{}/{} files corrupted.",
|
||||||
|
files.iter().filter(|file| file.is_bad()).count(),
|
||||||
|
files.len(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.pieces() {
|
if !self.pieces() {
|
||||||
write!(f, "pieces corrupted")?;
|
errln!(env, "Pieces corrupted.")?;
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(f, "ok")?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::common::*;
|
||||||
|
|
||||||
pub(crate) trait Step {
|
pub(crate) trait Step {
|
||||||
fn print(&self, env: &mut Env) -> Result<(), Error> {
|
fn print(&self, env: &mut Env) -> Result<(), Error> {
|
||||||
let style = env.err_style();
|
let style = env.err().style();
|
||||||
let dim = style.dim();
|
let dim = style.dim();
|
||||||
let message = style.message();
|
let message = style.message();
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ pub(crate) trait Step {
|
||||||
|
|
||||||
err!(env, "{}{} ", message.prefix(), self.symbol())?;
|
err!(env, "{}{} ", message.prefix(), self.symbol())?;
|
||||||
|
|
||||||
self.write_message(&mut env.err).context(error::Stderr)?;
|
self.write_message(env.err_mut()).context(error::Stderr)?;
|
||||||
|
|
||||||
errln!(env, "{}", message.suffix())?;
|
errln!(env, "{}", message.suffix())?;
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,16 @@ pub(crate) struct Style {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Style {
|
impl Style {
|
||||||
|
pub(crate) fn from_active(active: bool) -> Self {
|
||||||
|
Self { active }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn active() -> Self {
|
pub(crate) fn active() -> Self {
|
||||||
Self { active: true }
|
Self { active: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn inactive() -> Self {
|
pub(crate) fn inactive() -> Self {
|
||||||
Self { active: false }
|
Self { active: false }
|
||||||
}
|
}
|
||||||
|
|
|
@ -208,7 +208,7 @@ impl Create {
|
||||||
|
|
||||||
CreateStep::Searching.print(env)?;
|
CreateStep::Searching.print(env)?;
|
||||||
|
|
||||||
let spinner = if env.err_is_term() {
|
let spinner = if env.err().is_styled() {
|
||||||
let style = ProgressStyle::default_spinner()
|
let style = ProgressStyle::default_spinner()
|
||||||
.template("{spinner:.green} {msg:.bold}…")
|
.template("{spinner:.green} {msg:.bold}…")
|
||||||
.tick_chars(consts::TICK_CHARS);
|
.tick_chars(consts::TICK_CHARS);
|
||||||
|
@ -300,7 +300,7 @@ impl Create {
|
||||||
|
|
||||||
CreateStep::Hashing.print(env)?;
|
CreateStep::Hashing.print(env)?;
|
||||||
|
|
||||||
let progress_bar = if env.err_is_term() {
|
let progress_bar = if env.err().is_styled() {
|
||||||
let style = ProgressStyle::default_bar()
|
let style = ProgressStyle::default_bar()
|
||||||
.template(
|
.template(
|
||||||
"{spinner:.green} ⟪{elapsed_precise}⟫ ⟦{bar:40.cyan}⟧ \
|
"{spinner:.green} ⟪{elapsed_precise}⟫ ⟦{bar:40.cyan}⟧ \
|
||||||
|
@ -368,7 +368,7 @@ impl Create {
|
||||||
.and_then(|mut file| file.write_all(&bytes))
|
.and_then(|mut file| file.write_all(&bytes))
|
||||||
.context(error::Filesystem { path })?;
|
.context(error::Filesystem { path })?;
|
||||||
}
|
}
|
||||||
OutputTarget::Stdout => env.out.write_all(&bytes).context(error::Stdout)?,
|
OutputTarget::Stdout => env.out_mut().write_all(&bytes).context(error::Stdout)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -45,7 +45,7 @@ impl Verify {
|
||||||
metainfo_path.parent().unwrap().join(&metainfo.info.name)
|
metainfo_path.parent().unwrap().join(&metainfo.info.name)
|
||||||
};
|
};
|
||||||
|
|
||||||
let progress_bar = if env.err_is_term() {
|
let progress_bar = if env.err().is_styled() {
|
||||||
let style = ProgressStyle::default_bar()
|
let style = ProgressStyle::default_bar()
|
||||||
.template(
|
.template(
|
||||||
"{spinner:.green} ⟪{elapsed_precise}⟫ ⟦{bar:40.cyan}⟧ \
|
"{spinner:.green} ⟪{elapsed_precise}⟫ ⟦{bar:40.cyan}⟧ \
|
||||||
|
|
|
@ -58,21 +58,16 @@ impl TestEnvBuilder {
|
||||||
tempdir.path().to_owned()
|
tempdir.path().to_owned()
|
||||||
};
|
};
|
||||||
|
|
||||||
let env = Env::new(
|
let out_stream = OutputStream::new(
|
||||||
current_dir,
|
Box::new(out.clone()),
|
||||||
out.clone(),
|
self.use_color && self.out_is_term,
|
||||||
if self.use_color && self.out_is_term {
|
|
||||||
Style::active()
|
|
||||||
} else {
|
|
||||||
Style::inactive()
|
|
||||||
},
|
|
||||||
self.out_is_term,
|
self.out_is_term,
|
||||||
err.clone(),
|
|
||||||
Style::inactive(),
|
|
||||||
false,
|
|
||||||
self.args,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let err_stream = OutputStream::new(Box::new(err.clone()), false, false);
|
||||||
|
|
||||||
|
let env = Env::new(current_dir, self.args, out_stream, err_stream);
|
||||||
|
|
||||||
TestEnv::new(tempdir, env, err, out)
|
TestEnv::new(tempdir, env, err, out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,14 +46,14 @@ impl TorrentSummary {
|
||||||
pub(crate) fn write(&self, env: &mut Env) -> Result<(), Error> {
|
pub(crate) fn write(&self, env: &mut Env) -> Result<(), Error> {
|
||||||
let table = self.table();
|
let table = self.table();
|
||||||
|
|
||||||
if env.out_is_term() {
|
if env.out().is_term() {
|
||||||
let out_style = env.out_style();
|
let style = env.out().style();
|
||||||
table
|
table
|
||||||
.write_human_readable(&mut env.out, out_style)
|
.write_human_readable(env.out_mut(), style)
|
||||||
.context(error::Stdout)?;
|
.context(error::Stdout)?;
|
||||||
} else {
|
} else {
|
||||||
table
|
table
|
||||||
.write_tab_delimited(&mut env.out)
|
.write_tab_delimited(env.out_mut())
|
||||||
.context(error::Stdout)?;
|
.context(error::Stdout)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,22 +40,34 @@ impl<'a> Verifier<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_metainfo(mut self) -> Result<Status> {
|
fn verify_metainfo(mut self) -> Result<Status> {
|
||||||
let mut status = Vec::new();
|
match &self.metainfo.info.mode {
|
||||||
|
Mode::Single { length, md5sum } => {
|
||||||
|
self.hash(&self.base).ok();
|
||||||
|
let error = FileError::verify(&self.base, *length, *md5sum).err();
|
||||||
|
|
||||||
for (path, len, md5sum) in self.metainfo.files(&self.base) {
|
let pieces = self.finish();
|
||||||
status.push(FileStatus::status(&path, len, md5sum));
|
Ok(Status::single(pieces, error))
|
||||||
self.hash(&path).ok();
|
}
|
||||||
|
Mode::Multiple { files } => {
|
||||||
|
let mut status = Vec::new();
|
||||||
|
|
||||||
|
for file in files {
|
||||||
|
let path = file.path.absolute(self.base);
|
||||||
|
self.hash(&path).ok();
|
||||||
|
|
||||||
|
status.push(FileStatus::status(
|
||||||
|
&path,
|
||||||
|
file.path.clone(),
|
||||||
|
file.length,
|
||||||
|
file.md5sum,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let pieces = self.finish();
|
||||||
|
|
||||||
|
Ok(Status::multiple(pieces, status))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.piece_bytes_hashed > 0 {
|
|
||||||
self.pieces.push(self.sha1.digest().into());
|
|
||||||
self.sha1.reset();
|
|
||||||
self.piece_bytes_hashed = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pieces = self.pieces == self.metainfo.info.pieces;
|
|
||||||
|
|
||||||
Ok(Status::new(pieces, status))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn hash(&mut self, path: &Path) -> io::Result<()> {
|
pub(crate) fn hash(&mut self, path: &Path) -> io::Result<()> {
|
||||||
|
@ -94,6 +106,16 @@ impl<'a> Verifier<'a> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn finish(&mut self) -> bool {
|
||||||
|
if self.piece_bytes_hashed > 0 {
|
||||||
|
self.pieces.push(self.sha1.digest().into());
|
||||||
|
self.sha1.reset();
|
||||||
|
self.piece_bytes_hashed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.pieces == self.metainfo.info.pieces
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -157,7 +179,7 @@ mod tests {
|
||||||
|
|
||||||
let status = metainfo.verify(&env.resolve("foo"), None)?;
|
let status = metainfo.verify(&env.resolve("foo"), None)?;
|
||||||
|
|
||||||
assert!(status.files().iter().all(FileStatus::good));
|
assert_eq!(status.count_bad(), 0);
|
||||||
|
|
||||||
assert!(!status.pieces());
|
assert!(!status.pieces());
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user