use lava_torrent::torrent::v1::Torrent;
use rogue_logging::Failure;
use sha1::{Digest, Sha1};
use std::fs::File;
use std::io::{self, ErrorKind, Read};
use std::path::{Path, PathBuf};
use tokio::task::spawn_blocking;
use crate::utils::SourceIssue;
use super::TorrentVerifyAction;
pub struct TorrentVerifier;
impl TorrentVerifier {
pub async fn execute(
torrent_file: &Path,
directory: &Path,
) -> Result<Option<SourceIssue>, Failure<TorrentVerifyAction>> {
let torrent_file = torrent_file.to_path_buf();
let directory = directory.to_path_buf();
spawn_blocking(move || verify(&torrent_file, &directory))
.await
.expect("torrent verify task should not panic")
}
}
fn verify(
torrent_file: &Path,
directory: &Path,
) -> Result<Option<SourceIssue>, Failure<TorrentVerifyAction>> {
let torrent = Torrent::read_from_file(torrent_file).map_err(Failure::wrap_with_path(
TorrentVerifyAction::ReadTorrent,
torrent_file,
))?;
let file_paths = get_file_paths(&torrent, directory);
let mut stream = match open_content_stream(&file_paths) {
Ok(stream) => stream,
Err(issue) => return Ok(Some(issue)),
};
let piece_length = u64::try_from(torrent.piece_length).expect("piece length should fit in u64");
let mut hasher = Sha1::new();
for (index, expected) in torrent.pieces.iter().enumerate() {
let mut piece = stream.by_ref().take(piece_length);
let bytes_copied = io::copy(&mut piece, &mut hasher)
.map_err(Failure::wrap(TorrentVerifyAction::HashContent))?;
if bytes_copied == 0 {
return Ok(Some(SourceIssue::HashCheck { piece_index: index }));
}
if hasher.finalize_reset().as_slice() != expected.as_slice() {
return Ok(Some(SourceIssue::HashCheck { piece_index: index }));
}
}
let mut extra = [0_u8; 1];
if stream
.read(&mut extra)
.map_err(Failure::wrap(TorrentVerifyAction::HashContent))?
> 0
{
return Ok(Some(SourceIssue::ExcessContent));
}
Ok(None)
}
fn open_content_stream(paths: &[PathBuf]) -> Result<Box<dyn Read>, SourceIssue> {
let mut stream: Box<dyn Read> = Box::new(io::empty());
for path in paths {
let file = match File::open(path) {
Ok(f) => f,
Err(e) if e.kind() == ErrorKind::NotFound => {
return Err(SourceIssue::MissingFile { path: path.clone() });
}
Err(e) => {
return Err(SourceIssue::OpenFile {
path: path.clone(),
error: e.to_string(),
});
}
};
stream = Box::new(stream.chain(file));
}
Ok(stream)
}
fn get_file_paths(torrent: &Torrent, directory: &Path) -> Vec<PathBuf> {
match &torrent.files {
Some(files) => files.iter().map(|f| directory.join(&f.path)).collect(),
None => vec![directory.join(&torrent.name)],
}
}