use irontide_bencode::BencodeValue;
use crate::error::Error;
use crate::info_hashes::InfoHashes;
use crate::metainfo::TorrentMetaV1;
use crate::metainfo_v2::TorrentMetaV2;
use crate::torrent_version::TorrentVersion;
#[derive(Debug, Clone)]
pub enum TorrentMeta {
V1(TorrentMetaV1),
V2(TorrentMetaV2),
Hybrid(Box<TorrentMetaV1>, Box<TorrentMetaV2>),
}
impl TorrentMeta {
pub fn info_hashes(&self) -> InfoHashes {
match self {
TorrentMeta::V1(t) => InfoHashes::v1_only(t.info_hash),
TorrentMeta::V2(t) => t.info_hashes.clone(),
TorrentMeta::Hybrid(v1, v2) => InfoHashes::hybrid(
v1.info_hash,
v2.info_hashes.v2.expect("v2 torrent must have v2 hash"),
),
}
}
pub fn is_v1(&self) -> bool {
matches!(self, TorrentMeta::V1(_))
}
pub fn is_v2(&self) -> bool {
matches!(self, TorrentMeta::V2(_))
}
pub fn is_hybrid(&self) -> bool {
matches!(self, TorrentMeta::Hybrid(_, _))
}
pub fn version(&self) -> TorrentVersion {
match self {
TorrentMeta::V1(_) => TorrentVersion::V1Only,
TorrentMeta::V2(_) => TorrentVersion::V2Only,
TorrentMeta::Hybrid(_, _) => TorrentVersion::Hybrid,
}
}
pub fn as_v1(&self) -> Option<&TorrentMetaV1> {
match self {
TorrentMeta::V1(v1) => Some(v1),
TorrentMeta::Hybrid(v1, _) => Some(v1),
TorrentMeta::V2(_) => None,
}
}
pub fn as_v2(&self) -> Option<&TorrentMetaV2> {
match self {
TorrentMeta::V2(v2) => Some(v2),
TorrentMeta::Hybrid(_, v2) => Some(v2),
TorrentMeta::V1(_) => None,
}
}
pub fn best_v1_info_hash(&self) -> crate::hash::Id20 {
self.info_hashes().best_v1()
}
pub fn ssl_cert(&self) -> Option<&[u8]> {
match self {
TorrentMeta::V1(v1) => v1.ssl_cert.as_deref(),
TorrentMeta::V2(v2) => v2.ssl_cert.as_deref(),
TorrentMeta::Hybrid(v1, _) => v1.ssl_cert.as_deref(),
}
}
pub fn is_ssl(&self) -> bool {
self.ssl_cert().is_some()
}
}
impl From<TorrentMetaV1> for TorrentMeta {
fn from(meta: TorrentMetaV1) -> Self {
TorrentMeta::V1(meta)
}
}
impl From<TorrentMetaV2> for TorrentMeta {
fn from(meta: TorrentMetaV2) -> Self {
TorrentMeta::V2(meta)
}
}
enum DetectedVersion {
V1Only,
V2Only,
Hybrid,
}
pub fn torrent_from_bytes_any(data: &[u8]) -> Result<TorrentMeta, Error> {
match detect_version(data)? {
DetectedVersion::V1Only => Ok(TorrentMeta::V1(crate::metainfo::torrent_from_bytes(data)?)),
DetectedVersion::V2Only => Ok(TorrentMeta::V2(crate::metainfo_v2::torrent_v2_from_bytes(
data,
)?)),
DetectedVersion::Hybrid => {
let v1 = crate::metainfo::torrent_from_bytes(data)?;
let mut v2 = crate::metainfo_v2::torrent_v2_from_bytes(data)?;
v2.info_hashes.v1 = Some(v1.info_hash);
Ok(TorrentMeta::Hybrid(Box::new(v1), Box::new(v2)))
}
}
}
fn detect_version(data: &[u8]) -> Result<DetectedVersion, Error> {
let root: BencodeValue = irontide_bencode::from_bytes(data)?;
let root_dict = root
.as_dict()
.ok_or_else(|| Error::InvalidTorrent("torrent must be a dict".into()))?;
let info = root_dict
.get(b"info".as_ref())
.and_then(|v| v.as_dict())
.ok_or_else(|| Error::InvalidTorrent("missing or invalid 'info' dict".into()))?;
let has_v2 = info.get(b"meta version".as_ref()).and_then(|v| v.as_int()) == Some(2);
let has_v1_pieces = info.get(b"pieces".as_ref()).is_some();
Ok(match (has_v2, has_v1_pieces) {
(true, true) => DetectedVersion::Hybrid,
(true, false) => DetectedVersion::V2Only,
_ => DetectedVersion::V1Only,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auto_detect_v1() {
let data = b"d4:infod6:lengthi100e4:name4:test12:piece lengthi256e6:pieces20:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ee";
let meta = torrent_from_bytes_any(data).unwrap();
assert!(meta.is_v1());
assert!(!meta.is_v2());
assert!(!meta.is_hybrid());
}
#[test]
fn auto_detect_v2() {
use std::collections::BTreeMap;
let mut info_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
let mut ft_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
let mut attr_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
attr_map.insert(b"length".to_vec(), BencodeValue::Integer(100));
let mut file_node: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
file_node.insert(b"".to_vec(), BencodeValue::Dict(attr_map));
ft_map.insert(b"f.txt".to_vec(), BencodeValue::Dict(file_node));
info_map.insert(b"file tree".to_vec(), BencodeValue::Dict(ft_map));
info_map.insert(b"meta version".to_vec(), BencodeValue::Integer(2));
info_map.insert(b"name".to_vec(), BencodeValue::Bytes(b"test".to_vec()));
info_map.insert(b"piece length".to_vec(), BencodeValue::Integer(16384));
let mut root_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
root_map.insert(b"info".to_vec(), BencodeValue::Dict(info_map));
let data = irontide_bencode::to_bytes(&BencodeValue::Dict(root_map)).unwrap();
let meta = torrent_from_bytes_any(&data).unwrap();
assert!(meta.is_v2());
assert!(!meta.is_v1());
assert!(!meta.is_hybrid());
}
#[test]
fn auto_detect_hybrid() {
use std::collections::BTreeMap;
let mut info_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
let mut ft_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
let mut attr_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
attr_map.insert(b"length".to_vec(), BencodeValue::Integer(16384));
let mut file_node: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
file_node.insert(b"".to_vec(), BencodeValue::Dict(attr_map));
ft_map.insert(b"test.dat".to_vec(), BencodeValue::Dict(file_node));
info_map.insert(b"file tree".to_vec(), BencodeValue::Dict(ft_map));
info_map.insert(b"meta version".to_vec(), BencodeValue::Integer(2));
info_map.insert(b"name".to_vec(), BencodeValue::Bytes(b"test".to_vec()));
info_map.insert(b"piece length".to_vec(), BencodeValue::Integer(16384));
info_map.insert(b"length".to_vec(), BencodeValue::Integer(16384));
info_map.insert(b"pieces".to_vec(), BencodeValue::Bytes(vec![0xAA; 20]));
let mut root_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
root_map.insert(b"info".to_vec(), BencodeValue::Dict(info_map));
let data = irontide_bencode::to_bytes(&BencodeValue::Dict(root_map)).unwrap();
let meta = torrent_from_bytes_any(&data).unwrap();
assert!(meta.is_hybrid());
assert!(!meta.is_v1());
assert!(!meta.is_v2());
let hashes = meta.info_hashes();
assert!(hashes.has_v1());
assert!(hashes.has_v2());
assert!(hashes.is_hybrid());
if let TorrentMeta::Hybrid(ref v1, ref v2) = meta {
assert_eq!(hashes.v1.unwrap(), v1.info_hash);
assert!(hashes.v2.is_some());
assert_ne!(
&v1.info_hash.0[..],
&v2.info_hashes.v2.unwrap().0[..20],
"v1 hash should NOT be a truncation in hybrid — it's SHA-1 not SHA-256[:20]"
);
} else {
panic!("expected Hybrid variant");
}
}
#[test]
fn hybrid_version_accessor() {
let data = b"d4:infod6:lengthi100e4:name4:test12:piece lengthi256e6:pieces20:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ee";
let meta = torrent_from_bytes_any(data).unwrap();
assert_eq!(
meta.version(),
crate::torrent_version::TorrentVersion::V1Only
);
}
#[test]
fn enum_queries_work() {
let data = b"d4:infod6:lengthi100e4:name4:test12:piece lengthi256e6:pieces20:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ee";
let meta = torrent_from_bytes_any(data).unwrap();
let hashes = meta.info_hashes();
assert!(hashes.has_v1());
assert!(!hashes.has_v2());
}
#[test]
fn as_v1_accessor() {
let data = b"d4:infod6:lengthi100e4:name4:test12:piece lengthi256e6:pieces20:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ee";
let meta = torrent_from_bytes_any(data).unwrap();
assert!(meta.as_v1().is_some());
assert!(meta.as_v2().is_none());
}
#[test]
fn torrent_meta_ssl_accessors() {
let data = b"d4:infod6:lengthi100e4:name4:test12:piece lengthi256e6:pieces20:\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ee";
let meta = torrent_from_bytes_any(data).unwrap();
assert!(!meta.is_ssl());
assert!(meta.ssl_cert().is_none());
}
}