Print individual file torrent verification errors

If torrent verification fails, print all errors with individual files.

type: changed
This commit is contained in:
Casey Rodarmor 2020-03-15 03:22:33 -07:00
parent f8e3fd594b
commit 1532113782
No known key found for this signature in database
GPG Key ID: 556186B153EC6FE0
22 changed files with 401 additions and 225 deletions

View File

@ -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;

View File

@ -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

View File

@ -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
} }
} }

View File

@ -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)
}; };
} }

View File

@ -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)
}; };
} }

View File

@ -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
View 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(),
)
}
}

View File

@ -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()
} }
} }

View File

@ -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;

View File

@ -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::*;

View File

@ -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)
} }

View File

@ -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
View 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
View 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(())
}
}

View File

@ -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(())
} }
} }

View File

@ -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())?;

View File

@ -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 }
} }

View File

@ -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)]

View File

@ -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}⟧ \

View File

@ -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)
} }
} }

View File

@ -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)?;
} }

View File

@ -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());