use bytes::Bytes;
use serde::de::{self, Deserializer};
use serde::{Deserialize, Serialize};
use crate::error::Error;
use crate::hash::Id20;
fn deserialize_similar<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<Id20>, D::Error> {
struct SimilarVisitor;
impl<'de> de::Visitor<'de> for SimilarVisitor {
type Value = Vec<Id20>;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a list of 20-byte binary strings")
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<Id20>, A::Error> {
let mut hashes = Vec::new();
while let Some(bytes) = seq.next_element::<serde_bytes::ByteBuf>()? {
if let Ok(id) = Id20::from_bytes(bytes.as_ref()) {
hashes.push(id);
}
}
Ok(hashes)
}
}
deserializer.deserialize_seq(SimilarVisitor)
}
#[derive(Debug, Clone, Default)]
pub struct UrlList(pub Vec<String>);
impl<'de> Deserialize<'de> for UrlList {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct UrlListVisitor;
impl<'de> de::Visitor<'de> for UrlListVisitor {
type Value = UrlList;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a string or list of strings")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<UrlList, E> {
Ok(UrlList(vec![v.to_owned()]))
}
fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<UrlList, E> {
let s = std::str::from_utf8(v).map_err(de::Error::custom)?;
Ok(UrlList(vec![s.to_owned()]))
}
fn visit_seq<A: de::SeqAccess<'de>>(self, mut seq: A) -> Result<UrlList, A::Error> {
let mut urls = Vec::new();
while let Some(url) = seq.next_element::<String>()? {
urls.push(url);
}
Ok(UrlList(urls))
}
}
deserializer.deserialize_any(UrlListVisitor)
}
}
#[derive(Debug, Clone)]
pub struct TorrentMetaV1 {
pub info_hash: Id20,
pub announce: Option<String>,
pub announce_list: Option<Vec<Vec<String>>>,
pub comment: Option<String>,
pub created_by: Option<String>,
pub creation_date: Option<i64>,
pub info: InfoDict,
pub url_list: Vec<String>,
pub httpseeds: Vec<String>,
pub info_bytes: Option<Bytes>,
pub ssl_cert: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct InfoDict {
pub name: String,
#[serde(rename = "piece length")]
pub piece_length: u64,
#[serde(with = "serde_bytes")]
pub pieces: Vec<u8>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub length: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub files: Option<Vec<FileEntry>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub private: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub source: Option<String>,
#[serde(rename = "ssl-cert", skip_serializing_if = "Option::is_none", default)]
#[serde(with = "serde_bytes")]
pub ssl_cert: Option<Vec<u8>>,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
deserialize_with = "deserialize_similar"
)]
pub similar: Vec<Id20>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub collections: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FileEntry {
pub length: u64,
pub path: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub attr: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub mtime: Option<i64>,
#[serde(
rename = "symlink path",
skip_serializing_if = "Option::is_none",
default
)]
pub symlink_path: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileInfo {
pub path: Vec<String>,
pub length: u64,
}
#[derive(Deserialize)]
struct RawTorrent {
announce: Option<String>,
#[serde(rename = "announce-list")]
announce_list: Option<Vec<Vec<String>>>,
comment: Option<String>,
#[serde(rename = "created by")]
created_by: Option<String>,
#[serde(rename = "creation date")]
creation_date: Option<i64>,
info: InfoDict,
#[serde(rename = "url-list", default)]
url_list: UrlList,
#[serde(default)]
httpseeds: Vec<String>,
}
pub fn torrent_from_bytes(data: &[u8]) -> Result<TorrentMetaV1, Error> {
let info_span = irontide_bencode::find_dict_key_span(data, "info")?;
let info_hash = crate::sha1(&data[info_span.clone()]);
let info_raw = Bytes::copy_from_slice(&data[info_span]);
let raw: RawTorrent = irontide_bencode::from_bytes(data)?;
validate_info(&raw.info)?;
let ssl_cert = raw.info.ssl_cert.clone();
Ok(TorrentMetaV1 {
info_hash,
announce: raw.announce,
announce_list: raw.announce_list,
comment: raw.comment,
created_by: raw.created_by,
creation_date: raw.creation_date,
info: raw.info,
url_list: raw.url_list.0,
httpseeds: raw.httpseeds,
info_bytes: Some(info_raw),
ssl_cert,
})
}
fn validate_info(info: &InfoDict) -> Result<(), Error> {
if info.piece_length == 0 {
return Err(Error::InvalidTorrent("piece length is 0".into()));
}
if !info.pieces.len().is_multiple_of(20) {
return Err(Error::InvalidTorrent(format!(
"pieces length {} is not a multiple of 20",
info.pieces.len()
)));
}
if info.length.is_none() && info.files.is_none() {
return Err(Error::InvalidTorrent(
"neither 'length' nor 'files' present".into(),
));
}
if info.length.is_some() && info.files.is_some() {
return Err(Error::InvalidTorrent(
"both 'length' and 'files' present".into(),
));
}
Ok(())
}
impl InfoDict {
#[must_use]
pub fn total_length(&self) -> u64 {
if let Some(length) = self.length {
length
} else if let Some(ref files) = self.files {
files.iter().map(|f| f.length).sum()
} else {
0
}
}
#[must_use]
pub fn num_pieces(&self) -> usize {
self.pieces.len() / 20
}
#[must_use]
pub fn piece_hash(&self, index: usize) -> Option<Id20> {
let start = index * 20;
if start + 20 > self.pieces.len() {
return None;
}
let mut hash = [0u8; 20];
hash.copy_from_slice(&self.pieces[start..start + 20]);
Some(Id20(hash))
}
#[must_use]
pub fn files(&self) -> Vec<FileInfo> {
if let Some(length) = self.length {
vec![FileInfo {
path: vec![self.name.clone()],
length,
}]
} else if let Some(ref files) = self.files {
files
.iter()
.map(|f| {
let mut path = vec![self.name.clone()];
path.extend(f.path.clone());
FileInfo {
path,
length: f.length,
}
})
.collect()
} else {
vec![]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_torrent_bytes_sorted(before_info: &[u8], after_info: &[u8]) -> Vec<u8> {
let info = b"d6:lengthi1048576e4:name4:test12:piece lengthi262144e6:pieces20:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00e";
let mut buf = Vec::new();
buf.push(b'd');
buf.extend_from_slice(before_info);
buf.extend_from_slice(b"4:info");
buf.extend_from_slice(info);
buf.extend_from_slice(after_info);
buf.push(b'e');
buf
}
#[test]
fn url_list_single_string() {
let data = make_torrent_bytes_sorted(b"", b"8:url-list24:http://example.com/files");
let meta = torrent_from_bytes(&data).unwrap();
assert_eq!(meta.url_list, vec!["http://example.com/files"]);
}
#[test]
fn url_list_multiple() {
let data = make_torrent_bytes_sorted(
b"",
b"8:url-listl24:http://example.com/files26:http://mirror.example.com/e",
);
let meta = torrent_from_bytes(&data).unwrap();
assert_eq!(meta.url_list.len(), 2);
assert_eq!(meta.url_list[0], "http://example.com/files");
assert_eq!(meta.url_list[1], "http://mirror.example.com/");
}
#[test]
fn url_list_absent() {
let data = make_torrent_bytes_sorted(b"", b"");
let meta = torrent_from_bytes(&data).unwrap();
assert!(meta.url_list.is_empty());
}
#[test]
fn httpseeds_present() {
let data = make_torrent_bytes_sorted(b"9:httpseedsl28:http://seed.example.com/seede", b"");
let meta = torrent_from_bytes(&data).unwrap();
assert_eq!(meta.httpseeds, vec!["http://seed.example.com/seed"]);
}
#[test]
fn httpseeds_absent() {
let data = make_torrent_bytes_sorted(b"", b"");
let meta = torrent_from_bytes(&data).unwrap();
assert!(meta.httpseeds.is_empty());
}
#[test]
fn torrent_from_bytes_stores_raw_info_bytes() {
let data = make_torrent_bytes_sorted(b"", b"");
let meta = torrent_from_bytes(&data).unwrap();
assert!(meta.info_bytes.is_some());
let info_bytes = meta.info_bytes.unwrap();
let rehash = crate::sha1(&info_bytes);
assert_eq!(rehash, meta.info_hash);
}
#[test]
fn ssl_cert_parsed_from_info_dict() {
let cert_pem = b"-----BEGIN CERTIFICATE-----\nMIIBtest\n-----END CERTIFICATE-----\n";
let cert_len = cert_pem.len();
let mut info = Vec::new();
info.extend_from_slice(b"d");
info.extend_from_slice(b"6:lengthi1048576e");
info.extend_from_slice(b"4:name4:test");
info.extend_from_slice(b"12:piece lengthi262144e");
info.extend_from_slice(b"6:pieces20:");
info.extend_from_slice(&[0u8; 20]);
info.extend_from_slice(format!("8:ssl-cert{cert_len}:").as_bytes());
info.extend_from_slice(cert_pem);
info.extend_from_slice(b"e");
let mut torrent = Vec::new();
torrent.extend_from_slice(b"d4:info");
torrent.extend_from_slice(&info);
torrent.extend_from_slice(b"e");
let meta = torrent_from_bytes(&torrent).unwrap();
assert!(meta.ssl_cert.is_some());
assert_eq!(meta.ssl_cert.as_deref().unwrap(), cert_pem);
assert_eq!(meta.info.ssl_cert.as_deref().unwrap(), cert_pem);
}
#[test]
fn ssl_cert_absent_by_default() {
let data = make_torrent_bytes_sorted(b"", b"");
let meta = torrent_from_bytes(&data).unwrap();
assert!(meta.ssl_cert.is_none());
assert!(meta.info.ssl_cert.is_none());
}
fn make_torrent_with_bep38(similar: Option<&[u8]>, collections: Option<&[u8]>) -> Vec<u8> {
let mut info = Vec::new();
info.extend_from_slice(b"d");
if let Some(c) = collections {
info.extend_from_slice(b"11:collections");
info.extend_from_slice(c);
}
info.extend_from_slice(b"6:lengthi1048576e");
info.extend_from_slice(b"4:name4:test");
info.extend_from_slice(b"12:piece lengthi262144e");
info.extend_from_slice(b"6:pieces20:");
info.extend_from_slice(&[0u8; 20]);
if let Some(s) = similar {
info.extend_from_slice(b"7:similar");
info.extend_from_slice(s);
}
info.extend_from_slice(b"e");
let mut torrent = Vec::new();
torrent.extend_from_slice(b"d4:info");
torrent.extend_from_slice(&info);
torrent.extend_from_slice(b"e");
torrent
}
#[test]
fn parse_similar_torrents_from_info() {
let hash_a = [0xAAu8; 20];
let hash_b = [0xBBu8; 20];
let mut similar_list = Vec::new();
similar_list.extend_from_slice(b"l");
similar_list.extend_from_slice(b"20:");
similar_list.extend_from_slice(&hash_a);
similar_list.extend_from_slice(b"20:");
similar_list.extend_from_slice(&hash_b);
similar_list.extend_from_slice(b"e");
let data = make_torrent_with_bep38(Some(&similar_list), None);
let meta = torrent_from_bytes(&data).expect("parse should succeed");
assert_eq!(meta.info.similar.len(), 2);
assert_eq!(meta.info.similar[0], Id20(hash_a));
assert_eq!(meta.info.similar[1], Id20(hash_b));
}
#[test]
fn parse_collections_from_info() {
let collections_list = b"l6:movies6:sci-fie";
let data = make_torrent_with_bep38(None, Some(collections_list));
let meta = torrent_from_bytes(&data).expect("parse should succeed");
assert_eq!(meta.info.collections.len(), 2);
assert_eq!(meta.info.collections[0], "movies");
assert_eq!(meta.info.collections[1], "sci-fi");
}
#[test]
fn similar_empty_when_absent() {
let data = make_torrent_bytes_sorted(b"", b"");
let meta = torrent_from_bytes(&data).expect("parse should succeed");
assert!(meta.info.similar.is_empty());
assert!(meta.info.collections.is_empty());
}
#[test]
fn similar_ignores_wrong_length_hashes() {
let valid_hash = [0xCCu8; 20];
let too_short = [0xDDu8; 19];
let too_long = [0xEEu8; 21];
let mut similar_list = Vec::new();
similar_list.extend_from_slice(b"l");
similar_list.extend_from_slice(b"19:");
similar_list.extend_from_slice(&too_short);
similar_list.extend_from_slice(b"20:");
similar_list.extend_from_slice(&valid_hash);
similar_list.extend_from_slice(b"21:");
similar_list.extend_from_slice(&too_long);
similar_list.extend_from_slice(b"e");
let data = make_torrent_with_bep38(Some(&similar_list), None);
let meta = torrent_from_bytes(&data).expect("parse should succeed");
assert_eq!(meta.info.similar.len(), 1);
assert_eq!(meta.info.similar[0], Id20(valid_hash));
}
}