use bt_bencode::Value as BencodeValue;
use rustc_hex::ToHex;
use serde::{Deserialize, Serialize};
use sha1::{Digest, Sha1};
use std::collections::HashMap;
use std::path::PathBuf;
use crate::{InfoHash, InfoHashError, PieceLength, TorrentContent, TorrentID};
#[derive(Clone, Debug, PartialEq)]
pub enum TorrentFileError {
NoNameFound,
InvalidBencode { reason: String },
NotATorrent { reason: String },
WrongVersion { version: u64 },
InvalidHash { source: InfoHashError },
InvalidContentPath { path: String },
MissingPieceLength,
BadPieceLength { piece_length: u32 },
}
impl std::fmt::Display for TorrentFileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TorrentFileError::NoNameFound => write!(f, "No name found"),
TorrentFileError::InvalidBencode { reason } => write!(f, "Invalid bencode: {reason}"),
TorrentFileError::NotATorrent { reason } => write!(
f,
"Valid bencode, but does not seem to be a torrent ({reason})"
),
TorrentFileError::WrongVersion { version } => write!(
f,
"Wrong torrent version: {version}, only v1 and v2 are supported)"
),
TorrentFileError::InvalidHash { source } => write!(f, "Invalid hash: {source}"),
TorrentFileError::InvalidContentPath { path } => {
write!(f, "Invalid content file path in torrent: {path}")
}
TorrentFileError::MissingPieceLength => {
write!(f, "No \'piece length\' field found in info dict")
}
TorrentFileError::BadPieceLength { piece_length } => {
write!(f, "Torrent \'piece length\' is too big: {}", piece_length)
}
}
}
}
impl From<InfoHashError> for TorrentFileError {
fn from(e: InfoHashError) -> TorrentFileError {
TorrentFileError::InvalidHash { source: e }
}
}
impl From<bt_bencode::Error> for TorrentFileError {
fn from(e: bt_bencode::Error) -> TorrentFileError {
TorrentFileError::InvalidBencode {
reason: e.to_string(),
}
}
}
impl std::error::Error for TorrentFileError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
TorrentFileError::InvalidHash { source } => Some(source),
_ => None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TorrentFile {
pub hash: InfoHash,
pub name: String,
pub decoded: DecodedTorrent,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DecodedTorrent {
info: DecodedInfo,
#[serde(flatten)]
#[serde(skip_serializing_if = "HashMap::is_empty")]
extra: HashMap<String, BencodeValue>,
}
impl DecodedTorrent {
pub fn files(&self) -> Result<Vec<TorrentContent>, TorrentFileError> {
if self.info.files.is_none() {
if self.info.file_tree.is_none() {
Ok(vec![TorrentContent {
path: PathBuf::from(&self.info.name),
size: self.info.length.unwrap(),
}])
} else {
todo!("v2 torrent files");
}
} else {
let mut files: Vec<TorrentContent> = vec![];
for file in self.info.files.as_ref().unwrap() {
let f: UnsafeV1FileContent = bt_bencode::from_value(file.clone()).unwrap();
if let Some(parsed_file) = f.to_torrent_content()? {
files.push(parsed_file);
}
}
files.sort();
Ok(files)
}
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
pub struct UnsafeV1FileContent {
#[serde(rename = "path")]
pub raw_paths: Vec<String>,
pub length: u64,
#[serde(default)]
pub attr: String,
}
impl UnsafeV1FileContent {
pub fn to_torrent_content(&self) -> Result<Option<TorrentContent>, TorrentFileError> {
if self.attr.contains('p') {
return Ok(None);
}
let mut path = PathBuf::new();
for p in &self.raw_paths {
if p.contains('/') {
return Err(TorrentFileError::InvalidContentPath {
path: p.to_string(),
});
}
if p == ".." {
return Err(TorrentFileError::InvalidContentPath {
path: p.to_string(),
});
}
if p == "." {
continue;
}
path.push(p);
}
Ok(Some(TorrentContent {
path,
size: self.length,
}))
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DecodedInfo {
#[serde(rename = "meta version")]
#[serde(skip_serializing_if = "Option::is_none")]
version: Option<u64>,
name: String,
#[serde(rename = "piece length")]
piece_length: PieceLength,
#[serde(skip_serializing_if = "Option::is_none")]
length: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
files: Option<Vec<BencodeValue>>,
#[serde(rename = "file tree")]
#[serde(skip_serializing_if = "Option::is_none")]
file_tree: Option<BencodeValue>,
#[serde(flatten)]
#[serde(skip_serializing_if = "HashMap::is_empty")]
extra: HashMap<String, BencodeValue>,
}
impl TorrentFile {
pub fn from_slice(s: &[u8]) -> Result<TorrentFile, TorrentFileError> {
let torrent: DecodedTorrent = bt_bencode::from_slice(s).map_err(|e| {
TorrentFileError::NotATorrent {
reason: e.to_string(),
}
})?;
let info_bytes = bt_bencode::to_vec(&torrent.info).unwrap();
let infohash = match torrent.info.version {
Some(1) | None => {
let digest = Sha1::digest(&info_bytes).to_vec().to_hex::<String>();
InfoHash::new(&digest)?
}
Some(2) => {
if torrent.info.file_tree.is_some() {
let digest = sha256::digest(info_bytes.as_slice());
let hash = InfoHash::new(&digest)?;
if torrent.info.length.is_some() || torrent.info.files.is_some() {
let digest = Sha1::digest(&info_bytes).to_vec().to_hex::<String>();
hash.hybrid(&InfoHash::new(&digest)?)?
} else {
hash
}
} else {
return Err(TorrentFileError::NotATorrent {
reason: "Torrentv2 without 'file_tree' field".to_string(),
});
}
}
_ => {
return Err(TorrentFileError::WrongVersion {
version: torrent.info.version.unwrap(),
});
}
};
Ok(TorrentFile {
name: torrent.info.name.clone(),
hash: infohash,
decoded: torrent,
})
}
pub fn hash(&self) -> &str {
self.hash.as_str()
}
pub fn name(&self) -> &str {
&self.name
}
pub fn id(&self) -> TorrentID {
TorrentID::from_infohash(&self.hash)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn can_read_torrent_v1() {
let slice = std::fs::read("tests/bittorrent-v1-emma-goldman.torrent").unwrap();
let res = TorrentFile::from_slice(&slice);
println!("{:?}", res);
assert!(res.is_ok());
let torrent = res.unwrap();
assert_eq!(
&torrent.name,
"Goldman, Emma - Essential Works of Anarchism"
);
assert_eq!(
torrent.hash,
InfoHash::V1("c811b41641a09d192b8ed81b14064fff55d85ce3".to_string())
);
assert_eq!(torrent.decoded.files().unwrap().len(), 94);
}
#[test]
fn can_read_torrent_v1_multifile() {
let slice = std::fs::read("tests/libtorrent/good/sample.torrent").unwrap();
let res = TorrentFile::from_slice(&slice);
println!("{:?}", res);
assert!(res.is_ok());
let torrent = res.unwrap();
assert_eq!(&torrent.name, "sample");
assert_eq!(
torrent.hash,
InfoHash::V1("58d8d15a4eb3bd9afabc9cee2564f78192777edb".to_string())
);
assert_eq!(
torrent.decoded.files().unwrap(),
vec!(
TorrentContent {
path: PathBuf::from("text_file.txt"),
size: 20,
},
TorrentContent {
path: PathBuf::from("text_file2.txt"),
size: 25,
}
),
);
}
#[test]
fn can_read_torrent_v1_wrongpath() {
let slice = std::fs::read("tests/libtorrent/good/parent_path.torrent").unwrap();
let res = TorrentFile::from_slice(&slice);
println!("{:?}", res);
assert!(res.is_ok());
let torrent = res.unwrap();
assert_eq!(&torrent.name, "temp");
assert_eq!(
torrent.hash,
InfoHash::V1("9e1111f1ee4966f7d06d398f1d58e00ad150657a".to_string())
);
assert_eq!(
torrent.decoded.files().unwrap_err(),
TorrentFileError::InvalidContentPath {
path: "..".to_string()
},
);
}
#[test]
fn can_read_torrent_v1_singlepath() {
let slice = std::fs::read("tests/libtorrent/good/base.torrent").unwrap();
let res = TorrentFile::from_slice(&slice);
println!("{:?}", res);
assert!(res.is_ok());
let torrent = res.unwrap();
assert_eq!(&torrent.name, "temp");
assert_eq!(
torrent.hash,
InfoHash::V1("c0fda1edafdbdbb96443424e0b3899af7159d10e".to_string())
);
assert_eq!(
torrent.decoded.files().unwrap(),
vec!(TorrentContent {
path: PathBuf::from("temp"),
size: 425,
}),
);
}
#[test]
fn can_read_torrent_v2() {
let slice = std::fs::read("tests/bittorrent-v2-test.torrent").unwrap();
let res = TorrentFile::from_slice(&slice);
assert!(res.is_ok());
let torrent = res.unwrap();
assert_eq!(&torrent.name, "bittorrent-v2-test");
assert_eq!(
torrent.hash,
InfoHash::V2(
"caf1e1c30e81cb361b9ee167c4aa64228a7fa4fa9f6105232b28ad099f3a302e".to_string()
)
);
}
#[test]
fn can_read_torrent_hybrid() {
let slice = std::fs::read("tests/bittorrent-v2-hybrid-test.torrent").unwrap();
let res = TorrentFile::from_slice(&slice);
assert!(res.is_ok());
let torrent = res.unwrap();
assert_eq!(&torrent.name, "bittorrent-v1-v2-hybrid-test");
assert_eq!(
torrent.hash,
InfoHash::Hybrid((
"631a31dd0a46257d5078c0dee4e66e26f73e42ac".to_string(),
"d8dd32ac93357c368556af3ac1d95c9d76bd0dff6fa9833ecdac3d53134efabb".to_string()
))
);
}
#[test]
fn v1_piece_len() {
let slice = std::fs::read("tests/libtorrent/bad/negative_piece_len.torrent").unwrap();
let res = TorrentFile::from_slice(&slice);
assert!(res.is_err());
}
#[test]
fn v2_piece_len() {
let slice = std::fs::read("tests/libtorrent/bad/v2_piece_size.torrent").unwrap();
let res = TorrentFile::from_slice(&slice);
assert!(res.is_err());
}
}