1use irontide_bencode::BencodeValue;
9
10use crate::error::Error;
11use crate::info_hashes::InfoHashes;
12use crate::metainfo::TorrentMetaV1;
13use crate::metainfo_v2::TorrentMetaV2;
14use crate::torrent_version::TorrentVersion;
15
16#[derive(Debug, Clone)]
18pub enum TorrentMeta {
19 V1(TorrentMetaV1),
21 V2(TorrentMetaV2),
23 Hybrid(Box<TorrentMetaV1>, Box<TorrentMetaV2>),
26}
27
28impl TorrentMeta {
29 pub fn info_hashes(&self) -> InfoHashes {
31 match self {
32 Self::V1(t) => InfoHashes::v1_only(t.info_hash),
33 Self::V2(t) => t.info_hashes.clone(),
34 Self::Hybrid(v1, v2) => InfoHashes::hybrid(
35 v1.info_hash,
36 v2.info_hashes.v2.expect("v2 torrent must have v2 hash"),
37 ),
38 }
39 }
40
41 pub fn is_v1(&self) -> bool {
43 matches!(self, Self::V1(_))
44 }
45
46 pub fn is_v2(&self) -> bool {
48 matches!(self, Self::V2(_))
49 }
50
51 pub fn is_hybrid(&self) -> bool {
53 matches!(self, Self::Hybrid(_, _))
54 }
55
56 pub fn version(&self) -> TorrentVersion {
58 match self {
59 Self::V1(_) => TorrentVersion::V1Only,
60 Self::V2(_) => TorrentVersion::V2Only,
61 Self::Hybrid(_, _) => TorrentVersion::Hybrid,
62 }
63 }
64
65 pub fn as_v1(&self) -> Option<&TorrentMetaV1> {
67 match self {
68 Self::V1(v1) => Some(v1),
69 Self::Hybrid(v1, _) => Some(v1),
70 Self::V2(_) => None,
71 }
72 }
73
74 pub fn as_v2(&self) -> Option<&TorrentMetaV2> {
76 match self {
77 Self::V2(v2) => Some(v2),
78 Self::Hybrid(_, v2) => Some(v2),
79 Self::V1(_) => None,
80 }
81 }
82
83 pub fn best_v1_info_hash(&self) -> crate::hash::Id20 {
88 self.info_hashes().best_v1()
89 }
90
91 pub fn ssl_cert(&self) -> Option<&[u8]> {
93 match self {
94 Self::V1(v1) => v1.ssl_cert.as_deref(),
95 Self::V2(v2) => v2.ssl_cert.as_deref(),
96 Self::Hybrid(v1, _) => v1.ssl_cert.as_deref(),
97 }
98 }
99
100 pub fn is_ssl(&self) -> bool {
102 self.ssl_cert().is_some()
103 }
104}
105
106impl From<TorrentMetaV1> for TorrentMeta {
107 fn from(meta: TorrentMetaV1) -> Self {
108 Self::V1(meta)
109 }
110}
111
112impl From<TorrentMetaV2> for TorrentMeta {
113 fn from(meta: TorrentMetaV2) -> Self {
114 Self::V2(meta)
115 }
116}
117
118enum DetectedVersion {
120 V1Only,
121 V2Only,
122 Hybrid,
123}
124
125pub fn torrent_from_bytes_any(data: &[u8]) -> Result<TorrentMeta, Error> {
136 match detect_version(data)? {
137 DetectedVersion::V1Only => Ok(TorrentMeta::V1(crate::metainfo::torrent_from_bytes(data)?)),
138 DetectedVersion::V2Only => Ok(TorrentMeta::V2(crate::metainfo_v2::torrent_v2_from_bytes(
139 data,
140 )?)),
141 DetectedVersion::Hybrid => {
142 let v1 = crate::metainfo::torrent_from_bytes(data)?;
143 let mut v2 = crate::metainfo_v2::torrent_v2_from_bytes(data)?;
144 v2.info_hashes.v1 = Some(v1.info_hash);
148 Ok(TorrentMeta::Hybrid(Box::new(v1), Box::new(v2)))
149 }
150 }
151}
152
153fn detect_version(data: &[u8]) -> Result<DetectedVersion, Error> {
155 let root: BencodeValue = irontide_bencode::from_bytes(data)?;
156 let root_dict = root
157 .as_dict()
158 .ok_or_else(|| Error::InvalidTorrent("torrent must be a dict".into()))?;
159 let info = root_dict
160 .get(b"info".as_ref())
161 .and_then(|v| v.as_dict())
162 .ok_or_else(|| Error::InvalidTorrent("missing or invalid 'info' dict".into()))?;
163
164 let has_v2 = info
165 .get(b"meta version".as_ref())
166 .and_then(irontide_bencode::BencodeValue::as_int)
167 == Some(2);
168
169 let has_v1_pieces = info.get(b"pieces".as_ref()).is_some();
170
171 Ok(match (has_v2, has_v1_pieces) {
172 (true, true) => DetectedVersion::Hybrid,
173 (true, false) => DetectedVersion::V2Only,
174 _ => DetectedVersion::V1Only,
175 })
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181
182 #[test]
183 fn auto_detect_v1() {
184 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";
186 let meta = torrent_from_bytes_any(data).unwrap();
187 assert!(meta.is_v1());
188 assert!(!meta.is_v2());
189 assert!(!meta.is_hybrid());
190 }
191
192 #[test]
193 fn auto_detect_v2() {
194 use std::collections::BTreeMap;
195
196 let mut info_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
198 let mut ft_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
199 let mut attr_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
200 attr_map.insert(b"length".to_vec(), BencodeValue::Integer(100));
201 let mut file_node: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
202 file_node.insert(b"".to_vec(), BencodeValue::Dict(attr_map));
203 ft_map.insert(b"f.txt".to_vec(), BencodeValue::Dict(file_node));
204
205 info_map.insert(b"file tree".to_vec(), BencodeValue::Dict(ft_map));
206 info_map.insert(b"meta version".to_vec(), BencodeValue::Integer(2));
207 info_map.insert(b"name".to_vec(), BencodeValue::Bytes(b"test".to_vec()));
208 info_map.insert(b"piece length".to_vec(), BencodeValue::Integer(16384));
209
210 let mut root_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
211 root_map.insert(b"info".to_vec(), BencodeValue::Dict(info_map));
212
213 let data = irontide_bencode::to_bytes(&BencodeValue::Dict(root_map)).unwrap();
214 let meta = torrent_from_bytes_any(&data).unwrap();
215 assert!(meta.is_v2());
216 assert!(!meta.is_v1());
217 assert!(!meta.is_hybrid());
218 }
219
220 #[test]
221 fn auto_detect_hybrid() {
222 use std::collections::BTreeMap;
223
224 let mut info_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
226
227 let mut ft_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
229 let mut attr_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
230 attr_map.insert(b"length".to_vec(), BencodeValue::Integer(16384));
231 let mut file_node: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
232 file_node.insert(b"".to_vec(), BencodeValue::Dict(attr_map));
233 ft_map.insert(b"test.dat".to_vec(), BencodeValue::Dict(file_node));
234
235 info_map.insert(b"file tree".to_vec(), BencodeValue::Dict(ft_map));
236 info_map.insert(b"meta version".to_vec(), BencodeValue::Integer(2));
237
238 info_map.insert(b"name".to_vec(), BencodeValue::Bytes(b"test".to_vec()));
240 info_map.insert(b"piece length".to_vec(), BencodeValue::Integer(16384));
241 info_map.insert(b"length".to_vec(), BencodeValue::Integer(16384));
242 info_map.insert(b"pieces".to_vec(), BencodeValue::Bytes(vec![0xAA; 20]));
244
245 let mut root_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
246 root_map.insert(b"info".to_vec(), BencodeValue::Dict(info_map));
247
248 let data = irontide_bencode::to_bytes(&BencodeValue::Dict(root_map)).unwrap();
249 let meta = torrent_from_bytes_any(&data).unwrap();
250 assert!(meta.is_hybrid());
251 assert!(!meta.is_v1());
252 assert!(!meta.is_v2());
253
254 let hashes = meta.info_hashes();
256 assert!(hashes.has_v1());
257 assert!(hashes.has_v2());
258 assert!(hashes.is_hybrid());
259
260 if let TorrentMeta::Hybrid(ref v1, ref v2) = meta {
262 assert_eq!(hashes.v1.unwrap(), v1.info_hash);
264 assert!(hashes.v2.is_some());
266 assert_ne!(
267 &v1.info_hash.0[..],
268 &v2.info_hashes.v2.unwrap().0[..20],
269 "v1 hash should NOT be a truncation in hybrid — it's SHA-1 not SHA-256[:20]"
270 );
271 } else {
272 panic!("expected Hybrid variant");
273 }
274 }
275
276 #[test]
277 fn hybrid_version_accessor() {
278 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";
279 let meta = torrent_from_bytes_any(data).unwrap();
280 assert_eq!(
281 meta.version(),
282 crate::torrent_version::TorrentVersion::V1Only
283 );
284 }
285
286 #[test]
287 fn enum_queries_work() {
288 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";
289 let meta = torrent_from_bytes_any(data).unwrap();
290 let hashes = meta.info_hashes();
291 assert!(hashes.has_v1());
292 assert!(!hashes.has_v2());
293 }
294
295 #[test]
296 fn as_v1_accessor() {
297 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";
298 let meta = torrent_from_bytes_any(data).unwrap();
299 assert!(meta.as_v1().is_some());
300 assert!(meta.as_v2().is_none());
301 }
302
303 #[test]
304 fn torrent_meta_ssl_accessors() {
305 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";
307 let meta = torrent_from_bytes_any(data).unwrap();
308 assert!(!meta.is_ssl());
309 assert!(meta.ssl_cert().is_none());
310 }
311}