mod parse;
pub use crate::bencode::Bytes;
use sha1::{Digest, Sha1};
use crate::bencode::{self, Bencode};
use crate::error::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Metainfo {
pub announce: String,
pub announce_list: Vec<Vec<String>>,
pub info: Info,
pub creation_date: Option<i64>,
pub comment: Option<String>,
pub created_by: Option<String>,
pub encoding: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RawInfo {
Bytes(Bytes),
Hash([u8; 20]),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Info {
pub piece_length: u64,
pub pieces: Vec<[u8; 20]>,
pub mode: Mode,
pub raw_info: RawInfo,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Mode {
Single {
name: String,
length: u64,
},
Multiple {
name: String,
files: Vec<FileInfo>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FileInfo {
pub length: u64,
pub path: Vec<String>,
}
impl Metainfo {
pub fn info_hash(&self) -> [u8; 20] {
match &self.info.raw_info {
RawInfo::Hash(h) => *h,
RawInfo::Bytes(raw) => {
let mut hasher = Sha1::new();
hasher.update(raw);
hasher.finalize().into()
}
}
}
pub fn to_bytes(&self) -> Option<Vec<u8>> {
let raw_info = match &self.info.raw_info {
RawInfo::Bytes(raw) => raw.clone(),
RawInfo::Hash(_) => return None,
};
let mut entries: Vec<(Bytes, Bencode)> = Vec::new();
entries.push((
Bytes::from("announce"),
Bencode::Bytes(Bytes::copy_from_slice(self.announce.as_bytes())),
));
if !self.announce_list.is_empty() {
let tiers: Vec<Bencode> = self
.announce_list
.iter()
.map(|tier| {
Bencode::List(
tier.iter()
.map(|url| Bencode::Bytes(Bytes::copy_from_slice(url.as_bytes())))
.collect(),
)
})
.collect();
entries.push((Bytes::from("announce-list"), Bencode::List(tiers)));
}
if let Some(date) = self.creation_date {
entries.push((Bytes::from("creation date"), Bencode::Integer(date)));
}
if let Some(ref c) = self.comment {
entries.push((
Bytes::from("comment"),
Bencode::Bytes(Bytes::copy_from_slice(c.as_bytes())),
));
}
if let Some(ref cb) = self.created_by {
entries.push((
Bytes::from("created by"),
Bencode::Bytes(Bytes::copy_from_slice(cb.as_bytes())),
));
}
if let Some(ref enc) = self.encoding {
entries.push((
Bytes::from("encoding"),
Bencode::Bytes(Bytes::copy_from_slice(enc.as_bytes())),
));
}
let mut out: Vec<u8> = Vec::new();
out.push(b'd');
entries.sort_by(|(a, _), (b, _)| a.cmp(b));
for (key, val) in entries {
out.extend_from_slice(&bencode::encode(&Bencode::Bytes(key)));
out.extend_from_slice(&bencode::encode(&val));
}
let info_key = bencode::encode(&Bencode::Bytes(Bytes::from("info")));
out.extend_from_slice(&info_key);
out.extend_from_slice(&raw_info);
out.push(b'e');
Some(out)
}
}
impl Info {
pub fn set_raw_bytes(&mut self, raw: Bytes) {
self.raw_info = RawInfo::Bytes(raw);
}
pub fn total_size(&self) -> u64 {
match &self.mode {
Mode::Single { length, .. } => *length,
Mode::Multiple { files, .. } => files.iter().map(|f| f.length).sum(),
}
}
pub fn num_pieces(&self) -> usize {
self.pieces.len()
}
}
impl TryFrom<&[u8]> for Metainfo {
type Error = Error;
fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
self::parse::from_bytes(data)
}
}
impl<const N: usize> TryFrom<&[u8; N]> for Metainfo {
type Error = Error;
fn try_from(data: &[u8; N]) -> Result<Self, Self::Error> {
self::parse::from_bytes(data)
}
}
impl TryFrom<&Vec<u8>> for Metainfo {
type Error = Error;
fn try_from(data: &Vec<u8>) -> Result<Self, Self::Error> {
self::parse::from_bytes(data.as_slice())
}
}
impl TryFrom<Vec<u8>> for Metainfo {
type Error = Error;
fn try_from(data: Vec<u8>) -> Result<Self, Self::Error> {
self::parse::from_bytes(&data)
}
}
pub fn from_bytes(data: &[u8]) -> Result<Metainfo, Error> {
data.try_into()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_single() -> (Metainfo, Vec<u8>) {
let info = Bencode::Dict(vec![
(Bytes::from("name"), Bencode::Bytes(Bytes::from("f.txt"))),
(Bytes::from("piece length"), Bencode::Integer(16)),
(Bytes::from("length"), Bencode::Integer(32)),
(
Bytes::from("pieces"),
Bencode::Bytes(Bytes::from(vec![0u8; 20])),
),
]);
let root = Bencode::Dict(vec![
(
Bytes::from("announce"),
Bencode::Bytes(Bytes::from("http://t.com/a")),
),
(Bytes::from("info"), info),
]);
let data = bencode::encode(&root);
let meta = Metainfo::try_from(&data).unwrap();
(meta, data)
}
#[test]
fn to_bytes_roundtrip() {
let (meta, original) = make_single();
let re_encoded = meta.to_bytes().expect("should have raw bytes");
assert_eq!(re_encoded, original);
}
#[test]
fn to_bytes_magnet_stub_returns_none() {
let meta = Metainfo {
announce: String::new(),
announce_list: vec![],
info: Info {
piece_length: 0,
pieces: vec![],
mode: Mode::Single {
name: "stub".into(),
length: 0,
},
raw_info: RawInfo::Hash([0u8; 20]),
},
creation_date: None,
comment: None,
created_by: None,
encoding: None,
};
assert!(meta.to_bytes().is_none());
}
#[test]
fn set_raw_bytes_then_to_bytes() {
let (meta, _) = make_single();
let mut stub = Metainfo {
announce: "http://t.com/a".into(),
announce_list: vec![],
info: Info {
piece_length: 16,
pieces: vec![[0u8; 20]],
mode: Mode::Single {
name: "f.txt".into(),
length: 32,
},
raw_info: RawInfo::Hash([0u8; 20]),
},
creation_date: None,
comment: None,
created_by: None,
encoding: None,
};
assert!(stub.to_bytes().is_none());
let raw = match &meta.info.raw_info {
RawInfo::Bytes(b) => b.clone(),
_ => unreachable!(),
};
stub.info.set_raw_bytes(raw);
let re_encoded = stub.to_bytes().expect("should have raw bytes now");
let re_parsed = Metainfo::try_from(&re_encoded).unwrap();
assert_eq!(re_parsed.info_hash(), stub.info_hash());
}
#[test]
fn to_bytes_preserves_optional_fields() {
let info = Bencode::Dict(vec![
(Bytes::from("name"), Bencode::Bytes(Bytes::from("x"))),
(Bytes::from("piece length"), Bencode::Integer(32)),
(Bytes::from("length"), Bencode::Integer(64)),
(
Bytes::from("pieces"),
Bencode::Bytes(Bytes::from(vec![0u8; 20])),
),
]);
let root = Bencode::Dict(vec![
(
Bytes::from("announce"),
Bencode::Bytes(Bytes::from("http://t.com/a")),
),
(Bytes::from("comment"), Bencode::Bytes(Bytes::from("test"))),
(
Bytes::from("created by"),
Bencode::Bytes(Bytes::from("tool")),
),
(Bytes::from("creation date"), Bencode::Integer(1000)),
(
Bytes::from("encoding"),
Bencode::Bytes(Bytes::from("UTF-8")),
),
(Bytes::from("info"), info),
]);
let data = bencode::encode(&root);
let meta = Metainfo::try_from(&data).unwrap();
assert_eq!(meta.comment.as_deref(), Some("test"));
assert_eq!(meta.created_by.as_deref(), Some("tool"));
assert_eq!(meta.creation_date, Some(1000));
assert_eq!(meta.encoding.as_deref(), Some("UTF-8"));
let re_encoded = meta.to_bytes().unwrap();
let re_parsed = Metainfo::try_from(&re_encoded).unwrap();
assert_eq!(re_parsed.comment.as_deref(), Some("test"));
assert_eq!(re_parsed.created_by.as_deref(), Some("tool"));
}
}
#[cfg(all(test, feature = "serde"))]
mod serde_tests {
use super::*;
#[test]
fn metainfo_roundtrip_single_file() {
let raw = RawInfo::Bytes(Bytes::from_static(b"d4:infod...e"));
let info = Info {
piece_length: 262144,
pieces: vec![[0x42u8; 20]],
mode: Mode::Single {
name: "test.txt".into(),
length: 1024,
},
raw_info: raw,
};
let meta = Metainfo {
announce: "http://tracker.example.com/announce".into(),
announce_list: vec![vec!["http://t2.com/ann".into()]],
info,
creation_date: Some(1672531200),
comment: Some("test torrent".into()),
created_by: Some("torrent-rs".into()),
encoding: Some("UTF-8".into()),
};
let json = serde_json::to_string(&meta).unwrap();
let back: Metainfo = serde_json::from_str(&json).unwrap();
assert_eq!(back.announce, meta.announce);
assert_eq!(back.announce_list, meta.announce_list);
assert_eq!(back.info.piece_length, meta.info.piece_length);
assert_eq!(back.info.pieces, meta.info.pieces);
assert_eq!(back.creation_date, meta.creation_date);
assert_eq!(back.comment.as_deref(), Some("test torrent"));
assert_eq!(back.created_by.as_deref(), Some("torrent-rs"));
assert_eq!(back.encoding.as_deref(), Some("UTF-8"));
}
#[test]
fn metainfo_roundtrip_multi_file() {
let info = Info {
piece_length: 65536,
pieces: vec![[0u8; 20]],
mode: Mode::Multiple {
name: "my_data".into(),
files: vec![
FileInfo {
length: 100,
path: vec!["dir".into(), "a.txt".into()],
},
FileInfo {
length: 200,
path: vec!["dir".into(), "b.txt".into()],
},
],
},
raw_info: RawInfo::Hash([0xAB; 20]),
};
let meta = Metainfo {
announce: "http://t.com/a".into(),
announce_list: vec![],
info,
creation_date: None,
comment: None,
created_by: None,
encoding: None,
};
let json = serde_json::to_string(&meta).unwrap();
let back: Metainfo = serde_json::from_str(&json).unwrap();
match &back.info.mode {
Mode::Multiple { name, files } => {
assert_eq!(name, "my_data");
assert_eq!(files.len(), 2);
assert_eq!(files[0].length, 100);
assert_eq!(files[0].path, vec!["dir", "a.txt"]);
assert_eq!(files[1].length, 200);
assert_eq!(files[1].path, vec!["dir", "b.txt"]);
}
_ => panic!("expected Multiple mode"),
}
}
#[test]
fn magnet_origin_roundtrip() {
let meta = Metainfo {
announce: String::new(),
announce_list: vec![],
info: Info {
piece_length: 0,
pieces: vec![],
mode: Mode::Single {
name: "magnet-origin".into(),
length: 0,
},
raw_info: RawInfo::Hash([0x11; 20]),
},
creation_date: None,
comment: None,
created_by: None,
encoding: None,
};
let json = serde_json::to_string(&meta).unwrap();
let back: Metainfo = serde_json::from_str(&json).unwrap();
assert_eq!(back.info_hash(), meta.info_hash());
assert_eq!(back.announce, "");
assert_eq!(back.announce_list.len(), 0);
}
}