use bencode::BencodeElem;
use crypto::digest::Digest;
use crypto::sha1::Sha1;
use error::*;
use itertools::Itertools;
use std::borrow::Cow;
use std::collections::HashMap;
use std::fmt;
use std::path::{Path, PathBuf};
mod build;
mod read;
mod write;
const PIECE_STRING_LENGTH: usize = 20;
pub type Dictionary = HashMap<String, BencodeElem>;
pub type AnnounceList = Vec<Vec<String>>;
pub type Piece = Vec<u8>;
pub type Integer = i64;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct File {
pub length: Integer,
pub path: PathBuf,
pub extra_fields: Option<Dictionary>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Torrent {
pub announce: Option<String>,
pub announce_list: Option<AnnounceList>,
pub length: Integer,
pub files: Option<Vec<File>>,
pub name: String,
pub piece_length: Integer,
pub pieces: Vec<Piece>,
pub extra_fields: Option<Dictionary>,
pub extra_info_fields: Option<Dictionary>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TorrentBuilder {
announce: Option<String>,
announce_list: Option<AnnounceList>,
name: Option<String>,
path: PathBuf,
piece_length: Integer,
extra_fields: Option<Dictionary>,
extra_info_fields: Option<Dictionary>,
is_private: bool,
}
impl File {
pub fn absolute_path<P>(&self, parent: P) -> Result<PathBuf>
where
P: AsRef<Path>,
{
let result = parent.as_ref().join(&self.path);
if result.is_absolute() {
Ok(result)
} else {
bail!(ErrorKind::InvalidArgument(Cow::Borrowed(
"Joined path is not absolute."
)))
}
}
}
impl Torrent {
pub fn construct_info(&self) -> BencodeElem {
let mut info: HashMap<String, BencodeElem> = HashMap::new();
if let Some(ref files) = self.files {
info.insert(
"files".to_owned(),
BencodeElem::List(
files
.clone()
.into_iter()
.map(|file| file.into_bencode_elem())
.collect(),
),
);
} else {
info.insert("length".to_owned(), BencodeElem::Integer(self.length));
}
info.insert("name".to_owned(), BencodeElem::String(self.name.clone()));
info.insert(
"piece length".to_owned(),
BencodeElem::Integer(self.piece_length),
);
info.insert(
"pieces".to_owned(),
BencodeElem::Bytes(self.pieces.clone().into_iter().flatten().collect()),
);
if let Some(ref extra_info_fields) = self.extra_info_fields {
info.extend(extra_info_fields.clone());
}
BencodeElem::Dictionary(info)
}
pub fn info_hash(&self) -> String {
let mut hasher = Sha1::new();
hasher.input(&self.construct_info().encode());
hasher.result_str()
}
pub fn magnet_link(&self) -> String {
if let Some(ref list) = self.announce_list {
format!(
"magnet:?xt=urn:btih:{}&dn={}{}",
self.info_hash(),
self.name,
list.iter().format_with("", |tier, f| f(&format_args!(
"{}",
tier.iter()
.format_with("", |url, f| f(&format_args!("&tr={}", url)))
))),
)
} else if let Some(ref announce) = self.announce {
format!(
"magnet:?xt=urn:btih:{}&dn={}&tr={}",
self.info_hash(),
self.name,
announce,
)
} else {
format!("magnet:?xt=urn:btih:{}&dn={}", self.info_hash(), self.name,)
}
}
pub fn is_private(&self) -> bool {
if let Some(ref dict) = self.extra_info_fields {
match dict.get("private") {
Some(&BencodeElem::Integer(val)) => val == 1,
Some(_) => false,
None => false,
}
} else {
false
}
}
}
impl fmt::Display for File {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(
f,
"{}\n\
-size: {} bytes",
self.path.as_path().display(),
self.length
)?;
if let Some(ref fields) = self.extra_fields {
write!(
f,
"{}",
fields
.iter()
.sorted_by_key(|&(key, _)| key.as_bytes())
.format_with("", |(k, v), f| f(&format_args!("-{}: {}\n", k, v)))
)?;
}
writeln!(f, "========================================")
}
}
impl fmt::Display for Torrent {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "{}.torrent", self.name)?;
if let Some(ref announce) = self.announce {
writeln!(f, "-announce: {}", announce)?;
}
if let Some(ref tiers) = self.announce_list {
writeln!(
f,
"-announce-list: [{}]",
tiers.iter().format_with(", ", |tier, f| f(&format_args!(
"[{}]",
::itertools::join(tier, ", ")
)))
)?;
}
writeln!(f, "-size: {} bytes", self.length)?;
writeln!(f, "-piece length: {} bytes", self.piece_length)?;
if let Some(ref fields) = self.extra_fields {
write!(
f,
"{}",
fields
.iter()
.sorted_by_key(|&(key, _)| key.as_bytes())
.format_with("", |(k, v), f| f(&format_args!("-{}: {}\n", k, v)))
)?;
}
if let Some(ref fields) = self.extra_info_fields {
write!(
f,
"{}",
fields
.iter()
.sorted_by_key(|&(key, _)| key.as_bytes())
.format_with("", |(k, v), f| f(&format_args!("-{}: {}\n", k, v)))
)?;
}
if let Some(ref files) = self.files {
writeln!(f, "-files:")?;
for (counter, file) in files.iter().enumerate() {
writeln!(f, "[{}] {}", counter + 1, file)?;
}
}
writeln!(
f,
"-pieces: [{}]",
self.pieces
.iter()
.format_with(", ", |piece, f| f(&format_args!(
"[{:02x}]",
piece.iter().format("")
))),
)
}
}
#[cfg(test)]
mod file_tests {
use super::*;
#[test]
fn absolute_path_ok() {
let file = File {
length: 42,
path: PathBuf::from("dir1/file"),
extra_fields: None,
};
assert_eq!(
file.absolute_path("/root").unwrap(),
PathBuf::from("/root/dir1/file")
);
}
#[test]
fn absolute_path_not_absolute() {
let file = File {
length: 42,
path: PathBuf::from("dir1/file"),
extra_fields: None,
};
match file.absolute_path("root") {
Err(Error(ErrorKind::InvalidArgument(m), _)) => {
assert_eq!(m, "Joined path is not absolute.");
}
_ => assert!(false),
}
}
}
#[cfg(test)]
mod torrent_tests {
use super::*;
use std::iter::FromIterator;
#[test]
fn construct_info_ok() {
let torrent = Torrent {
announce: Some("url".to_owned()),
announce_list: None,
length: 4,
files: None,
name: "sample".to_owned(),
piece_length: 2,
pieces: vec![vec![1, 2], vec![3, 4]],
extra_fields: None,
extra_info_fields: Some(HashMap::from_iter(
vec![("key".to_owned(), bencode_elem!("val"))].into_iter(),
)),
};
assert_eq!(
torrent.construct_info(),
bencode_elem!({
("length", 4),
("name", "sample"),
("piece length", 2),
("pieces", (1, 2, 3, 4)),
("key", "val"),
}),
);
}
#[test]
fn info_hash_ok() {
let torrent = Torrent {
announce: Some("url".to_owned()),
announce_list: None,
length: 4,
files: None,
name: "sample".to_owned(),
piece_length: 2,
pieces: vec![vec![1, 2], vec![3, 4]],
extra_fields: None,
extra_info_fields: None,
};
assert_eq!(
torrent.info_hash(),
"074f42efaf8267f137f114f722d4e7d1dcbfbda5".to_owned(),
);
}
#[test]
fn magnet_link_ok() {
let torrent = Torrent {
announce: Some("url".to_owned()),
announce_list: None,
length: 4,
files: None,
name: "sample".to_owned(),
piece_length: 2,
pieces: vec![vec![1, 2], vec![3, 4]],
extra_fields: None,
extra_info_fields: None,
};
assert_eq!(
torrent.magnet_link(),
"magnet:?xt=urn:btih:074f42efaf8267f137f114f722d4e7d1dcbfbda5\
&dn=sample&tr=url"
.to_owned()
);
}
#[test]
fn magnet_link_with_announce_list() {
let torrent = Torrent {
announce: Some("url".to_owned()),
announce_list: Some(vec![
vec!["url1".to_owned()],
vec!["url2".to_owned(), "url3".to_owned()],
]),
length: 4,
files: None,
name: "sample".to_owned(),
piece_length: 2,
pieces: vec![vec![1, 2], vec![3, 4]],
extra_fields: None,
extra_info_fields: None,
};
assert_eq!(
torrent.magnet_link(),
"magnet:?xt=urn:btih:074f42efaf8267f137f114f722d4e7d1dcbfbda5\
&dn=sample&tr=url1&tr=url2&tr=url3"
.to_owned()
);
}
#[test]
fn is_private_ok() {
let torrent = Torrent {
announce: Some("url".to_owned()),
announce_list: None,
length: 4,
files: None,
name: "sample".to_owned(),
piece_length: 2,
pieces: vec![vec![1, 2], vec![3, 4]],
extra_fields: None,
extra_info_fields: Some(HashMap::from_iter(
vec![("private".to_owned(), bencode_elem!(1))].into_iter(),
)),
};
assert!(torrent.is_private());
}
#[test]
fn is_private_no_extra_fields() {
let torrent = Torrent {
announce: Some("url".to_owned()),
announce_list: None,
length: 4,
files: None,
name: "sample".to_owned(),
piece_length: 2,
pieces: vec![vec![1, 2], vec![3, 4]],
extra_fields: None,
extra_info_fields: None,
};
assert!(!torrent.is_private());
}
#[test]
fn is_private_no_key() {
let torrent = Torrent {
announce: Some("url".to_owned()),
announce_list: None,
length: 4,
files: None,
name: "sample".to_owned(),
piece_length: 2,
pieces: vec![vec![1, 2], vec![3, 4]],
extra_fields: None,
extra_info_fields: Some(HashMap::from_iter(
vec![("".to_owned(), bencode_elem!(1))].into_iter(),
)),
};
assert!(!torrent.is_private());
}
#[test]
fn is_private_incorrect_val_type() {
let torrent = Torrent {
announce: Some("url".to_owned()),
announce_list: None,
length: 4,
files: None,
name: "sample".to_owned(),
piece_length: 2,
pieces: vec![vec![1, 2], vec![3, 4]],
extra_fields: None,
extra_info_fields: Some(HashMap::from_iter(
vec![("private".to_owned(), bencode_elem!("1"))].into_iter(),
)),
};
assert!(!torrent.is_private());
}
#[test]
fn is_private_incorrect_val() {
let torrent = Torrent {
announce: Some("url".to_owned()),
announce_list: None,
length: 4,
files: None,
name: "sample".to_owned(),
piece_length: 2,
pieces: vec![vec![1, 2], vec![3, 4]],
extra_fields: None,
extra_info_fields: Some(HashMap::from_iter(
vec![("private".to_owned(), bencode_elem!(2))].into_iter(),
)),
};
assert!(!torrent.is_private());
}
}
#[cfg(test)]
mod file_display_tests {
use super::*;
use std::iter::FromIterator;
#[test]
fn file_display_ok() {
let file = File {
length: 42,
path: PathBuf::from("dir1/file"),
extra_fields: None,
};
assert_eq!(
file.to_string(),
"dir1/file\n\
-size: 42 bytes\n\
========================================\n"
);
}
#[test]
fn file_display_with_extra_fields() {
let file = File {
length: 42,
path: PathBuf::from("dir1/file"),
extra_fields: Some(HashMap::from_iter(
vec![
("comment2".to_owned(), bencode_elem!("no comment")),
("comment1".to_owned(), bencode_elem!("no comment")),
]
.into_iter(),
)),
};
assert_eq!(
file.to_string(),
"dir1/file\n\
-size: 42 bytes\n\
-comment1: \"no comment\"\n\
-comment2: \"no comment\"\n\
========================================\n"
);
}
}
#[cfg(test)]
mod torrent_display_tests {
use super::*;
use std::iter::FromIterator;
#[test]
fn torrent_display_ok() {
let torrent = Torrent {
announce: Some("url".to_owned()),
announce_list: None,
length: 4,
files: None,
name: "sample".to_owned(),
piece_length: 2,
pieces: vec![vec![1, 2], vec![3, 4]],
extra_fields: None,
extra_info_fields: None,
};
assert_eq!(
torrent.to_string(),
"sample.torrent\n\
-announce: url\n\
-size: 4 bytes\n\
-piece length: 2 bytes\n\
-pieces: [[0102], [0304]]\n"
);
}
#[test]
fn torrent_display_with_announce_list() {
let torrent = Torrent {
announce: Some("url".to_owned()),
announce_list: Some(vec![
vec!["url1".to_owned(), "url2".to_owned()],
vec!["url3".to_owned(), "url4".to_owned()],
]),
length: 4,
files: None,
name: "sample".to_owned(),
piece_length: 2,
pieces: vec![vec![1, 2], vec![3, 4]],
extra_fields: None,
extra_info_fields: None,
};
assert_eq!(
torrent.to_string(),
"sample.torrent\n\
-announce: url\n\
-announce-list: [[url1, url2], [url3, url4]]\n\
-size: 4 bytes\n\
-piece length: 2 bytes\n\
-pieces: [[0102], [0304]]\n"
);
}
#[test]
fn torrent_display_with_extra_fields() {
let torrent = Torrent {
announce: Some("url".to_owned()),
announce_list: None,
length: 4,
files: None,
name: "sample".to_owned(),
piece_length: 2,
pieces: vec![vec![1, 2], vec![3, 4]],
extra_fields: Some(HashMap::from_iter(
vec![
("comment2".to_owned(), bencode_elem!("no comment")),
("comment1".to_owned(), bencode_elem!("no comment")),
]
.into_iter(),
)),
extra_info_fields: None,
};
assert_eq!(
torrent.to_string(),
"sample.torrent\n\
-announce: url\n\
-size: 4 bytes\n\
-piece length: 2 bytes\n\
-comment1: \"no comment\"\n\
-comment2: \"no comment\"\n\
-pieces: [[0102], [0304]]\n"
);
}
#[test]
fn torrent_display_with_extra_info_fields() {
let torrent = Torrent {
announce: Some("url".to_owned()),
announce_list: None,
length: 4,
files: None,
name: "sample".to_owned(),
piece_length: 2,
pieces: vec![vec![1, 2], vec![3, 4]],
extra_fields: None,
extra_info_fields: Some(HashMap::from_iter(
vec![
("comment2".to_owned(), bencode_elem!("no comment")),
("comment1".to_owned(), bencode_elem!("no comment")),
]
.into_iter(),
)),
};
assert_eq!(
torrent.to_string(),
"sample.torrent\n\
-announce: url\n\
-size: 4 bytes\n\
-piece length: 2 bytes\n\
-comment1: \"no comment\"\n\
-comment2: \"no comment\"\n\
-pieces: [[0102], [0304]]\n"
);
}
#[test]
fn torrent_display_with_multiple_files() {
let torrent = Torrent {
announce: Some("url".to_owned()),
announce_list: None,
length: 4,
files: Some(vec![
File {
length: 2,
path: PathBuf::from("dir1/dir2/file1"),
extra_fields: None,
},
File {
length: 2,
path: PathBuf::from("dir1/dir2/file2"),
extra_fields: None,
},
]),
name: "sample".to_owned(),
piece_length: 2,
pieces: vec![vec![1, 2], vec![3, 4]],
extra_fields: None,
extra_info_fields: None,
};
assert_eq!(
torrent.to_string(),
"sample.torrent\n\
-announce: url\n\
-size: 4 bytes\n\
-piece length: 2 bytes\n\
-files:\n\
[1] dir1/dir2/file1\n\
-size: 2 bytes\n\
========================================\n\
\n\
[2] dir1/dir2/file2\n\
-size: 2 bytes\n\
========================================\n\
\n\
-pieces: [[0102], [0304]]\n"
);
}
}