1#![allow(
2 clippy::cast_possible_truncation,
3 clippy::cast_possible_wrap,
4 clippy::cast_sign_loss,
5 reason = "M175: BEP 52 metainfo — Merkle tree geometry uses fixed widths per spec"
6)]
7
8use std::collections::BTreeMap;
14
15use bytes::Bytes;
16use irontide_bencode::BencodeValue;
17
18use crate::error::Error;
19use crate::file_tree::{FileTreeNode, V2FileInfo};
20use crate::hash::{Id20, Id32};
21use crate::info_hashes::InfoHashes;
22
23#[derive(Debug, Clone)]
29pub struct InfoDictV2 {
30 pub name: String,
32 pub piece_length: u64,
34 pub meta_version: u64,
36 pub file_tree: FileTreeNode,
38 pub ssl_cert: Option<Vec<u8>>,
41}
42
43impl InfoDictV2 {
44 #[must_use]
46 pub fn files(&self) -> Vec<V2FileInfo> {
47 self.file_tree.flatten()
48 }
49
50 #[must_use]
52 pub fn total_length(&self) -> u64 {
53 self.files().iter().map(|f| f.attr.length).sum()
54 }
55
56 #[must_use]
61 pub fn num_pieces(&self) -> u32 {
62 self.files()
63 .iter()
64 .map(|f| file_piece_count(f.attr.length, self.piece_length))
65 .sum()
66 }
67
68 #[must_use]
72 pub fn file_piece_ranges(&self) -> Vec<(V2FileInfo, u32, u32)> {
73 let files = self.files();
74 let mut result = Vec::with_capacity(files.len());
75 let mut offset = 0u32;
76
77 for file in files {
78 let count = file_piece_count(file.attr.length, self.piece_length);
79 result.push((file, offset, count));
80 offset += count;
81 }
82
83 result
84 }
85}
86
87fn file_piece_count(file_length: u64, piece_length: u64) -> u32 {
89 if file_length == 0 {
90 return 0;
91 }
92 file_length.div_ceil(piece_length) as u32
93}
94
95pub fn validate_info_v2(info: &InfoDictV2) -> Result<(), Error> {
97 if info.meta_version != 2 {
98 return Err(Error::InvalidTorrent(format!(
99 "expected meta version 2, got {}",
100 info.meta_version
101 )));
102 }
103
104 if info.piece_length < 16384 {
105 return Err(Error::InvalidTorrent(format!(
106 "piece length {} is less than minimum 16384",
107 info.piece_length
108 )));
109 }
110
111 if !info.piece_length.is_power_of_two() {
112 return Err(Error::InvalidTorrent(format!(
113 "piece length {} is not a power of 2",
114 info.piece_length
115 )));
116 }
117
118 Ok(())
119}
120
121#[derive(Debug, Clone)]
123pub struct TorrentMetaV2 {
124 pub info_hashes: InfoHashes,
126 pub info_bytes: Option<Bytes>,
128 pub announce: Option<String>,
130 pub announce_list: Option<Vec<Vec<String>>>,
132 pub comment: Option<String>,
134 pub created_by: Option<String>,
136 pub creation_date: Option<i64>,
138 pub info: InfoDictV2,
140 pub piece_layers: BTreeMap<Id32, Vec<u8>>,
145 pub ssl_cert: Option<Vec<u8>>,
147}
148
149impl TorrentMetaV2 {
150 pub fn validate_piece_layers(&self) -> Result<(), Error> {
159 for file in self.info.files() {
160 if file.attr.length <= self.info.piece_length {
161 continue; }
163
164 let root = file.attr.pieces_root.ok_or_else(|| {
165 Error::InvalidTorrent(format!(
166 "file {:?} has length {} but no pieces_root",
167 file.path, file.attr.length
168 ))
169 })?;
170
171 let layer = self.piece_layers.get(&root).ok_or_else(|| {
172 Error::InvalidTorrent(format!(
173 "missing piece layer for file {:?} (root: {})",
174 file.path, root
175 ))
176 })?;
177
178 let expected_pieces =
179 file_piece_count(file.attr.length, self.info.piece_length) as usize;
180 let actual_hashes = layer.len() / 32;
181
182 if layer.len() % 32 != 0 {
183 return Err(Error::InvalidTorrent(format!(
184 "piece layer for {:?} has length {} which is not a multiple of 32",
185 file.path,
186 layer.len()
187 )));
188 }
189
190 if actual_hashes != expected_pieces {
191 return Err(Error::InvalidTorrent(format!(
192 "piece layer for {:?} has {} hashes, expected {}",
193 file.path, actual_hashes, expected_pieces
194 )));
195 }
196 }
197 Ok(())
198 }
199
200 pub fn file_piece_hashes(&self, pieces_root: &Id32) -> Option<Vec<Id32>> {
202 let layer = self.piece_layers.get(pieces_root)?;
203 Some(
204 layer
205 .chunks_exact(32)
206 .map(|chunk| {
207 let mut hash = [0u8; 32];
208 hash.copy_from_slice(chunk);
209 Id32(hash)
210 })
211 .collect(),
212 )
213 }
214
215 pub fn piece_layer_for_file(&self, file_index: usize) -> Option<Vec<Id32>> {
217 let files = self.info.files();
218 let file = files.get(file_index)?;
219 let root = file.attr.pieces_root.as_ref()?;
220 self.file_piece_hashes(root)
221 }
222
223 pub fn take_piece_layers(&mut self) -> BTreeMap<Id32, Vec<u8>> {
228 std::mem::take(&mut self.piece_layers)
229 }
230}
231
232pub fn torrent_v2_from_bytes(data: &[u8]) -> Result<TorrentMetaV2, Error> {
238 let info_span = irontide_bencode::find_dict_key_span(data, "info")?;
240 let info_hash_v2 = crate::sha256(&data[info_span.clone()]);
241 let info_raw = Bytes::copy_from_slice(&data[info_span]);
242
243 let mut v1_truncated = [0u8; 20];
245 v1_truncated.copy_from_slice(&info_hash_v2.0[..20]);
246 let info_hashes = InfoHashes {
247 v1: Some(Id20(v1_truncated)),
248 v2: Some(info_hash_v2),
249 };
250
251 let root: BencodeValue = irontide_bencode::from_bytes(data)?;
253 let root_dict = root
254 .as_dict()
255 .ok_or_else(|| Error::InvalidTorrent("torrent must be a dict".into()))?;
256
257 let info_value = root_dict
259 .get(b"info".as_ref())
260 .ok_or_else(|| Error::InvalidTorrent("missing 'info' key".into()))?;
261 let info_dict = info_value
262 .as_dict()
263 .ok_or_else(|| Error::InvalidTorrent("'info' must be a dict".into()))?;
264
265 let name = info_dict
266 .get(b"name".as_ref())
267 .and_then(|v| v.as_bytes_raw())
268 .and_then(|b| std::str::from_utf8(b).ok())
269 .ok_or_else(|| Error::InvalidTorrent("missing or invalid 'name' in info".into()))?
270 .to_owned();
271
272 let piece_length = info_dict
273 .get(b"piece length".as_ref())
274 .and_then(irontide_bencode::BencodeValue::as_int)
275 .ok_or_else(|| Error::InvalidTorrent("missing 'piece length' in info".into()))?
276 as u64;
277
278 let meta_version = info_dict
279 .get(b"meta version".as_ref())
280 .and_then(irontide_bencode::BencodeValue::as_int)
281 .ok_or_else(|| Error::InvalidTorrent("missing 'meta version' in info".into()))?
282 as u64;
283
284 let file_tree_value = info_dict
285 .get(b"file tree".as_ref())
286 .ok_or_else(|| Error::InvalidTorrent("missing 'file tree' in info".into()))?;
287 let file_tree = FileTreeNode::from_bencode(file_tree_value)?;
288
289 let ssl_cert = info_dict
290 .get(b"ssl-cert".as_ref())
291 .and_then(|v| v.as_bytes_raw())
292 .map(<[u8]>::to_vec);
293
294 let info = InfoDictV2 {
295 name,
296 piece_length,
297 meta_version,
298 file_tree,
299 ssl_cert: ssl_cert.clone(),
300 };
301
302 validate_info_v2(&info)?;
303
304 let announce = root_dict
306 .get(b"announce".as_ref())
307 .and_then(|v| v.as_bytes_raw())
308 .and_then(|b| std::str::from_utf8(b).ok())
309 .map(std::borrow::ToOwned::to_owned);
310
311 let announce_list = root_dict.get(b"announce-list".as_ref()).and_then(|v| {
312 v.as_list().map(|tiers| {
313 tiers
314 .iter()
315 .filter_map(|tier| {
316 tier.as_list().map(|urls| {
317 urls.iter()
318 .filter_map(|u| {
319 u.as_bytes_raw()
320 .and_then(|b| std::str::from_utf8(b).ok())
321 .map(std::borrow::ToOwned::to_owned)
322 })
323 .collect()
324 })
325 })
326 .collect()
327 })
328 });
329
330 let comment = root_dict
331 .get(b"comment".as_ref())
332 .and_then(|v| v.as_bytes_raw())
333 .and_then(|b| std::str::from_utf8(b).ok())
334 .map(std::borrow::ToOwned::to_owned);
335
336 let created_by = root_dict
337 .get(b"created by".as_ref())
338 .and_then(|v| v.as_bytes_raw())
339 .and_then(|b| std::str::from_utf8(b).ok())
340 .map(std::borrow::ToOwned::to_owned);
341
342 let creation_date = root_dict
343 .get(b"creation date".as_ref())
344 .and_then(irontide_bencode::BencodeValue::as_int);
345
346 let piece_layers = parse_piece_layers(root_dict)?;
348
349 Ok(TorrentMetaV2 {
350 info_hashes,
351 info_bytes: Some(info_raw),
352 announce,
353 announce_list,
354 comment,
355 created_by,
356 creation_date,
357 info,
358 piece_layers,
359 ssl_cert,
360 })
361}
362
363fn parse_piece_layers(
365 root_dict: &BTreeMap<Vec<u8>, BencodeValue>,
366) -> Result<BTreeMap<Id32, Vec<u8>>, Error> {
367 let mut layers = BTreeMap::new();
368
369 let Some(layers_value) = root_dict.get(b"piece layers".as_ref()) else {
370 return Ok(layers);
371 };
372
373 let layers_dict = layers_value
374 .as_dict()
375 .ok_or_else(|| Error::InvalidTorrent("'piece layers' must be a dict".into()))?;
376
377 for (key, value) in layers_dict {
378 let root = Id32::from_bytes(key)?;
379 let hashes = value
380 .as_bytes_raw()
381 .ok_or_else(|| Error::InvalidTorrent("piece layer value must be bytes".into()))?;
382 layers.insert(root, hashes.to_vec());
383 }
384
385 Ok(layers)
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 fn make_v2_torrent_bytes(
394 name: &str,
395 piece_length: u64,
396 files: &[(&str, u64, Option<[u8; 32]>)],
397 piece_layers: &[([u8; 32], Vec<u8>)],
398 ) -> Vec<u8> {
399 let mut root_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
400
401 let mut info_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
403
404 let mut ft_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
406 for &(fname, length, ref root) in files {
407 let mut attr_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
408 attr_map.insert(b"length".to_vec(), BencodeValue::Integer(length as i64));
409 if let Some(root_bytes) = root {
410 attr_map.insert(
411 b"pieces root".to_vec(),
412 BencodeValue::Bytes(root_bytes.to_vec()),
413 );
414 }
415
416 let mut file_node: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
417 file_node.insert(b"".to_vec(), BencodeValue::Dict(attr_map));
418
419 ft_map.insert(fname.as_bytes().to_vec(), BencodeValue::Dict(file_node));
420 }
421
422 info_map.insert(b"file tree".to_vec(), BencodeValue::Dict(ft_map));
423 info_map.insert(b"meta version".to_vec(), BencodeValue::Integer(2));
424 info_map.insert(
425 b"name".to_vec(),
426 BencodeValue::Bytes(name.as_bytes().to_vec()),
427 );
428 info_map.insert(
429 b"piece length".to_vec(),
430 BencodeValue::Integer(piece_length as i64),
431 );
432
433 root_map.insert(b"info".to_vec(), BencodeValue::Dict(info_map));
434
435 if !piece_layers.is_empty() {
437 let mut pl_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
438 for (root_hash, hashes) in piece_layers {
439 pl_map.insert(root_hash.to_vec(), BencodeValue::Bytes(hashes.clone()));
440 }
441 root_map.insert(b"piece layers".to_vec(), BencodeValue::Dict(pl_map));
442 }
443
444 irontide_bencode::to_bytes(&BencodeValue::Dict(root_map)).unwrap()
445 }
446
447 #[test]
448 fn parse_minimal_v2_torrent() {
449 let data = make_v2_torrent_bytes("test", 16384, &[("file.txt", 1024, None)], &[]);
450 let torrent = torrent_v2_from_bytes(&data).unwrap();
451
452 assert_eq!(torrent.info.name, "test");
453 assert_eq!(torrent.info.piece_length, 16384);
454 assert_eq!(torrent.info.meta_version, 2);
455 assert_eq!(torrent.info.total_length(), 1024);
456 assert!(torrent.info_hashes.has_v2());
457 }
458
459 #[test]
460 fn parse_with_piece_layers() {
461 let root = [0xABu8; 32];
462 let hashes = vec![0xCDu8; 64];
464 let data = make_v2_torrent_bytes(
465 "test",
466 16384,
467 &[("big.dat", 32768, Some(root))],
468 &[(root, hashes)],
469 );
470
471 let torrent = torrent_v2_from_bytes(&data).unwrap();
472 assert_eq!(torrent.piece_layers.len(), 1);
473 let layer = torrent.piece_layers.get(&Id32(root)).unwrap();
474 assert_eq!(layer.len(), 64);
475 }
476
477 #[test]
478 fn reject_bad_meta_version() {
479 let mut info_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
481 let mut ft_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
482 let mut attr_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
483 attr_map.insert(b"length".to_vec(), BencodeValue::Integer(100));
484 let mut file_node: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
485 file_node.insert(b"".to_vec(), BencodeValue::Dict(attr_map));
486 ft_map.insert(b"f.txt".to_vec(), BencodeValue::Dict(file_node));
487
488 info_map.insert(b"file tree".to_vec(), BencodeValue::Dict(ft_map));
489 info_map.insert(b"meta version".to_vec(), BencodeValue::Integer(1));
490 info_map.insert(b"name".to_vec(), BencodeValue::Bytes(b"test".to_vec()));
491 info_map.insert(b"piece length".to_vec(), BencodeValue::Integer(16384));
492
493 let mut root_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
494 root_map.insert(b"info".to_vec(), BencodeValue::Dict(info_map));
495
496 let data = irontide_bencode::to_bytes(&BencodeValue::Dict(root_map)).unwrap();
497 assert!(torrent_v2_from_bytes(&data).is_err());
498 }
499
500 #[test]
501 fn info_bytes_populated() {
502 let data = make_v2_torrent_bytes("test", 16384, &[("f.txt", 100, None)], &[]);
503 let torrent = torrent_v2_from_bytes(&data).unwrap();
504 assert!(torrent.info_bytes.is_some());
505 let rehash = crate::sha256(&torrent.info_bytes.unwrap());
507 assert_eq!(rehash, torrent.info_hashes.v2.unwrap());
508 }
509
510 #[test]
511 fn piece_layer_for_file_indexed() {
512 let root = [0xABu8; 32];
513 let hashes = vec![0xCDu8; 64]; let data = make_v2_torrent_bytes(
515 "test",
516 16384,
517 &[("a.txt", 100, None), ("big.dat", 32768, Some(root))],
518 &[(root, hashes)],
519 );
520
521 let torrent = torrent_v2_from_bytes(&data).unwrap();
522
523 assert!(torrent.piece_layer_for_file(0).is_none());
525 let pieces = torrent.piece_layer_for_file(1).unwrap();
527 assert_eq!(pieces.len(), 2);
528 }
529
530 #[test]
533 fn valid_info_dict_v2() {
534 let data = make_v2_torrent_bytes("test", 16384, &[("f.txt", 1000, None)], &[]);
535 let torrent = torrent_v2_from_bytes(&data).unwrap();
536 assert_eq!(torrent.info.meta_version, 2);
537 }
538
539 #[test]
540 fn reject_piece_length_not_power_of_two() {
541 let mut info_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
542 let mut ft_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
543 let mut attr_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
544 attr_map.insert(b"length".to_vec(), BencodeValue::Integer(100));
545 let mut file_node: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
546 file_node.insert(b"".to_vec(), BencodeValue::Dict(attr_map));
547 ft_map.insert(b"f.txt".to_vec(), BencodeValue::Dict(file_node));
548
549 info_map.insert(b"file tree".to_vec(), BencodeValue::Dict(ft_map));
550 info_map.insert(b"meta version".to_vec(), BencodeValue::Integer(2));
551 info_map.insert(b"name".to_vec(), BencodeValue::Bytes(b"test".to_vec()));
552 info_map.insert(b"piece length".to_vec(), BencodeValue::Integer(30000));
553
554 let mut root_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
555 root_map.insert(b"info".to_vec(), BencodeValue::Dict(info_map));
556
557 let data = irontide_bencode::to_bytes(&BencodeValue::Dict(root_map)).unwrap();
558 assert!(torrent_v2_from_bytes(&data).is_err());
559 }
560
561 #[test]
562 fn reject_piece_length_too_small() {
563 let mut info_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
564 let mut ft_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
565 let mut attr_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
566 attr_map.insert(b"length".to_vec(), BencodeValue::Integer(100));
567 let mut file_node: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
568 file_node.insert(b"".to_vec(), BencodeValue::Dict(attr_map));
569 ft_map.insert(b"f.txt".to_vec(), BencodeValue::Dict(file_node));
570
571 info_map.insert(b"file tree".to_vec(), BencodeValue::Dict(ft_map));
572 info_map.insert(b"meta version".to_vec(), BencodeValue::Integer(2));
573 info_map.insert(b"name".to_vec(), BencodeValue::Bytes(b"test".to_vec()));
574 info_map.insert(b"piece length".to_vec(), BencodeValue::Integer(8192));
575
576 let mut root_map: BTreeMap<Vec<u8>, BencodeValue> = BTreeMap::new();
577 root_map.insert(b"info".to_vec(), BencodeValue::Dict(info_map));
578
579 let data = irontide_bencode::to_bytes(&BencodeValue::Dict(root_map)).unwrap();
580 assert!(torrent_v2_from_bytes(&data).is_err());
581 }
582
583 #[test]
584 fn files_list_and_total_length() {
585 let data = make_v2_torrent_bytes(
586 "test",
587 16384,
588 &[("a.txt", 100, None), ("b.txt", 200, None)],
589 &[],
590 );
591 let torrent = torrent_v2_from_bytes(&data).unwrap();
592 let files = torrent.info.files();
593 assert_eq!(files.len(), 2);
594 assert_eq!(torrent.info.total_length(), 300);
595 }
596
597 #[test]
598 fn num_pieces_with_per_file_alignment() {
599 let data = make_v2_torrent_bytes(
601 "test",
602 16384,
603 &[("a.dat", 16384, None), ("b.dat", 16384, None)],
604 &[],
605 );
606 let torrent = torrent_v2_from_bytes(&data).unwrap();
607 assert_eq!(torrent.info.num_pieces(), 2);
608
609 let data2 = make_v2_torrent_bytes(
612 "test",
613 16384,
614 &[("a.dat", 1, None), ("b.dat", 1, None)],
615 &[],
616 );
617 let torrent2 = torrent_v2_from_bytes(&data2).unwrap();
618 assert_eq!(torrent2.info.num_pieces(), 2);
619 }
620
621 #[test]
624 fn correct_layers_pass_validation() {
625 let root = [0xABu8; 32];
626 let hashes = vec![0xCDu8; 64]; let data = make_v2_torrent_bytes(
629 "test",
630 16384,
631 &[("big.dat", 32768, Some(root))],
632 &[(root, hashes)],
633 );
634 let torrent = torrent_v2_from_bytes(&data).unwrap();
635 assert!(torrent.validate_piece_layers().is_ok());
636 }
637
638 #[test]
639 fn missing_layer_fails_validation() {
640 let root = [0xABu8; 32];
641 let data = make_v2_torrent_bytes(
643 "test",
644 16384,
645 &[("big.dat", 32768, Some(root))],
646 &[], );
648 let torrent = torrent_v2_from_bytes(&data).unwrap();
649 assert!(torrent.validate_piece_layers().is_err());
650 }
651
652 #[test]
653 fn small_file_no_layer_needed() {
654 let data = make_v2_torrent_bytes("test", 16384, &[("small.txt", 100, None)], &[]);
656 let torrent = torrent_v2_from_bytes(&data).unwrap();
657 assert!(torrent.validate_piece_layers().is_ok());
658 }
659
660 #[test]
661 fn wrong_hash_count_fails_validation() {
662 let root = [0xABu8; 32];
663 let hashes = vec![0xCDu8; 96]; let data = make_v2_torrent_bytes(
666 "test",
667 16384,
668 &[("big.dat", 32768, Some(root))],
669 &[(root, hashes)],
670 );
671 let torrent = torrent_v2_from_bytes(&data).unwrap();
672 assert!(torrent.validate_piece_layers().is_err());
673 }
674}