Skip to main content

irontide_core/
info_hashes.rs

1//! Unified info-hash container for v1 (SHA-1) and v2 (SHA-256) hashes.
2//!
3//! Mirrors libtorrent's `info_hash_t` — every component that needs an info hash
4//! uses `InfoHashes`, which gracefully handles v1-only, v2-only, and hybrid torrents.
5
6use crate::hash::{Id20, Id32};
7use serde::Serialize;
8
9/// Holds optional v1 (SHA-1) and v2 (SHA-256) info hashes.
10///
11/// At least one hash must be present. Used throughout the stack as the canonical
12/// way to identify a torrent regardless of protocol version.
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
14pub struct InfoHashes {
15    /// v1 info hash (SHA-1 of the v1 info dict).
16    pub v1: Option<Id20>,
17    /// v2 info hash (SHA-256 of the v2 info dict).
18    pub v2: Option<Id32>,
19}
20
21impl InfoHashes {
22    /// Create with only a v1 (SHA-1) hash.
23    #[must_use]
24    pub fn v1_only(hash: Id20) -> Self {
25        Self {
26            v1: Some(hash),
27            v2: None,
28        }
29    }
30
31    /// Create with only a v2 (SHA-256) hash.
32    #[must_use]
33    pub fn v2_only(hash: Id32) -> Self {
34        Self {
35            v1: None,
36            v2: Some(hash),
37        }
38    }
39
40    /// Create with both v1 and v2 hashes (hybrid torrent).
41    #[must_use]
42    pub fn hybrid(v1: Id20, v2: Id32) -> Self {
43        Self {
44            v1: Some(v1),
45            v2: Some(v2),
46        }
47    }
48
49    /// Whether a v1 hash is present.
50    #[must_use]
51    pub fn has_v1(&self) -> bool {
52        self.v1.is_some()
53    }
54
55    /// Whether a v2 hash is present.
56    #[must_use]
57    pub fn has_v2(&self) -> bool {
58        self.v2.is_some()
59    }
60
61    /// Whether both v1 and v2 hashes are present (hybrid torrent).
62    #[must_use]
63    pub fn is_hybrid(&self) -> bool {
64        self.v1.is_some() && self.v2.is_some()
65    }
66
67    /// Get the best available v1 hash for tracker/DHT compatibility.
68    ///
69    /// Returns the v1 hash if present, otherwise truncates the v2 SHA-256
70    /// hash to 20 bytes (as specified by BEP 52 for DHT/tracker fallback).
71    #[must_use]
72    pub fn best_v1(&self) -> Id20 {
73        if let Some(v1) = self.v1 {
74            v1
75        } else if let Some(v2) = self.v2 {
76            let mut truncated = [0u8; 20];
77            truncated.copy_from_slice(&v2.0[..20]);
78            Id20(truncated)
79        } else {
80            Id20::ZERO
81        }
82    }
83}
84
85impl std::fmt::Display for InfoHashes {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        match (&self.v1, &self.v2) {
88            (Some(v1), Some(v2)) => write!(f, "v1:{v1} v2:{v2}"),
89            (Some(v1), None) => write!(f, "v1:{v1}"),
90            (None, Some(v2)) => write!(f, "v2:{v2}"),
91            (None, None) => write!(f, "<no hash>"),
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn v1_only_construction() {
102        let hash = Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap();
103        let ih = InfoHashes::v1_only(hash);
104        assert!(ih.has_v1());
105        assert!(!ih.has_v2());
106        assert!(!ih.is_hybrid());
107        assert_eq!(ih.best_v1(), hash);
108    }
109
110    #[test]
111    fn v2_only_construction() {
112        let hash =
113            Id32::from_hex("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
114                .unwrap();
115        let ih = InfoHashes::v2_only(hash);
116        assert!(!ih.has_v1());
117        assert!(ih.has_v2());
118        assert!(!ih.is_hybrid());
119        assert_eq!(ih.v2, Some(hash));
120    }
121
122    #[test]
123    fn hybrid_construction() {
124        let v1 = Id20::from_hex("aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d").unwrap();
125        let v2 = Id32::from_hex("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
126            .unwrap();
127        let ih = InfoHashes::hybrid(v1, v2);
128        assert!(ih.has_v1());
129        assert!(ih.has_v2());
130        assert!(ih.is_hybrid());
131        assert_eq!(ih.best_v1(), v1);
132    }
133
134    #[test]
135    fn best_v1_truncation_from_v2() {
136        let v2 = Id32::from_hex("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
137            .unwrap();
138        let ih = InfoHashes::v2_only(v2);
139        let truncated = ih.best_v1();
140        // First 20 bytes of the SHA-256 hash
141        assert_eq!(
142            truncated.to_hex(),
143            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4"
144        );
145    }
146}