use crate::bcodec::bencoder::BEncoder;
use crate::bcodec::bvalue::BValue;
use crate::bcodec::raw_finder::RawFinder;
use crate::constants::{HASH_SIZE, PIECE_LENGTH};
use crate::hashmap;
use crate::Error;
use crate::{BDecoder, DeepFinder};
use sha1;
use std::collections::HashMap;
use std::convert::{TryFrom, TryInto};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(PartialEq, Clone, Debug)]
pub struct Metainfo {
announce: String,
name: String,
piece_length: u64,
pieces: Vec<[u8; HASH_SIZE]>,
files: Vec<File>,
info_hash: [u8; HASH_SIZE],
}
#[derive(PartialEq, Clone, Debug)]
pub struct File {
pub length: u64,
pub path: String,
}
pub struct PiecePos {
pub file_index: usize,
pub byte_index: usize,
}
impl Metainfo {
pub fn create_file(path: &Path, tracker_addr: &String) -> Result<(), Error> {
let metadata = match fs::metadata(path) {
Ok(metadata) => {
if metadata.is_dir() {
return Err(Error::FileNotFound);
}
metadata
}
Err(_) => return Err(Error::FileNotFound),
};
let name = match path.file_name() {
Some(name) => match name.to_str() {
Some(name) => name.as_bytes().to_vec(),
None => return Err(Error::FileNotFound),
},
None => return Err(Error::FileNotFound),
};
let data = match fs::read(path) {
Ok(data) => data,
Err(_) => return Err(Error::FileNotFound),
};
let pieces = data
.chunks(PIECE_LENGTH)
.flat_map(|chunk| {
let mut hasher = sha1::Sha1::new();
hasher.update(chunk);
hasher.digest().bytes().as_ref().to_vec()
})
.collect::<Vec<u8>>();
let info = hashmap![
b"name".to_vec() => BValue::ByteStr(name),
b"piece length".to_vec() => BValue::Int(PIECE_LENGTH as i64),
b"pieces".to_vec() => BValue::ByteStr(pieces),
b"length".to_vec() => BValue::Int(metadata.len() as i64)
];
let torrent = hashmap![
b"announce".to_vec() => BValue::ByteStr(tracker_addr.to_owned().into_bytes()),
b"info".to_vec() => BValue::Dict(info)
];
let torrent_file = match path.file_name() {
Some(file_name) => {
let mut torrent_file = file_name.to_os_string();
torrent_file.push(".torrent");
torrent_file
}
None => return Err(Error::FileNotFound),
};
match fs::write(torrent_file, BEncoder::new().add_dict(&torrent).encode()) {
Ok(()) => Ok(()),
Err(_) => Err(Error::FileCannotWrite),
}
}
pub fn from_file(path: &Path) -> Result<Metainfo, Error> {
match &fs::read(path) {
Ok(val) => Self::from_bencode(val),
Err(_) => Err(Error::MetaFileNotFound),
}
}
pub fn from_bencode(data: &[u8]) -> Result<Metainfo, Error> {
let bvalues = BDecoder::from_array(data)?;
if bvalues.is_empty() {
return Err(Error::MetaBEncodeMissing);
}
let mut err = Err(Error::MetaDataMissing);
for val in bvalues {
match val {
BValue::Dict(dict) => match Self::parse(data, &dict) {
Ok(torrent) => return Ok(torrent),
Err(e) => err = Err(e),
},
_ => (),
}
}
err
}
fn parse(data: &[u8], dict: &HashMap<Vec<u8>, BValue>) -> Result<Metainfo, Error> {
let length = Self::find_length(dict);
let multi_files = Self::find_files(dict);
if length.is_some() && multi_files.is_some() {
return Err(Error::MetaLenAndFilesConflict);
} else if length.is_none() && multi_files.is_none() {
return Err(Error::MetaLenOrFilesMissing);
}
let name = Self::find_name(dict)?;
let files = match length {
Some(length) => vec![File {
length,
path: name.clone(),
}],
None => match multi_files {
Some(multi_files) => multi_files,
None => vec![],
},
};
let metainfo = Metainfo {
announce: Self::find_announce(dict)?,
name,
piece_length: Self::find_piece_length(dict)?,
pieces: Self::find_pieces(dict)?,
files,
info_hash: Self::calculate_hash(data)?,
};
Ok(metainfo)
}
pub fn find_announce(dict: &HashMap<Vec<u8>, BValue>) -> Result<String, Error> {
match dict.get(&b"announce".to_vec()) {
Some(BValue::ByteStr(val)) => {
String::from_utf8(val.to_vec()).or(Err(Error::MetaInvalidUtf8("announce")))
}
_ => Err(Error::MetaIncorrectOrMissing("announce")),
}
}
pub fn find_name(dict: &HashMap<Vec<u8>, BValue>) -> Result<String, Error> {
match dict.get(&b"info".to_vec()) {
Some(BValue::Dict(info)) => match info.get(&b"name".to_vec()) {
Some(BValue::ByteStr(val)) => {
String::from_utf8(val.to_vec()).or(Err(Error::MetaInvalidUtf8("name")))
}
_ => Err(Error::MetaIncorrectOrMissing("name")),
},
_ => Err(Error::MetaIncorrectOrMissing("info".into())),
}
}
pub fn find_piece_length(dict: &HashMap<Vec<u8>, BValue>) -> Result<u64, Error> {
match dict.get(&b"info".to_vec()) {
Some(BValue::Dict(info)) => match info.get(&b"piece length".to_vec()) {
Some(BValue::Int(length)) => {
u64::try_from(*length).or(Err(Error::MetaInvalidU64("piece length")))
}
_ => Err(Error::MetaIncorrectOrMissing("piece length")),
},
_ => Err(Error::MetaIncorrectOrMissing("info".into())),
}
}
pub fn find_pieces(dict: &HashMap<Vec<u8>, BValue>) -> Result<Vec<[u8; HASH_SIZE]>, Error> {
match dict.get(&b"info".to_vec()) {
Some(BValue::Dict(info)) => match info.get(&b"pieces".to_vec()) {
Some(BValue::ByteStr(pieces)) => {
if pieces.len() % HASH_SIZE != 0 {
return Err(Error::MetaNotDivisible("pieces"));
}
Ok(pieces
.chunks(HASH_SIZE)
.map(|chunk| chunk.try_into().unwrap())
.collect())
}
_ => Err(Error::MetaIncorrectOrMissing("pieces")),
},
_ => Err(Error::MetaIncorrectOrMissing("info".into())),
}
}
pub fn find_length(dict: &HashMap<Vec<u8>, BValue>) -> Option<u64> {
match dict.get(&b"info".to_vec()) {
Some(BValue::Dict(info)) => match info.get(&b"length".to_vec()) {
Some(BValue::Int(length)) => u64::try_from(*length).ok(),
_ => None,
},
_ => None,
}
}
pub fn find_files(dict: &HashMap<Vec<u8>, BValue>) -> Option<Vec<File>> {
match dict.get(&b"info".to_vec()) {
Some(BValue::Dict(info)) => match info.get(&b"files".to_vec()) {
Some(BValue::List(list)) => Some(Self::file_list(list)),
_ => None,
},
_ => None,
}
}
fn file_list(list: &Vec<BValue>) -> Vec<File> {
list.iter()
.filter_map(|elem| match elem {
BValue::Dict(dict) => Some(dict),
_ => None,
})
.filter_map(
|dict| match (dict.get(&b"length".to_vec()), dict.get(&b"path".to_vec())) {
(Some(BValue::Int(length)), Some(BValue::ByteStr(path))) => {
Some((length, path))
}
_ => None,
},
)
.filter_map(|(length, path)| {
match (u64::try_from(*length), String::from_utf8(path.to_vec())) {
(Ok(l), Ok(p)) => Some(File { length: l, path: p }),
_ => None,
}
})
.collect()
}
fn calculate_hash(data: &[u8]) -> Result<[u8; HASH_SIZE], Error> {
if let Some(info) = DeepFinder::find_first("4:info", data) {
let mut hasher = sha1::Sha1::new();
hasher.update(info.as_ref());
return Ok(hasher.digest().bytes());
}
Err(Error::InfoMissing)
}
pub fn tracker_url(&self) -> &String {
&self.announce
}
pub fn piece(&self, piece_index: usize) -> &[u8; HASH_SIZE] {
&self.pieces[piece_index]
}
pub fn pieces_num(&self) -> usize {
self.pieces.len()
}
pub fn piece_length(&self, piece_index: usize) -> usize {
if piece_index < self.pieces.len() - 1 {
return self.piece_length as usize;
}
let last = self.total_length() as usize % self.piece_length as usize;
if last != 0 {
return last;
}
return self.piece_length as usize;
}
pub fn total_length(&self) -> u64 {
self.files.iter().map(|file| file.length).sum()
}
pub fn info_hash(&self) -> &[u8; HASH_SIZE] {
&self.info_hash
}
pub fn file_piece_ranges(&self) -> Vec<(PathBuf, PiecePos, PiecePos)> {
let dir = match self.files.len() > 1 {
true => PathBuf::from(&self.name),
false => PathBuf::new(),
};
let mut ranges: Vec<(PathBuf, PiecePos, PiecePos)> = vec![];
let mut pos: usize = 0;
for File { length, path } in self.files.iter() {
ranges.push((
dir.join(path),
self.piece_pos(pos),
self.piece_pos(pos + *length as usize),
));
pos += *length as usize;
}
ranges
}
fn piece_pos(&self, pos: usize) -> PiecePos {
PiecePos {
file_index: pos / self.piece_length as usize,
byte_index: pos % self.piece_length as usize,
}
}
}