1mod path_types;
4
5pub use path_types::{TorrentBytes, TorrentPath};
6
7use sha1::{Digest as _, Sha1};
8use sha2::Sha256;
9
10use crate::bencode::{ByteString, Span, Value, ValueKind, parse_with_budget};
11use crate::limits::AllocationBudget;
12use crate::metadata::{
13 DhtNode, OptionalMetadata, validate_creation_date, validate_nodes, validate_tracker_tiers,
14 validate_web_seeds,
15};
16use crate::{Error, ErrorCategory, ParseOptions, Result};
17
18const INFO: &[u8] = b"info";
19const KNOWN_TOP_LEVEL_FIELDS: &[&[u8]] = &[
20 b"announce",
21 b"announce-list",
22 b"url-list",
23 b"nodes",
24 b"comment",
25 b"created by",
26 b"creation date",
27 INFO,
28];
29
30#[derive(Debug)]
32pub struct RawMetainfo<'a> {
33 original: &'a [u8],
34 root: Value<'a>,
35 info_index: usize,
36}
37
38impl<'a> RawMetainfo<'a> {
39 pub fn from_bytes(original: &'a [u8]) -> Result<Self> {
47 Self::from_bytes_with_options(original, ParseOptions::default())
48 }
49
50 pub fn from_bytes_with_options(original: &'a [u8], options: ParseOptions) -> Result<Self> {
57 let mut budget = AllocationBudget::new(options.limits());
58 let root = parse_with_budget(original, options.limits(), &mut budget)?;
59 Self::from_parsed(original, root)
60 }
61
62 fn from_parsed(original: &'a [u8], root: Value<'a>) -> Result<Self> {
63 let ValueKind::Dictionary(entries) = root.kind() else {
64 return Err(Error::metainfo_field(
65 "<root>",
66 "top-level metainfo must be a dictionary",
67 ));
68 };
69
70 let mut info_indices = entries
71 .iter()
72 .enumerate()
73 .filter_map(|(index, (key, _))| (key.bytes() == INFO).then_some(index));
74 let Some(info_index) = info_indices.next() else {
75 return Err(Error::metainfo_field("info", "missing required dictionary"));
76 };
77 if info_indices.next().is_some() {
78 return Err(Error::metainfo_field("info", "duplicate top-level key"));
79 }
80 if !matches!(entries[info_index].1.kind(), ValueKind::Dictionary(_)) {
81 return Err(Error::metainfo_field("info", "value must be a dictionary"));
82 }
83
84 Ok(Self {
85 original,
86 root,
87 info_index,
88 })
89 }
90
91 #[must_use]
93 pub fn canonicality(&self) -> Canonicality {
94 canonicality(self.original, &self.root)
95 }
96
97 #[must_use]
99 pub const fn original_bytes(&self) -> &'a [u8] {
100 self.original
101 }
102
103 #[must_use]
105 pub fn info_bytes(&self) -> &'a [u8] {
106 let span = self.info_span();
107 &self.original[span.range()]
108 }
109
110 #[must_use]
112 pub fn info_span(&self) -> Span {
113 self.info_value().span()
114 }
115
116 #[must_use]
118 pub fn info_value(&self) -> &Value<'a> {
119 &self.entries()[self.info_index].1
120 }
121
122 pub(crate) const fn root(&self) -> &Value<'a> {
123 &self.root
124 }
125
126 #[must_use]
128 pub fn announce(&self) -> Option<&Value<'a>> {
129 self.field(b"announce")
130 }
131
132 #[must_use]
134 pub fn announce_list(&self) -> Option<&Value<'a>> {
135 self.field(b"announce-list")
136 }
137
138 #[must_use]
140 pub fn url_list(&self) -> Option<&Value<'a>> {
141 self.field(b"url-list")
142 }
143
144 #[must_use]
146 pub fn nodes(&self) -> Option<&Value<'a>> {
147 self.field(b"nodes")
148 }
149
150 #[must_use]
152 pub fn comment(&self) -> Option<&Value<'a>> {
153 self.field(b"comment")
154 }
155
156 #[must_use]
158 pub fn created_by(&self) -> Option<&Value<'a>> {
159 self.field(b"created by")
160 }
161
162 #[must_use]
164 pub fn creation_date(&self) -> Option<&Value<'a>> {
165 self.field(b"creation date")
166 }
167
168 #[must_use]
170 pub fn unknown_fields(&self) -> Vec<(ByteString<'a>, &Value<'a>)> {
171 self.entries()
172 .iter()
173 .filter(|(key, _)| !KNOWN_TOP_LEVEL_FIELDS.contains(&key.bytes()))
174 .map(|(key, value)| (*key, value))
175 .collect()
176 }
177
178 #[must_use]
180 pub fn info_hash_sha1(&self) -> [u8; 20] {
181 Sha1::digest(self.info_bytes()).into()
182 }
183
184 #[must_use]
186 pub fn info_hash_sha256(&self) -> [u8; 32] {
187 Sha256::digest(self.info_bytes()).into()
188 }
189
190 pub(crate) fn field(&self, name: &[u8]) -> Option<&Value<'a>> {
191 self.entries()
192 .iter()
193 .find_map(|(key, value)| (key.bytes() == name).then_some(value))
194 }
195
196 pub(crate) fn entries(&self) -> &[(ByteString<'a>, Value<'a>)] {
197 let ValueKind::Dictionary(entries) = self.root.kind() else {
198 unreachable!("constructor requires a top-level dictionary")
199 };
200 entries
201 }
202}
203
204#[derive(Clone, Copy, Debug, Eq, PartialEq)]
206#[non_exhaustive]
207pub enum TorrentMode {
208 V1,
210 V2,
212 Hybrid,
214}
215
216#[derive(Debug)]
218pub struct V1File<'a> {
219 length: u64,
220 path: Vec<&'a [u8]>,
221 attributes: &'a [u8],
222}
223
224impl<'a> V1File<'a> {
225 #[must_use]
227 pub const fn length(&self) -> u64 {
228 self.length
229 }
230
231 #[must_use]
233 pub fn path_components(&self) -> &[&'a [u8]] {
234 &self.path
235 }
236
237 #[must_use]
239 pub fn path_utf8(&self) -> Option<Vec<&'a str>> {
240 self.path
241 .iter()
242 .map(|component| std::str::from_utf8(component).ok())
243 .collect()
244 }
245
246 #[must_use]
248 pub const fn attributes(&self) -> &'a [u8] {
249 self.attributes
250 }
251
252 #[must_use]
254 pub fn is_padding(&self) -> bool {
255 self.attributes.contains(&b'p') && is_padding_path(&self.path, self.length)
256 }
257}
258
259#[derive(Debug)]
261pub struct V1Metainfo<'a> {
262 name: &'a [u8],
263 piece_length: u64,
264 pieces: &'a [u8],
265 files: Vec<V1File<'a>>,
266 total_length: u64,
267 single_file: bool,
268 warnings: Vec<String>,
269}
270
271impl<'a> V1Metainfo<'a> {
272 pub fn from_raw(raw: &'a RawMetainfo<'a>) -> Result<Self> {
279 let info = dictionary_entries(raw.info_value(), "info")?;
280 let name = required_bytes(info, b"name", "info.name")?;
281 validate_path_component(name, "info.name")?;
282 let piece_length = positive_integer(info, b"piece length", "info.piece length")?;
283 let pieces = required_bytes(info, b"pieces", "info.pieces")?;
284 if pieces.len() % 20 != 0 {
285 return Err(Error::metainfo_field(
286 "info.pieces",
287 "length must be divisible by 20",
288 ));
289 }
290
291 let (files, total_length, single_file) = parse_v1_files(info, name)?;
292
293 let expected_piece_count = if total_length == 0 {
294 0
295 } else {
296 1 + (total_length - 1) / piece_length
297 };
298 let actual_piece_count = u64::try_from(pieces.len() / 20).map_err(|_| {
299 Error::metainfo_field("info.pieces", "piece count cannot be represented")
300 })?;
301 if actual_piece_count != expected_piece_count {
302 return Err(Error::metainfo_field(
303 "info.pieces",
304 format!(
305 "piece count {actual_piece_count} does not match expected {expected_piece_count}"
306 ),
307 ));
308 }
309
310 Ok(Self {
311 name,
312 piece_length,
313 pieces,
314 files,
315 total_length,
316 single_file,
317 warnings: Vec::new(),
318 })
319 }
320
321 #[must_use]
323 pub const fn mode(&self) -> TorrentMode {
324 TorrentMode::V1
325 }
326
327 #[must_use]
329 pub const fn name(&self) -> &'a [u8] {
330 self.name
331 }
332
333 #[must_use]
335 pub fn name_utf8(&self) -> Option<&'a str> {
336 std::str::from_utf8(self.name).ok()
337 }
338
339 #[must_use]
341 pub const fn piece_length(&self) -> u64 {
342 self.piece_length
343 }
344
345 #[must_use]
347 pub const fn pieces(&self) -> &'a [u8] {
348 self.pieces
349 }
350
351 #[must_use]
353 pub fn files(&self) -> &[V1File<'a>] {
354 &self.files
355 }
356
357 #[must_use]
359 pub const fn total_length(&self) -> u64 {
360 self.total_length
361 }
362
363 #[must_use]
365 pub const fn is_single_file(&self) -> bool {
366 self.single_file
367 }
368
369 #[must_use]
371 pub fn warnings(&self) -> &[String] {
372 &self.warnings
373 }
374}
375
376fn parse_v1_files<'a>(
377 info: &'a [(ByteString<'a>, Value<'a>)],
378 name: &'a [u8],
379) -> Result<(Vec<V1File<'a>>, u64, bool)> {
380 let length_value = unique_field(info, b"length", "info.length")?;
381 let files_value = unique_field(info, b"files", "info.files")?;
382 match (length_value, files_value) {
383 (Some(length), None) => {
384 let length = non_negative_value(length, "info.length")?;
385 Ok((
386 vec![V1File {
387 length,
388 path: vec![name],
389 attributes: unique_field(info, b"attr", "info.attr")?
390 .map_or(Ok(&[][..]), |value| value_bytes(value, "info.attr"))?,
391 }],
392 length,
393 true,
394 ))
395 }
396 (None, Some(files)) => parse_v1_file_list(files),
397 (Some(_), Some(_)) | (None, None) => Err(Error::metainfo_field(
398 "info",
399 "exactly one of length or files is required",
400 )),
401 }
402}
403
404fn parse_v1_file_list<'a>(files: &'a Value<'a>) -> Result<(Vec<V1File<'a>>, u64, bool)> {
405 let values = list_values(files, "info.files")?;
406 if values.is_empty() {
407 return Err(Error::metainfo_field(
408 "info.files",
409 "multi-file torrents require at least one file",
410 ));
411 }
412 let mut parsed = Vec::with_capacity(values.len());
413 let mut total = 0_u64;
414 for value in values {
415 let entry = dictionary_entries(value, "info.files")?;
416 let length = non_negative_value(
417 required_field(entry, b"length", "info.files.length")?,
418 "info.files.length",
419 )?;
420 total = total.checked_add(length).ok_or_else(|| {
421 Error::metainfo_field("info.files.length", "total payload length overflowed")
422 })?;
423 let components = list_values(
424 required_field(entry, b"path", "info.files.path")?,
425 "info.files.path",
426 )?;
427 if components.is_empty() {
428 return Err(Error::metainfo_field(
429 "info.files.path",
430 "path must contain at least one component",
431 ));
432 }
433 let mut path = Vec::with_capacity(components.len());
434 for component in components {
435 let component = value_bytes(component, "info.files.path")?;
436 validate_path_component(component, "info.files.path")?;
437 path.push(component);
438 }
439 let attributes = unique_field(entry, b"attr", "info.files.attr")?
440 .map_or(Ok(&[][..]), |value| value_bytes(value, "info.files.attr"))?;
441 parsed.push(V1File {
442 length,
443 path,
444 attributes,
445 });
446 }
447 validate_v1_path_graph(&parsed)?;
448 Ok((parsed, total, false))
449}
450
451fn validate_v1_path_graph(files: &[V1File<'_>]) -> Result<()> {
452 validate_torrent_path_graph(
453 files
454 .iter()
455 .map(|file| file.path_components().to_vec())
456 .collect(),
457 "info.files.path",
458 )
459}
460
461pub(crate) fn validate_torrent_path_graph(
462 mut paths: Vec<Vec<&[u8]>>,
463 field: &'static str,
464) -> Result<()> {
465 paths.sort_unstable();
466 for pair in paths.windows(2) {
467 if pair[0] == pair[1] || pair[1].starts_with(&pair[0]) {
468 return Err(Error::metainfo_field(
469 field,
470 "duplicate or prefix-colliding file path",
471 ));
472 }
473 }
474 #[cfg(windows)]
475 {
476 let mut mapped = paths
477 .iter()
478 .map(|path| windows_path_key(path))
479 .collect::<Vec<_>>();
480 mapped.sort_unstable();
481 if mapped
482 .windows(2)
483 .any(|pair| pair[0] == pair[1] || pair[1].starts_with(&pair[0]))
484 {
485 return Err(Error::metainfo_field(
486 field,
487 "paths collide after Windows filesystem mapping",
488 ));
489 }
490 }
491 Ok(())
492}
493
494#[cfg(windows)]
495fn windows_path_key(path: &[&[u8]]) -> Vec<Vec<u8>> {
496 path.iter()
497 .map(|component| component.iter().map(u8::to_ascii_lowercase).collect())
498 .collect()
499}
500
501fn is_padding_path(path: &[&[u8]], length: u64) -> bool {
502 let [directory, name] = path else {
503 return false;
504 };
505 if *directory != b".pad" {
506 return false;
507 }
508 let Some((_, encoded_length)) = parse_padding_name(name) else {
509 return false;
510 };
511 encoded_length == length
512}
513
514fn parse_padding_name(value: &[u8]) -> Option<(Option<u64>, u64)> {
515 if let Some(separator) = value.iter().position(|byte| *byte == b'-') {
516 let (left, right_with_separator) = value.split_at(separator);
517 let right = right_with_separator.get(1..)?;
518 if left.is_empty() || right.is_empty() {
519 return None;
520 }
521 let offset = std::str::from_utf8(left).ok()?.parse().ok()?;
522 let length = std::str::from_utf8(right).ok()?.parse().ok()?;
523 return Some((Some(offset), length));
524 }
525 let length = std::str::from_utf8(value).ok()?.parse().ok()?;
526 Some((None, length))
527}
528
529fn dictionary_entries<'a>(
530 value: &'a Value<'a>,
531 field: &'static str,
532) -> Result<&'a [(ByteString<'a>, Value<'a>)]> {
533 match value.kind() {
534 ValueKind::Dictionary(entries) => Ok(entries),
535 _ => Err(Error::metainfo_field(field, "must be a dictionary")),
536 }
537}
538
539fn list_values<'a>(value: &'a Value<'a>, field: &'static str) -> Result<&'a [Value<'a>]> {
540 match value.kind() {
541 ValueKind::List(values) => Ok(values),
542 _ => Err(Error::metainfo_field(field, "must be a list")),
543 }
544}
545
546fn value_bytes<'a>(value: &'a Value<'a>, field: &'static str) -> Result<&'a [u8]> {
547 value
548 .as_bytes()
549 .ok_or_else(|| Error::metainfo_field(field, "must be a byte string"))
550}
551
552fn unique_field<'a>(
553 entries: &'a [(ByteString<'a>, Value<'a>)],
554 key: &[u8],
555 field: &'static str,
556) -> Result<Option<&'a Value<'a>>> {
557 let mut matching = entries
558 .iter()
559 .filter_map(|(candidate, value)| (candidate.bytes() == key).then_some(value));
560 let value = matching.next();
561 if matching.next().is_some() {
562 return Err(Error::metainfo_field(field, "duplicate field"));
563 }
564 Ok(value)
565}
566
567fn required_field<'a>(
568 entries: &'a [(ByteString<'a>, Value<'a>)],
569 key: &[u8],
570 field: &'static str,
571) -> Result<&'a Value<'a>> {
572 unique_field(entries, key, field)?
573 .ok_or_else(|| Error::metainfo_field(field, "missing required field"))
574}
575
576fn required_bytes<'a>(
577 entries: &'a [(ByteString<'a>, Value<'a>)],
578 key: &[u8],
579 field: &'static str,
580) -> Result<&'a [u8]> {
581 value_bytes(required_field(entries, key, field)?, field)
582}
583
584fn positive_integer(
585 entries: &[(ByteString<'_>, Value<'_>)],
586 key: &[u8],
587 field: &'static str,
588) -> Result<u64> {
589 let value = required_field(entries, key, field)?;
590 let integer = value
591 .integer()
592 .ok_or_else(|| Error::metainfo_field(field, "must be an integer"))?;
593 let Some(integer) = integer.to_u64() else {
594 return Err(Error::metainfo_field(field, "is out of range"));
595 };
596 if integer == 0 {
597 return Err(Error::metainfo_field(field, "must be positive"));
598 }
599 Ok(integer)
600}
601
602fn non_negative_value(value: &Value<'_>, field: &'static str) -> Result<u64> {
603 let integer = value
604 .integer()
605 .ok_or_else(|| Error::metainfo_field(field, "must be an integer"))?;
606 integer
607 .to_u64()
608 .ok_or_else(|| Error::metainfo_field(field, "must be non-negative and fit in u64"))
609}
610
611fn validate_path_component(component: &[u8], field: &'static str) -> Result<()> {
612 if component.is_empty()
613 || component == b"."
614 || component == b".."
615 || component.contains(&b'/')
616 || component.contains(&b'\\')
617 || component.contains(&0)
618 {
619 return Err(Error::metainfo_field(
620 field,
621 "contains an unsafe path component",
622 ));
623 }
624 Ok(())
625}
626
627#[derive(Debug)]
629pub struct V2File<'a> {
630 length: u64,
631 path: Vec<&'a [u8]>,
632 pieces_root: Option<&'a [u8; 32]>,
633 attributes: &'a [u8],
634 piece_layer: Option<&'a [u8]>,
635 properties: &'a Value<'a>,
636}
637
638impl<'a> V2File<'a> {
639 #[must_use]
641 pub const fn length(&self) -> u64 {
642 self.length
643 }
644
645 #[must_use]
647 pub fn path_components(&self) -> &[&'a [u8]] {
648 &self.path
649 }
650
651 #[must_use]
653 pub fn path_utf8(&self) -> Option<Vec<&'a str>> {
654 self.path
655 .iter()
656 .map(|component| std::str::from_utf8(component).ok())
657 .collect()
658 }
659
660 #[must_use]
662 pub const fn pieces_root(&self) -> Option<&'a [u8; 32]> {
663 self.pieces_root
664 }
665
666 #[must_use]
668 pub const fn attributes(&self) -> &'a [u8] {
669 self.attributes
670 }
671
672 #[must_use]
674 pub const fn piece_layer(&self) -> Option<&'a [u8]> {
675 self.piece_layer
676 }
677
678 #[must_use]
680 pub const fn properties(&self) -> &'a Value<'a> {
681 self.properties
682 }
683}
684
685#[derive(Debug)]
687pub struct V2Metainfo<'a> {
688 name: &'a [u8],
689 piece_length: u64,
690 files: Vec<V2File<'a>>,
691 total_length: u64,
692 piece_count: u64,
693 mode: TorrentMode,
694}
695
696impl<'a> V2Metainfo<'a> {
697 pub fn from_raw(raw: &'a RawMetainfo<'a>) -> Result<Self> {
704 let info = dictionary_entries(raw.info_value(), "info")?;
705 let meta_version = required_field(info, b"meta version", "info.meta version")?
706 .integer()
707 .ok_or_else(|| Error::metainfo_field("info.meta version", "must be an integer"))?;
708 let meta_version = meta_version.to_i64().ok_or_else(|| {
709 Error::metainfo_field("info.meta version", "is outside the supported range")
710 })?;
711 if meta_version != 2 {
712 return Err(Error::metainfo_field(
713 "info.meta version",
714 "unsupported meta version",
715 ));
716 }
717 let name = required_bytes(info, b"name", "info.name")?;
718 validate_path_component(name, "info.name")?;
719 let piece_length = positive_integer(info, b"piece length", "info.piece length")?;
720 validate_v2_piece_length(piece_length)?;
721 let tree = required_field(info, b"file tree", "info.file tree")?;
722 let root_entries = dictionary_entries(tree, "info.file tree")?;
723 if root_entries.iter().any(|(key, _)| key.bytes().is_empty()) {
724 return Err(Error::metainfo_field(
725 "info.file tree",
726 "root must not be a file",
727 ));
728 }
729 let mut pending = Vec::new();
730 let mut path = Vec::new();
731 walk_file_tree(root_entries, &mut path, &mut pending)?;
732 validate_torrent_path_graph(
733 pending.iter().map(|file| file.path.clone()).collect(),
734 "info.file tree",
735 )?;
736 if pending.is_empty() {
737 return Err(Error::metainfo_field(
738 "info.file tree",
739 "must contain at least one file",
740 ));
741 }
742 let layer_entries = raw
743 .field(b"piece layers")
744 .map(|layers| dictionary_entries(layers, "piece layers"))
745 .transpose()?
746 .unwrap_or_default();
747 let files = validate_piece_layers(pending, layer_entries, piece_length)?;
748 let total_length =
749 checked_total_length(files.iter().map(V2File::length), "info.file tree.length")?;
750 let piece_count = checked_piece_count(
751 files.iter().map(V2File::length),
752 piece_length,
753 "info.file tree.piece count",
754 )?;
755 let has_v1 = unique_field(info, b"pieces", "info.pieces")?.is_some()
756 || unique_field(info, b"length", "info.length")?.is_some()
757 || unique_field(info, b"files", "info.files")?.is_some();
758 let mode = if has_v1 {
759 let v1 = V1Metainfo::from_raw(raw)?;
760 validate_hybrid(&v1, &files, piece_length)?;
761 TorrentMode::Hybrid
762 } else {
763 TorrentMode::V2
764 };
765 Ok(Self {
766 name,
767 piece_length,
768 files,
769 total_length,
770 piece_count,
771 mode,
772 })
773 }
774
775 #[must_use]
777 pub const fn mode(&self) -> TorrentMode {
778 self.mode
779 }
780
781 #[must_use]
783 pub const fn name(&self) -> &'a [u8] {
784 self.name
785 }
786
787 #[must_use]
789 pub const fn piece_length(&self) -> u64 {
790 self.piece_length
791 }
792
793 #[must_use]
795 pub fn files(&self) -> &[V2File<'a>] {
796 &self.files
797 }
798
799 #[must_use]
801 pub const fn total_length(&self) -> u64 {
802 self.total_length
803 }
804
805 #[must_use]
807 pub const fn piece_count(&self) -> u64 {
808 self.piece_count
809 }
810}
811
812pub(crate) fn checked_total_length(
813 lengths: impl IntoIterator<Item = u64>,
814 field: &'static str,
815) -> Result<u64> {
816 lengths.into_iter().try_fold(0_u64, |total, length| {
817 total
818 .checked_add(length)
819 .ok_or_else(|| Error::metainfo_field(field, "total payload length overflowed"))
820 })
821}
822
823pub(crate) fn checked_piece_count(
824 lengths: impl IntoIterator<Item = u64>,
825 piece_length: u64,
826 field: &'static str,
827) -> Result<u64> {
828 lengths.into_iter().try_fold(0_u64, |total, length| {
829 total
830 .checked_add(length.div_ceil(piece_length))
831 .ok_or_else(|| Error::metainfo_field(field, "aggregate piece count overflowed"))
832 })
833}
834
835struct PendingV2File<'a> {
836 length: u64,
837 path: Vec<&'a [u8]>,
838 pieces_root: Option<&'a [u8; 32]>,
839 attributes: &'a [u8],
840 properties: &'a Value<'a>,
841}
842
843fn walk_file_tree<'a>(
844 entries: &'a [(ByteString<'a>, Value<'a>)],
845 path: &mut Vec<&'a [u8]>,
846 files: &mut Vec<PendingV2File<'a>>,
847) -> Result<()> {
848 for (key, value) in entries {
849 if key.bytes().is_empty() {
850 return Err(Error::metainfo_field(
851 "info.file tree",
852 "file marker cannot be a sibling of child entries",
853 ));
854 }
855 validate_path_component(key.bytes(), "info.file tree")?;
856 path.push(key.bytes());
857 let node = dictionary_entries(value, "info.file tree")?;
858 if let Some(properties) = unique_field(node, b"", "info.file tree")? {
859 if node.len() != 1 {
860 return Err(Error::metainfo_field(
861 "info.file tree",
862 "file properties cannot have sibling child entries",
863 ));
864 }
865 files.push(parse_v2_file(properties, path)?);
866 } else {
867 if node.is_empty() {
868 return Err(Error::metainfo_field(
869 "info.file tree",
870 "directory node must not be empty",
871 ));
872 }
873 walk_file_tree(node, path, files)?;
874 }
875 path.pop();
876 }
877 Ok(())
878}
879
880fn parse_v2_file<'a>(properties: &'a Value<'a>, path: &[&'a [u8]]) -> Result<PendingV2File<'a>> {
881 let entries = dictionary_entries(properties, "info.file tree")?;
882 let length = non_negative_value(
883 required_field(entries, b"length", "info.file tree.length")?,
884 "info.file tree.length",
885 )?;
886 let root = unique_field(entries, b"pieces root", "info.file tree.pieces root")?
887 .map(|value| value_bytes(value, "info.file tree.pieces root"))
888 .transpose()?;
889 let pieces_root = match (length, root) {
890 (0, None) => None,
891 (0, Some(_)) => {
892 return Err(Error::metainfo_field(
893 "info.file tree.pieces root",
894 "empty files must not have a pieces root",
895 ));
896 }
897 (_, None) => {
898 return Err(Error::metainfo_field(
899 "info.file tree.pieces root",
900 "non-empty files require a pieces root",
901 ));
902 }
903 (_, Some(root)) => Some(<&[u8; 32]>::try_from(root).map_err(|_| {
904 Error::metainfo_field("info.file tree.pieces root", "must be exactly 32 bytes")
905 })?),
906 };
907 let attributes = unique_field(entries, b"attr", "info.file tree.attr")?
908 .map_or(Ok(&[][..]), |value| {
909 value_bytes(value, "info.file tree.attr")
910 })?;
911 Ok(PendingV2File {
912 length,
913 path: path.to_vec(),
914 pieces_root,
915 attributes,
916 properties,
917 })
918}
919
920pub(crate) fn validate_v2_piece_length(piece_length: u64) -> Result<u64> {
921 if piece_length < 16_384 || !piece_length.is_power_of_two() {
922 return Err(Error::metainfo_field(
923 "info.piece length",
924 "must be a power of two and at least 16384",
925 ));
926 }
927 Ok(piece_length)
928}
929
930fn validate_piece_layers<'a>(
931 pending: Vec<PendingV2File<'a>>,
932 layer_entries: &'a [(ByteString<'a>, Value<'a>)],
933 piece_length: u64,
934) -> Result<Vec<V2File<'a>>> {
935 let mut layers = std::collections::BTreeMap::new();
936 for (key, value) in layer_entries {
937 if layers.insert(key.bytes(), (value, false)).is_some() {
938 return Err(Error::metainfo_field(
939 "piece layers",
940 "duplicate pieces root",
941 ));
942 }
943 }
944 let mut files = Vec::with_capacity(pending.len());
945 for file in pending {
946 let piece_count = file.length.div_ceil(piece_length);
947 let piece_layer = if piece_count > 1 {
948 let root = file.pieces_root.expect("non-empty files have roots");
949 let Some((value, validated)) = layers.get_mut(root.as_slice()) else {
950 return Err(Error::metainfo_field(
951 "piece layers",
952 "missing layer for a multi-piece file",
953 ));
954 };
955 let bytes = value_bytes(value, "piece layers")?;
956 let expected_len = usize::try_from(piece_count)
957 .ok()
958 .and_then(|count| count.checked_mul(32))
959 .ok_or_else(|| Error::metainfo_field("piece layers", "length overflowed"))?;
960 if bytes.len() != expected_len {
961 return Err(Error::metainfo_field(
962 "piece layers",
963 "layer length does not match file piece count",
964 ));
965 }
966 if !*validated {
967 if merkle_root_from_piece_layer(bytes, piece_length)? != *root {
968 return Err(Error::metainfo_field(
969 "piece layers",
970 "layer hashes do not match the pieces root",
971 ));
972 }
973 *validated = true;
974 }
975 Some(bytes)
976 } else {
977 None
978 };
979 files.push(V2File {
980 length: file.length,
981 path: file.path,
982 pieces_root: file.pieces_root,
983 attributes: file.attributes,
984 piece_layer,
985 properties: file.properties,
986 });
987 }
988 if layers.values().any(|(_, validated)| !validated) {
989 return Err(Error::metainfo_field(
990 "piece layers",
991 "contains a layer not required by the file tree",
992 ));
993 }
994 Ok(files)
995}
996
997fn merkle_root_from_piece_layer(bytes: &[u8], piece_length: u64) -> Result<[u8; 32]> {
998 #[cfg(test)]
999 PIECE_LAYER_VALIDATIONS.with(|count| count.set(count.get() + 1));
1000 let mut hashes = bytes
1001 .chunks_exact(32)
1002 .map(|chunk| <[u8; 32]>::try_from(chunk).expect("exact chunks"))
1003 .collect::<Vec<_>>();
1004 let target = hashes.len().next_power_of_two();
1005 hashes.resize(target, zero_hash_for_piece_length(piece_length));
1006 while hashes.len() > 1 {
1007 hashes = hashes
1008 .chunks_exact(2)
1009 .map(|pair| hash_pair(pair[0], pair[1]))
1010 .collect();
1011 }
1012 hashes
1013 .pop()
1014 .ok_or_else(|| Error::metainfo_field("piece layers", "layer must not be empty"))
1015}
1016
1017#[cfg(test)]
1018thread_local! {
1019 static PIECE_LAYER_VALIDATIONS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1020}
1021
1022#[cfg(test)]
1023mod piece_layer_work_tests {
1024 use super::{PIECE_LAYER_VALIDATIONS, RawMetainfo, V2Metainfo, hash_pair};
1025 use crate::bencode::OwnedValue;
1026
1027 #[test]
1029 fn shared_piece_layer_is_hashed_once() {
1030 let hashes = [[1; 32], [2; 32]];
1031 let root = hash_pair(hashes[0], hashes[1]);
1032 let properties = OwnedValue::dictionary([
1033 (b"length".to_vec(), OwnedValue::integer(32_768)),
1034 (b"pieces root".to_vec(), OwnedValue::bytes(root.to_vec())),
1035 ])
1036 .unwrap();
1037 let leaf = OwnedValue::dictionary([(Vec::new(), properties)]).unwrap();
1038 let tree = OwnedValue::dictionary(
1039 (0..100).map(|index| (format!("file-{index:03}").into_bytes(), leaf.clone())),
1040 )
1041 .unwrap();
1042 let info = OwnedValue::dictionary([
1043 (b"file tree".to_vec(), tree),
1044 (b"meta version".to_vec(), OwnedValue::integer(2)),
1045 (b"name".to_vec(), OwnedValue::bytes(b"payload".to_vec())),
1046 (b"piece length".to_vec(), OwnedValue::integer(16_384)),
1047 ])
1048 .unwrap();
1049 let layers =
1050 OwnedValue::dictionary([(root.to_vec(), OwnedValue::bytes(hashes.concat()))]).unwrap();
1051 let bytes =
1052 OwnedValue::dictionary([(b"info".to_vec(), info), (b"piece layers".to_vec(), layers)])
1053 .unwrap()
1054 .to_vec()
1055 .unwrap();
1056 let raw = RawMetainfo::from_bytes(&bytes).unwrap();
1057 PIECE_LAYER_VALIDATIONS.with(|count| count.set(0));
1058 assert_eq!(V2Metainfo::from_raw(&raw).unwrap().files().len(), 100);
1059 assert_eq!(PIECE_LAYER_VALIDATIONS.with(std::cell::Cell::get), 1);
1060 }
1061}
1062
1063fn zero_hash_for_piece_length(piece_length: u64) -> [u8; 32] {
1064 let mut hash = [0; 32];
1065 let mut covered = 16_384;
1066 while covered < piece_length {
1067 hash = hash_pair(hash, hash);
1068 covered *= 2;
1069 }
1070 hash
1071}
1072
1073fn hash_pair(left: [u8; 32], right: [u8; 32]) -> [u8; 32] {
1074 use sha2::Digest as _;
1075 let mut hasher = Sha256::new();
1076 hasher.update(left);
1077 hasher.update(right);
1078 hasher.finalize().into()
1079}
1080
1081fn validate_hybrid(v1: &V1Metainfo<'_>, v2: &[V2File<'_>], piece_length: u64) -> Result<()> {
1082 if v1.piece_length() != piece_length {
1083 return Err(Error::metainfo_field(
1084 "info.piece length",
1085 "v1 and v2 piece lengths differ",
1086 ));
1087 }
1088 let mut real_files = Vec::new();
1089 let mut offset = 0_u64;
1090 let mut previous_was_padding = false;
1091 for (index, file) in v1.files().iter().enumerate() {
1092 if file.attributes().contains(&b'p') {
1093 if !file.is_padding() {
1094 return Err(Error::metainfo_field(
1095 "info.files.path",
1096 "padding file must use .pad/<length> or .pad/<offset>-<length>",
1097 ));
1098 }
1099 let required = (piece_length - (offset % piece_length)) % piece_length;
1100 let [_, name] = file.path_components() else {
1101 unreachable!("validated padding paths have two components");
1102 };
1103 let (encoded_offset, encoded_length) =
1104 parse_padding_name(name).expect("validated padding path has decimal length");
1105 if index == 0
1106 || index + 1 == v1.files().len()
1107 || previous_was_padding
1108 || required == 0
1109 || file.length() != required
1110 || encoded_offset.is_some_and(|encoded_offset| encoded_offset != offset)
1111 || encoded_length != file.length()
1112 {
1113 return Err(Error::metainfo_field(
1114 "info.files.attr",
1115 "padding file has invalid placement, path, or alignment length",
1116 ));
1117 }
1118 offset = offset.checked_add(file.length()).ok_or_else(|| {
1119 Error::metainfo_field("info.files.length", "hybrid offset overflowed")
1120 })?;
1121 previous_was_padding = true;
1122 continue;
1123 }
1124 if offset % piece_length != 0 {
1125 return Err(Error::metainfo_field(
1126 "info.files",
1127 "hybrid real file is not piece-aligned",
1128 ));
1129 }
1130 if file.path_components().first() == Some(&b".pad".as_slice()) {
1131 return Err(Error::metainfo_field(
1132 "info.files.path",
1133 "hybrid real files must not use the reserved .pad directory",
1134 ));
1135 }
1136 real_files.push(file);
1137 offset = offset.checked_add(file.length()).ok_or_else(|| {
1138 Error::metainfo_field("info.files.length", "hybrid offset overflowed")
1139 })?;
1140 previous_was_padding = false;
1141 }
1142 if real_files.len() != v2.len() {
1143 return Err(Error::metainfo_field(
1144 "info.files",
1145 "v1 and v2 file counts differ",
1146 ));
1147 }
1148 for (v1_file, v2_file) in real_files.into_iter().zip(v2) {
1149 if v1_file.length() != v2_file.length()
1150 || v1_file.path_components() != v2_file.path_components()
1151 {
1152 return Err(Error::metainfo_field(
1153 "info.files",
1154 "v1 and v2 paths or lengths differ",
1155 ));
1156 }
1157 }
1158 Ok(())
1159}
1160
1161#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1163pub struct InfoHashV1([u8; 20]);
1164
1165impl InfoHashV1 {
1166 pub(crate) const fn new(bytes: [u8; 20]) -> Self {
1167 Self(bytes)
1168 }
1169
1170 #[must_use]
1172 pub const fn as_bytes(&self) -> &[u8; 20] {
1173 &self.0
1174 }
1175
1176 #[must_use]
1178 pub fn hex(&self) -> String {
1179 encode_hex(&self.0)
1180 }
1181}
1182
1183impl std::fmt::Display for InfoHashV1 {
1184 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1185 formatter.write_str(&self.hex())
1186 }
1187}
1188
1189#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
1191pub struct InfoHashV2([u8; 32]);
1192
1193impl InfoHashV2 {
1194 pub(crate) const fn new(bytes: [u8; 32]) -> Self {
1195 Self(bytes)
1196 }
1197
1198 #[must_use]
1200 pub const fn as_bytes(&self) -> &[u8; 32] {
1201 &self.0
1202 }
1203
1204 #[must_use]
1206 pub fn hex(&self) -> String {
1207 encode_hex(&self.0)
1208 }
1209}
1210
1211impl std::fmt::Display for InfoHashV2 {
1212 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1213 formatter.write_str(&self.hex())
1214 }
1215}
1216
1217#[derive(Clone, Debug, Eq, PartialEq)]
1219pub struct TorrentFile {
1220 length: u64,
1221 path: Vec<Vec<u8>>,
1222 attributes: Vec<u8>,
1223 pieces_root: Option<[u8; 32]>,
1224}
1225
1226impl TorrentFile {
1227 #[must_use]
1229 pub const fn length(&self) -> u64 {
1230 self.length
1231 }
1232
1233 #[must_use]
1235 pub fn path_components(&self) -> &[Vec<u8>] {
1236 &self.path
1237 }
1238
1239 #[must_use]
1241 pub fn torrent_path(&self) -> TorrentPath {
1242 TorrentPath::from_raw(&self.path)
1243 }
1244
1245 #[must_use]
1247 pub fn path_utf8(&self) -> Option<Vec<&str>> {
1248 self.path
1249 .iter()
1250 .map(|component| std::str::from_utf8(component).ok())
1251 .collect()
1252 }
1253
1254 #[must_use]
1256 pub fn attributes(&self) -> &[u8] {
1257 &self.attributes
1258 }
1259
1260 #[must_use]
1262 pub fn is_padding(&self) -> bool {
1263 let path = self.path.iter().map(Vec::as_slice).collect::<Vec<_>>();
1264 self.attributes.contains(&b'p') && is_padding_path(&path, self.length)
1265 }
1266
1267 #[must_use]
1269 pub const fn pieces_root(&self) -> Option<&[u8; 32]> {
1270 self.pieces_root.as_ref()
1271 }
1272}
1273
1274#[derive(Clone, Debug, Eq, PartialEq)]
1276pub struct UnknownField {
1277 key: Vec<u8>,
1278 value: crate::bencode::OwnedValue,
1279 span: Span,
1280}
1281
1282impl UnknownField {
1283 #[must_use]
1285 pub fn key(&self) -> &[u8] {
1286 &self.key
1287 }
1288
1289 #[must_use]
1291 pub const fn value(&self) -> &crate::bencode::OwnedValue {
1292 &self.value
1293 }
1294
1295 #[must_use]
1297 pub const fn span(&self) -> Span {
1298 self.span
1299 }
1300}
1301
1302#[derive(Clone, Debug, Default, Eq, PartialEq)]
1305#[non_exhaustive]
1306pub enum Canonicality {
1307 #[default]
1309 Canonical,
1310 NonCanonical {
1312 offset: usize,
1314 message: String,
1316 },
1317}
1318
1319impl Canonicality {
1320 #[must_use]
1322 pub const fn is_canonical(&self) -> bool {
1323 matches!(self, Self::Canonical)
1324 }
1325
1326 #[must_use]
1328 pub const fn offset(&self) -> Option<usize> {
1329 match self {
1330 Self::Canonical => None,
1331 Self::NonCanonical { offset, .. } => Some(*offset),
1332 }
1333 }
1334
1335 #[must_use]
1337 pub fn message(&self) -> Option<&str> {
1338 match self {
1339 Self::Canonical => None,
1340 Self::NonCanonical { message, .. } => Some(message),
1341 }
1342 }
1343}
1344
1345fn canonicality(bytes: &[u8], root: &Value<'_>) -> Canonicality {
1346 match crate::bencode::validate_parsed_canonical(bytes, root) {
1347 Ok(()) => Canonicality::Canonical,
1348 Err(error) if error.category() == ErrorCategory::BencodeCanonical => {
1349 let offset = error.offset().unwrap_or_default();
1350 let Error::BencodeCanonical { message, .. } = error else {
1351 unreachable!("canonical validation returns canonical errors");
1352 };
1353 Canonicality::NonCanonical { offset, message }
1354 }
1355 Err(_) => unreachable!("already parsed bytes remain syntactically valid"),
1356 }
1357}
1358
1359#[derive(Clone, Debug, Eq, PartialEq)]
1361#[non_exhaustive]
1362pub struct ValidationWarning {
1363 message: String,
1364 field: Option<String>,
1365 offset: Option<usize>,
1366}
1367
1368impl ValidationWarning {
1369 #[must_use]
1371 pub fn message(&self) -> &str {
1372 &self.message
1373 }
1374
1375 #[must_use]
1377 pub fn field(&self) -> Option<&str> {
1378 self.field.as_deref()
1379 }
1380
1381 #[must_use]
1383 pub const fn offset(&self) -> Option<usize> {
1384 self.offset
1385 }
1386}
1387
1388#[derive(Clone, Debug, Default, Eq, PartialEq)]
1389pub struct ValidationReport {
1390 warnings: Vec<String>,
1391 warning_details: Vec<ValidationWarning>,
1392 canonicality: Canonicality,
1393}
1394
1395impl ValidationReport {
1396 #[must_use]
1398 pub const fn is_valid(&self) -> bool {
1399 true
1400 }
1401
1402 #[must_use]
1404 pub fn warnings(&self) -> &[String] {
1405 &self.warnings
1406 }
1407
1408 #[must_use]
1410 pub fn warning_details(&self) -> &[ValidationWarning] {
1411 &self.warning_details
1412 }
1413
1414 #[must_use]
1416 pub const fn canonicality(&self) -> &Canonicality {
1417 &self.canonicality
1418 }
1419}
1420
1421#[derive(Clone, Debug)]
1440pub struct Metainfo {
1441 original: Vec<u8>,
1442 canonical_root: crate::bencode::OwnedValue,
1443 canonical: std::sync::OnceLock<Vec<u8>>,
1444 mode: TorrentMode,
1445 name: Vec<u8>,
1446 piece_length: u64,
1447 total_length: u64,
1448 piece_count: u64,
1449 files: Vec<TorrentFile>,
1450 optional_metadata: OptionalMetadata,
1451 private: Option<bool>,
1452 info_hash_v1: Option<InfoHashV1>,
1453 info_hash_v2: Option<InfoHashV2>,
1454 unknown_fields: Vec<UnknownField>,
1455 validation: ValidationReport,
1456 parse_options: ParseOptions,
1457}
1458
1459impl Metainfo {
1460 #[must_use]
1462 pub fn magnet(&self, options: &crate::magnet::MagnetOptions) -> String {
1463 crate::magnet::generate(self, options)
1464 }
1465
1466 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
1472 Self::from_bytes_with_options(bytes, ParseOptions::default())
1473 }
1474
1475 pub fn from_bytes_with_options(bytes: &[u8], options: ParseOptions) -> Result<Self> {
1481 Self::from_vec_with_options(bytes.to_vec(), options)
1482 }
1483
1484 pub fn from_vec(bytes: Vec<u8>) -> Result<Self> {
1490 Self::from_vec_with_options(bytes, ParseOptions::default())
1491 }
1492
1493 pub fn from_vec_with_options(bytes: Vec<u8>, options: ParseOptions) -> Result<Self> {
1502 let limits = options.limits();
1503 limits.check_total_input(bytes.len())?;
1504 let mut budget = AllocationBudget::new(limits);
1505 let root = parse_with_budget(&bytes, limits, &mut budget)?;
1506 budget.charge(owned_load_cost(&root, bytes.len(), true)?)?;
1507 let raw = RawMetainfo::from_parsed(&bytes, root)?;
1508 let source_canonicality = raw.canonicality();
1509 let info_entries = dictionary_entries(raw.info_value(), "info")?;
1510 let mut snapshot = inspection_snapshot(&raw, info_entries)?;
1511 let canonical_root = value_to_owned(raw.root())?;
1512 let (optional_metadata, optional_warnings) = parse_optional_metadata(&raw, info_entries)?;
1513 snapshot.warnings.extend(optional_warnings);
1514 let private = unique_field(info_entries, b"private", "info.private")?
1515 .map(|value| {
1516 value
1517 .integer()
1518 .ok_or_else(|| Error::metainfo_field("info.private", "must be an integer"))
1519 .and_then(|value| match value.to_i64() {
1520 Some(0) => Ok(false),
1521 Some(1) => Ok(true),
1522 _ => Err(Error::metainfo_field("info.private", "must be 0 or 1")),
1523 })
1524 })
1525 .transpose()?;
1526 let unknown_fields = raw
1527 .unknown_fields()
1528 .into_iter()
1529 .map(|(key, value)| {
1530 Ok(UnknownField {
1531 key: key.bytes().to_vec(),
1532 value: value_to_owned(value)?,
1533 span: Span::new(key.span().start(), value.span().end()),
1534 })
1535 })
1536 .collect::<Result<Vec<_>>>()?;
1537 let info_hash_v1 = matches!(snapshot.mode, TorrentMode::V1 | TorrentMode::Hybrid)
1538 .then(|| InfoHashV1(raw.info_hash_sha1()));
1539 let info_hash_v2 = matches!(snapshot.mode, TorrentMode::V2 | TorrentMode::Hybrid)
1540 .then(|| InfoHashV2(raw.info_hash_sha256()));
1541 drop(raw);
1542 Ok(Self {
1543 original: bytes,
1544 canonical_root,
1545 canonical: std::sync::OnceLock::new(),
1546 mode: snapshot.mode,
1547 name: snapshot.name,
1548 piece_length: snapshot.piece_length,
1549 total_length: snapshot.total_length,
1550 piece_count: snapshot.piece_count,
1551 files: snapshot.files,
1552 optional_metadata,
1553 private,
1554 info_hash_v1,
1555 info_hash_v2,
1556 unknown_fields,
1557 validation: ValidationReport {
1558 warnings: snapshot
1559 .warnings
1560 .iter()
1561 .map(|warning| warning.message.clone())
1562 .collect(),
1563 warning_details: snapshot.warnings,
1564 canonicality: source_canonicality,
1565 },
1566 parse_options: options,
1567 })
1568 }
1569
1570 pub fn from_path(path: impl AsRef<std::path::Path>) -> Result<Self> {
1576 Self::from_path_with_options(path, ParseOptions::default())
1577 }
1578
1579 pub fn from_path_with_options(
1589 path: impl AsRef<std::path::Path>,
1590 options: ParseOptions,
1591 ) -> Result<Self> {
1592 use std::io::Read as _;
1593
1594 let path = path.as_ref();
1595 let limits = options.limits();
1596 let file = std::fs::File::open(path).map_err(|source| Error::io(path, source))?;
1597 let metadata = file.metadata().map_err(|source| Error::io(path, source))?;
1598 if metadata.is_file() {
1599 limits.check_total_input(usize::try_from(metadata.len()).unwrap_or(usize::MAX))?;
1600 }
1601 let maximum = limits.max_total_input();
1602 let initial = usize::try_from(metadata.len())
1603 .unwrap_or(maximum)
1604 .min(maximum);
1605 let mut bytes = Vec::with_capacity(initial);
1606 let read_limit = maximum.saturating_add(1);
1607 file.take(u64::try_from(read_limit).unwrap_or(u64::MAX))
1608 .read_to_end(&mut bytes)
1609 .map_err(|source| Error::io(path, source))?;
1610 limits.check_total_input(bytes.len())?;
1611 Self::from_vec_with_options(bytes, options)
1612 }
1613
1614 #[must_use]
1616 pub const fn parse_options(&self) -> ParseOptions {
1617 self.parse_options
1618 }
1619
1620 #[must_use]
1622 pub const fn mode(&self) -> TorrentMode {
1623 self.mode
1624 }
1625
1626 #[must_use]
1628 pub fn name(&self) -> &[u8] {
1629 &self.name
1630 }
1631
1632 #[must_use]
1634 pub fn name_utf8(&self) -> Option<&str> {
1635 std::str::from_utf8(&self.name).ok()
1636 }
1637
1638 #[must_use]
1640 pub fn files(&self) -> &[TorrentFile] {
1641 &self.files
1642 }
1643
1644 #[must_use]
1646 pub const fn piece_length(&self) -> u64 {
1647 self.piece_length
1648 }
1649
1650 #[must_use]
1652 pub const fn total_length(&self) -> u64 {
1653 self.total_length
1654 }
1655
1656 #[must_use]
1658 pub const fn piece_count(&self) -> u64 {
1659 self.piece_count
1660 }
1661
1662 #[must_use]
1664 pub fn trackers(&self) -> &[Vec<Vec<u8>>] {
1665 self.optional_metadata.trackers()
1666 }
1667
1668 #[must_use]
1670 pub fn web_seeds(&self) -> &[Vec<u8>] {
1671 self.optional_metadata.web_seeds()
1672 }
1673
1674 #[must_use]
1676 pub const fn optional_metadata(&self) -> &OptionalMetadata {
1677 &self.optional_metadata
1678 }
1679
1680 #[must_use]
1682 pub fn nodes(&self) -> &[DhtNode] {
1683 self.optional_metadata.nodes()
1684 }
1685
1686 #[must_use]
1688 pub const fn private(&self) -> Option<bool> {
1689 self.private
1690 }
1691
1692 #[must_use]
1694 pub fn source(&self) -> Option<&[u8]> {
1695 self.optional_metadata.source()
1696 }
1697
1698 #[must_use]
1700 pub fn comment(&self) -> Option<&[u8]> {
1701 self.optional_metadata.comment()
1702 }
1703
1704 #[must_use]
1706 pub fn created_by(&self) -> Option<&[u8]> {
1707 self.optional_metadata.created_by()
1708 }
1709
1710 #[must_use]
1712 pub const fn creation_date(&self) -> Option<i64> {
1713 self.optional_metadata.creation_date()
1714 }
1715
1716 #[must_use]
1718 pub const fn info_hash_v1(&self) -> Option<InfoHashV1> {
1719 self.info_hash_v1
1720 }
1721
1722 #[must_use]
1724 pub const fn info_hash_v2(&self) -> Option<InfoHashV2> {
1725 self.info_hash_v2
1726 }
1727
1728 #[must_use]
1730 pub fn original_bytes(&self) -> &[u8] {
1731 &self.original
1732 }
1733
1734 #[must_use]
1736 pub fn unknown_fields(&self) -> &[UnknownField] {
1737 &self.unknown_fields
1738 }
1739
1740 #[must_use]
1742 pub fn unknown_field_bytes(&self, field: &UnknownField) -> &[u8] {
1743 &self.original[field.span().range()]
1744 }
1745
1746 #[must_use]
1748 pub const fn validate(&self) -> &ValidationReport {
1749 &self.validation
1750 }
1751
1752 pub fn write_original(&self, writer: &mut impl std::io::Write) -> Result<()> {
1758 writer
1759 .write_all(&self.original)
1760 .map_err(|source| Error::io("<writer>", source))
1761 }
1762
1763 pub fn write_canonical(&self, writer: &mut impl std::io::Write) -> Result<()> {
1769 writer
1770 .write_all(self.canonical_bytes()?)
1771 .map_err(|source| Error::io("<writer>", source))
1772 }
1773
1774 pub fn to_bytes(&self) -> Result<Vec<u8>> {
1781 Ok(self.canonical_bytes()?.clone())
1782 }
1783
1784 #[doc(hidden)]
1786 #[must_use]
1787 pub fn canonical_bytes_cached(&self) -> bool {
1788 self.canonical.get().is_some()
1789 }
1790
1791 fn canonical_bytes(&self) -> Result<&Vec<u8>> {
1792 if let Some(bytes) = self.canonical.get() {
1793 return Ok(bytes);
1794 }
1795 let bytes = self.canonical_root.to_vec()?;
1796 let _ = self.canonical.set(bytes);
1797 Ok(self
1798 .canonical
1799 .get()
1800 .expect("canonical bytes were initialized"))
1801 }
1802}
1803
1804fn encode_hex(bytes: &[u8]) -> String {
1805 const HEX: &[u8; 16] = b"0123456789abcdef";
1806 let mut output = String::with_capacity(bytes.len() * 2);
1807 for byte in bytes {
1808 output.push(char::from(HEX[usize::from(byte >> 4)]));
1809 output.push(char::from(HEX[usize::from(byte & 0x0f)]));
1810 }
1811 output
1812}
1813
1814fn owned_load_cost(root: &Value<'_>, input_length: usize, copy_original: bool) -> Result<usize> {
1815 let tree = owned_tree_cost(root)?;
1816 let repeated_tree = tree.checked_mul(2).ok_or_else(owned_allocation_overflow)?;
1817 let original = if copy_original { input_length } else { 0 };
1818 original
1819 .checked_add(repeated_tree)
1820 .ok_or_else(owned_allocation_overflow)
1821}
1822
1823fn owned_tree_cost(value: &Value<'_>) -> Result<usize> {
1824 let base = std::mem::size_of::<crate::bencode::OwnedValue>();
1825 match value.kind() {
1826 ValueKind::Integer(value) => base
1827 .checked_add(value.encoded().len())
1828 .ok_or_else(owned_allocation_overflow),
1829 ValueKind::Bytes(bytes) => base
1830 .checked_add(bytes.len())
1831 .ok_or_else(owned_allocation_overflow),
1832 ValueKind::List(values) => values.iter().try_fold(
1833 base.checked_add(
1834 values
1835 .len()
1836 .checked_mul(std::mem::size_of::<crate::bencode::OwnedValue>())
1837 .ok_or_else(owned_allocation_overflow)?,
1838 )
1839 .ok_or_else(owned_allocation_overflow)?,
1840 |total, value| {
1841 total
1842 .checked_add(owned_tree_cost(value)?)
1843 .ok_or_else(owned_allocation_overflow)
1844 },
1845 ),
1846 ValueKind::Dictionary(entries) => entries.iter().try_fold(
1847 base.checked_add(
1848 entries
1849 .len()
1850 .checked_mul(
1851 std::mem::size_of::<(Vec<u8>, crate::bencode::OwnedValue)>()
1852 + 3 * std::mem::size_of::<usize>(),
1853 )
1854 .ok_or_else(owned_allocation_overflow)?,
1855 )
1856 .ok_or_else(owned_allocation_overflow)?,
1857 |total, (key, value)| {
1858 total
1859 .checked_add(key.bytes().len())
1860 .and_then(|total| total.checked_add(owned_tree_cost(value).ok()?))
1861 .ok_or_else(owned_allocation_overflow)
1862 },
1863 ),
1864 }
1865}
1866
1867fn owned_allocation_overflow() -> Error {
1868 Error::resource_limit("owned allocation", usize::MAX, usize::MAX)
1869}
1870
1871fn value_to_owned(value: &Value<'_>) -> Result<crate::bencode::OwnedValue> {
1872 use crate::bencode::OwnedValue;
1873 match value.kind() {
1874 ValueKind::Integer(value) => OwnedValue::integer_bytes(value.canonical_bytes()),
1875 ValueKind::Bytes(bytes) => Ok(OwnedValue::bytes(bytes.to_vec())),
1876 ValueKind::List(values) => values
1877 .iter()
1878 .map(value_to_owned)
1879 .collect::<Result<Vec<_>>>()
1880 .map(OwnedValue::list),
1881 ValueKind::Dictionary(entries) => OwnedValue::dictionary(
1882 entries
1883 .iter()
1884 .map(|(key, value)| Ok((key.bytes().to_vec(), value_to_owned(value)?)))
1885 .collect::<Result<Vec<_>>>()?,
1886 ),
1887 }
1888}
1889
1890fn parse_optional_metadata(
1891 raw: &RawMetainfo<'_>,
1892 info: &[(ByteString<'_>, Value<'_>)],
1893) -> Result<(OptionalMetadata, Vec<ValidationWarning>)> {
1894 let root = dictionary_entries(raw.root(), "<root>")?;
1895 let announce = unique_field(root, b"announce", "announce")?
1896 .map(|value| value_bytes(value, "announce").map(ToOwned::to_owned))
1897 .transpose()?;
1898 if announce.as_ref().is_some_and(Vec::is_empty) {
1899 return Err(Error::metainfo_field("announce", "tracker URL is empty"));
1900 }
1901 let announce_list = unique_field(root, b"announce-list", "announce-list")?;
1902 let mut warnings = Vec::new();
1903 let trackers = if let Some(value) = announce_list {
1904 let tiers = list_values(value, "announce-list")?;
1905 if tiers.is_empty() {
1906 warnings.push(ValidationWarning {
1907 message: "empty announce-list ignored in favor of announce".into(),
1908 field: Some("announce-list".into()),
1909 offset: Some(value.span().start()),
1910 });
1911 announce.into_iter().map(|url| vec![url]).collect()
1912 } else {
1913 tiers
1914 .iter()
1915 .map(|tier| {
1916 list_values(tier, "announce-list")?
1917 .iter()
1918 .map(|tracker| Ok(value_bytes(tracker, "announce-list")?.to_vec()))
1919 .collect()
1920 })
1921 .collect::<Result<Vec<_>>>()?
1922 }
1923 } else {
1924 announce.into_iter().map(|url| vec![url]).collect()
1925 };
1926 validate_tracker_tiers(&trackers)?;
1927
1928 let web_seeds = match unique_field(root, b"url-list", "url-list")? {
1929 None => Vec::new(),
1930 Some(value) if value.as_bytes().is_some() => {
1931 vec![value_bytes(value, "url-list")?.to_vec()]
1932 }
1933 Some(value) => list_values(value, "url-list")?
1934 .iter()
1935 .map(|seed| Ok(value_bytes(seed, "url-list")?.to_vec()))
1936 .collect::<Result<Vec<_>>>()?,
1937 };
1938 validate_web_seeds(&web_seeds)?;
1939
1940 let nodes = parse_nodes(unique_field(root, b"nodes", "nodes")?)?;
1941 validate_nodes(&nodes)?;
1942
1943 let source = unique_field(info, b"source", "info.source")?
1944 .map(|value| value_bytes(value, "info.source").map(ToOwned::to_owned))
1945 .transpose()?;
1946 let comment = unique_field(root, b"comment", "comment")?
1947 .map(|value| value_bytes(value, "comment").map(ToOwned::to_owned))
1948 .transpose()?;
1949 let created_by = unique_field(root, b"created by", "created by")?
1950 .map(|value| value_bytes(value, "created by").map(ToOwned::to_owned))
1951 .transpose()?;
1952 let creation_date = unique_field(root, b"creation date", "creation date")?
1953 .map(|value| {
1954 value
1955 .integer()
1956 .and_then(crate::bencode::Integer::to_i64)
1957 .ok_or_else(|| Error::metainfo_field("creation date", "must be an i64 integer"))
1958 })
1959 .transpose()?;
1960 validate_creation_date(creation_date)?;
1961 Ok((
1962 OptionalMetadata::new(
1963 trackers,
1964 web_seeds,
1965 nodes
1966 .into_iter()
1967 .map(|(host, port)| DhtNode::new(host, port))
1968 .collect(),
1969 source,
1970 comment,
1971 created_by,
1972 creation_date,
1973 ),
1974 warnings,
1975 ))
1976}
1977
1978fn parse_nodes(value: Option<&Value<'_>>) -> Result<Vec<(Vec<u8>, u16)>> {
1979 let Some(value) = value else {
1980 return Ok(Vec::new());
1981 };
1982 list_values(value, "nodes")?
1983 .iter()
1984 .map(|node| {
1985 let pair = list_values(node, "nodes")?;
1986 if pair.len() != 2 {
1987 return Err(Error::metainfo_field(
1988 "nodes",
1989 "each node must be [host, port]",
1990 ));
1991 }
1992 let host = value_bytes(&pair[0], "nodes.host")?.to_vec();
1993 let port = pair[1]
1994 .integer()
1995 .and_then(crate::bencode::Integer::to_i64)
1996 .and_then(|value| u16::try_from(value).ok())
1997 .ok_or_else(|| {
1998 Error::metainfo_field("nodes.port", "must be an integer in 1..=65535")
1999 })?;
2000 Ok((host, port))
2001 })
2002 .collect()
2003}
2004
2005struct InspectionSnapshot {
2006 mode: TorrentMode,
2007 name: Vec<u8>,
2008 piece_length: u64,
2009 total_length: u64,
2010 piece_count: u64,
2011 files: Vec<TorrentFile>,
2012 warnings: Vec<ValidationWarning>,
2013}
2014
2015fn inspection_snapshot(
2016 raw: &RawMetainfo<'_>,
2017 info: &[(ByteString<'_>, Value<'_>)],
2018) -> Result<InspectionSnapshot> {
2019 if unique_field(info, b"meta version", "info.meta version")?.is_some() {
2020 let typed = V2Metainfo::from_raw(raw)?;
2021 let mut warnings = Vec::new();
2022 if raw
2023 .field(b"piece layers")
2024 .map(|layers| dictionary_entries(layers, "piece layers"))
2025 .transpose()?
2026 .is_some_and(<[_]>::is_empty)
2027 {
2028 warnings.push(ValidationWarning {
2029 message: "piece layers is present but empty; canonical output omits it".into(),
2030 field: Some("piece layers".into()),
2031 offset: raw.field(b"piece layers").map(|value| value.span().start()),
2032 });
2033 }
2034 Ok(InspectionSnapshot {
2035 mode: typed.mode(),
2036 name: typed.name().to_vec(),
2037 piece_length: typed.piece_length(),
2038 total_length: typed.total_length(),
2039 piece_count: typed.piece_count(),
2040 files: if typed.mode() == TorrentMode::Hybrid {
2041 V1Metainfo::from_raw(raw)?
2042 .files()
2043 .iter()
2044 .map(|file| TorrentFile {
2045 length: file.length(),
2046 path: file
2047 .path_components()
2048 .iter()
2049 .map(|part| part.to_vec())
2050 .collect(),
2051 attributes: file.attributes().to_vec(),
2052 pieces_root: typed
2053 .files()
2054 .iter()
2055 .find(|candidate| candidate.path_components() == file.path_components())
2056 .and_then(V2File::pieces_root)
2057 .copied(),
2058 })
2059 .collect()
2060 } else {
2061 typed
2062 .files()
2063 .iter()
2064 .map(|file| TorrentFile {
2065 length: file.length(),
2066 path: file
2067 .path_components()
2068 .iter()
2069 .map(|part| part.to_vec())
2070 .collect(),
2071 attributes: file.attributes().to_vec(),
2072 pieces_root: file.pieces_root().copied(),
2073 })
2074 .collect()
2075 },
2076 warnings,
2077 })
2078 } else {
2079 let typed = V1Metainfo::from_raw(raw)?;
2080 Ok(InspectionSnapshot {
2081 mode: typed.mode(),
2082 name: typed.name().to_vec(),
2083 piece_length: typed.piece_length(),
2084 total_length: typed.total_length(),
2085 piece_count: u64::try_from(typed.pieces().len() / 20)
2086 .expect("usize values fit in u64 on supported targets"),
2087 files: typed
2088 .files()
2089 .iter()
2090 .map(|file| TorrentFile {
2091 length: file.length(),
2092 path: file
2093 .path_components()
2094 .iter()
2095 .map(|part| part.to_vec())
2096 .collect(),
2097 attributes: file.attributes().to_vec(),
2098 pieces_root: None,
2099 })
2100 .collect(),
2101 warnings: typed
2102 .warnings()
2103 .iter()
2104 .map(|message| ValidationWarning {
2105 message: message.clone(),
2106 field: None,
2107 offset: None,
2108 })
2109 .collect(),
2110 })
2111 }
2112}
2113
2114#[cfg(test)]
2115mod checked_arithmetic_tests {
2116 use super::{checked_piece_count, checked_total_length};
2117 use crate::ErrorCategory;
2118
2119 #[test]
2122 fn platform_mapping_checks_case_collisions_separated_by_raw_byte_order() {
2123 let cases: [Vec<Vec<&[u8]>>; 2] = [
2124 vec![vec![b"A"], vec![b"B"], vec![b"a"]],
2125 vec![vec![b"A"], vec![b"B"], vec![b"a", b"child"]],
2126 ];
2127 for paths in cases {
2128 let result = super::validate_torrent_path_graph(paths, "info.files");
2129 if cfg!(windows) {
2130 assert_eq!(result.unwrap_err().category(), ErrorCategory::Metainfo);
2131 } else {
2132 assert!(result.is_ok());
2133 }
2134 }
2135 }
2136
2137 #[test]
2138 fn aggregate_boundaries_return_structured_errors() {
2139 assert_eq!(
2140 checked_total_length([u64::MAX], "length").unwrap(),
2141 u64::MAX
2142 );
2143 let length_error = checked_total_length([u64::MAX, 1], "length").unwrap_err();
2144 assert_eq!(length_error.category(), ErrorCategory::Metainfo);
2145 assert_eq!(length_error.field(), Some("length"));
2146
2147 assert_eq!(
2148 checked_piece_count([u64::MAX], 1, "pieces").unwrap(),
2149 u64::MAX
2150 );
2151 let piece_error = checked_piece_count([u64::MAX, 1], 1, "pieces").unwrap_err();
2152 assert_eq!(piece_error.category(), ErrorCategory::Metainfo);
2153 assert_eq!(piece_error.field(), Some("pieces"));
2154 }
2155}