use crate::magnet::MagnetUri;
use crate::metainfo::{Metainfo, Mode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TorrentSpec {
Metainfo(Metainfo),
Magnet(MagnetUri),
}
impl TorrentSpec {
pub fn info_hash(&self) -> [u8; 20] {
match self {
TorrentSpec::Metainfo(m) => m.info_hash(),
TorrentSpec::Magnet(m) => *m.primary_info_hash(),
}
}
pub fn name(&self) -> Option<&str> {
match self {
TorrentSpec::Metainfo(m) => match &m.info.mode {
Mode::Single { name, .. } | Mode::Multiple { name, .. } => Some(name),
},
TorrentSpec::Magnet(m) => m.display_name.as_deref(),
}
}
pub fn trackers(&self) -> Vec<&str> {
match self {
TorrentSpec::Metainfo(m) => std::iter::once(m.announce.as_str())
.chain(m.announce_list.iter().flatten().map(|s| s.as_str()))
.collect(),
TorrentSpec::Magnet(m) => m.trackers.iter().map(|s| s.as_str()).collect(),
}
}
}
impl From<Metainfo> for TorrentSpec {
fn from(meta: Metainfo) -> Self {
TorrentSpec::Metainfo(meta)
}
}
impl From<MagnetUri> for TorrentSpec {
fn from(uri: MagnetUri) -> Self {
TorrentSpec::Magnet(uri)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metainfo::{Bytes, FileInfo, Info, Metainfo, Mode, RawInfo};
fn make_meta() -> Metainfo {
Metainfo {
announce: "http://tracker.example.com/announce".into(),
announce_list: vec![vec!["http://t2.com/ann".into(), "http://t3.com/ann".into()]],
info: Info {
piece_length: 262144,
pieces: vec![[0u8; 20]],
mode: Mode::Single {
name: "test.txt".into(),
length: 1024,
},
raw_info: RawInfo::Bytes(Bytes::from_static(b"d4:infod...e")),
},
creation_date: None,
comment: None,
created_by: None,
encoding: None,
}
}
fn make_multi_meta() -> Metainfo {
Metainfo {
announce: "http://t.com/ann".into(),
announce_list: vec![],
info: Info {
piece_length: 16384,
pieces: vec![[0u8; 20], [0u8; 20]],
mode: Mode::Multiple {
name: "root_dir".into(),
files: vec![FileInfo {
length: 512,
path: vec!["a.txt".into()],
}],
},
raw_info: RawInfo::Bytes(Bytes::from_static(b"d4:infod...e")),
},
creation_date: None,
comment: None,
created_by: None,
encoding: None,
}
}
#[test]
fn from_metainfo_info_hash() {
let meta = make_meta();
let hash = meta.info_hash();
let spec = TorrentSpec::from(meta);
assert_eq!(spec.info_hash(), hash);
}
#[test]
fn from_magnet_info_hash() {
let uri = "magnet:?xt=urn:btih:0123456789abcdef0123456789abcdef01234567"
.parse::<MagnetUri>()
.unwrap();
let expected = *uri.primary_info_hash();
let spec = TorrentSpec::from(uri);
assert_eq!(spec.info_hash(), expected);
}
#[test]
fn name_from_metainfo_single() {
let spec = TorrentSpec::from(make_meta());
assert_eq!(spec.name(), Some("test.txt"));
}
#[test]
fn name_from_metainfo_multi() {
let spec = TorrentSpec::from(make_multi_meta());
assert_eq!(spec.name(), Some("root_dir"));
}
#[test]
fn name_from_magnet_with_dn() {
let uri = "magnet:?xt=urn:btih:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa&dn=Ubuntu+24.04"
.parse::<MagnetUri>()
.unwrap();
let spec = TorrentSpec::from(uri);
assert_eq!(spec.name(), Some("Ubuntu+24.04"));
}
#[test]
fn name_from_magnet_without_dn() {
let uri = "magnet:?xt=urn:btih:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
.parse::<MagnetUri>()
.unwrap();
let spec = TorrentSpec::from(uri);
assert_eq!(spec.name(), None);
}
#[test]
fn trackers_from_metainfo() {
let spec = TorrentSpec::from(make_meta());
let trackers = spec.trackers();
assert_eq!(trackers.len(), 3);
assert!(trackers.contains(&"http://tracker.example.com/announce"));
assert!(trackers.contains(&"http://t2.com/ann"));
assert!(trackers.contains(&"http://t3.com/ann"));
}
#[test]
fn trackers_from_metainfo_no_list() {
let spec = TorrentSpec::from(make_multi_meta());
let trackers = spec.trackers();
assert_eq!(trackers.len(), 1);
assert_eq!(trackers[0], "http://t.com/ann");
}
#[test]
fn trackers_from_magnet() {
let uri = "magnet:?xt=urn:btih:cccccccccccccccccccccccccccccccccccccccc\
&tr=http://t1.com/ann&tr=http://t2.com/ann"
.parse::<MagnetUri>()
.unwrap();
let spec = TorrentSpec::from(uri);
let trackers = spec.trackers();
assert_eq!(trackers.len(), 2);
assert_eq!(trackers[0], "http://t1.com/ann");
assert_eq!(trackers[1], "http://t2.com/ann");
}
#[test]
fn trackers_from_magnet_empty() {
let uri = "magnet:?xt=urn:btih:dddddddddddddddddddddddddddddddddddddddd"
.parse::<MagnetUri>()
.unwrap();
let spec = TorrentSpec::from(uri);
assert!(spec.trackers().is_empty());
}
}