torrust_info_hash/info_hash.rs
1//! A `BitTorrent` `InfoHash`. It's a unique identifier for a `BitTorrent` torrent.
2//!
3//! "The 20-byte sha1 hash of the bencoded form of the info value
4//! from the metainfo file."
5//!
6//! See [BEP 3. The `BitTorrent` Protocol Specification](https://www.bittorrent.org/beps/bep_0003.html)
7//! for the official specification.
8//!
9//! This module provides a type that can be used to represent infohashes.
10//!
11//! > **NOTICE**: It only supports Info Hash v1.
12//!
13//! Typically infohashes are represented as hex strings, but internally they are
14//! a 20-byte array.
15//!
16//! # Calculating the info-hash of a torrent file
17//!
18//! A sample torrent:
19//!
20//! - Torrent file: `mandelbrot_2048x2048_infohash_v1.png.torrent`
21//! - File: `mandelbrot_2048x2048.png`
22//! - Info Hash v1: `5452869be36f9f3350ccee6b4544e7e76caaadab`
23//! - Sha1 hash of the info dictionary: `5452869BE36F9F3350CCEE6B4544E7E76CAAADAB`
24//!
25//! A torrent file is a binary file encoded with [Bencode encoding](https://en.wikipedia.org/wiki/Bencode):
26//!
27// cspell:disable
28//! ```text
29//! 0000000: 6431 303a 6372 6561 7465 6420 6279 3138 d10:created by18
30//! 0000010: 3a71 4269 7474 6f72 7265 6e74 2076 342e :qBittorrent v4.
31//! 0000020: 342e 3131 333a 6372 6561 7469 6f6e 2064 4.113:creation d
32//! 0000030: 6174 6569 3136 3739 3637 3436 3238 6534 atei1679674628e4
33//! 0000040: 3a69 6e66 6f64 363a 6c65 6e67 7468 6931 :infod6:lengthi1
34//! 0000050: 3732 3230 3465 343a 6e61 6d65 3234 3a6d 72204e4:name24:m
35//! 0000060: 616e 6465 6c62 726f 745f 3230 3438 7832 andelbrot_2048x2
36//! 0000070: 3034 382e 706e 6731 323a 7069 6563 6520 048.png12:piece
37//! 0000080: 6c65 6e67 7468 6931 3633 3834 6536 3a70 lengthi16384e6:p
38//! 0000090: 6965 6365 7332 3230 3a7d 9171 0d9d 4dba ieces220:}.q..M.
39//! 00000a0: 889b 5420 54d5 2672 8d5a 863f e121 df77 ..T T.&r.Z.?.!.w
40//! 00000b0: c7f7 bb6c 7796 2166 2538 c5d9 cdab 8b08 ...lw.!f%8......
41//! 00000c0: ef8c 249b b2f5 c4cd 2adf 0bc0 0cf0 addf ..$.....*.......
42//! 00000d0: 7290 e5b6 414c 236c 479b 8e9f 46aa 0c0d r...AL#lG...F...
43//! 00000e0: 8ed1 97ff ee68 8b5f 34a3 87d7 71c5 a6f9 .....h._4...q...
44//! 00000f0: 8e2e a631 7cbd f0f9 e223 f9cc 80af 5400 ...1|....#....T.
45//! 0000100: 04f9 8569 1c77 89c1 764e d6aa bf61 a6c2 ...i.w..vN...a..
46//! 0000110: 8099 abb6 5f60 2f40 a825 be32 a33d 9d07 ...._`/@.%.2.=..
47//! 0000120: 0c79 6898 d49d 6349 af20 5866 266f 986b .yh...cI. Xf&o.k
48//! 0000130: 6d32 34cd 7d08 155e 1ad0 0009 57ab 303b m24.}..^....W.0;
49//! 0000140: 2060 c1dc 1287 d6f3 e745 4f70 6709 3631 `.......EOpg.61
50//! 0000150: 55f2 20f6 6ca5 156f 2c89 9569 1653 817d U. .l..o,..i.S.}
51//! 0000160: 31f1 b6bd 3742 cc11 0bb2 fc2b 49a5 85b6 1...7B.....+I...
52//! 0000170: fc76 7444 9365 65 .vtD.ee
53//! ```
54// cspell:enable
55//!
56//! You can generate that output with the command:
57//!
58//! ```text
59//! xxd mandelbrot_2048x2048_infohash_v1.png.torrent
60//! ```
61//!
62//! And you can show only the bytes (hexadecimal):
63//!
64//! ```text
65//! 6431303a6372656174656420627931383a71426974746f7272656e742076
66//! 342e342e3131333a6372656174696f6e2064617465693136373936373436
67//! 323865343a696e666f64363a6c656e6774686931373232303465343a6e61
68//! 6d6532343a6d616e64656c62726f745f3230343878323034382e706e6731
69//! 323a7069656365206c656e67746869313633383465363a70696563657332
70//! 32303a7d91710d9d4dba889b542054d526728d5a863fe121df77c7f7bb6c
71//! 779621662538c5d9cdab8b08ef8c249bb2f5c4cd2adf0bc00cf0addf7290
72//! e5b6414c236c479b8e9f46aa0c0d8ed197ffee688b5f34a387d771c5a6f9
73//! 8e2ea6317cbdf0f9e223f9cc80af540004f985691c7789c1764ed6aabf61
74//! a6c28099abb65f602f40a825be32a33d9d070c796898d49d6349af205866
75//! 266f986b6d3234cd7d08155e1ad0000957ab303b2060c1dc1287d6f3e745
76//! 4f706709363155f220f66ca5156f2c8995691653817d31f1b6bd3742cc11
77//! 0bb2fc2b49a585b6fc767444936565
78//! ```
79//!
80//! You can generate that output with the command:
81//!
82//! ```text
83//! `xxd -ps mandelbrot_2048x2048_infohash_v1.png.torrent`.
84//! ```
85//!
86//! The same data can be represented in a JSON format:
87//!
88//! ```json
89//! {
90//! "created by": "qBittorrent v4.4.1",
91//! "creation date": 1679674628,
92//! "info": {
93//! "length": 172204,
94//! "name": "mandelbrot_2048x2048.png",
95//! "piece length": 16384,
96//! "pieces": "<hex>7D 91 71 0D 9D 4D BA 88 9B 54 20 54 D5 26 72 8D 5A 86 3F E1 21 DF 77 C7 F7 BB 6C 77 96 21 66 25 38 C5 D9 CD AB 8B 08 EF 8C 24 9B B2 F5 C4 CD 2A DF 0B C0 0C F0 AD DF 72 90 E5 B6 41 4C 23 6C 47 9B 8E 9F 46 AA 0C 0D 8E D1 97 FF EE 68 8B 5F 34 A3 87 D7 71 C5 A6 F9 8E 2E A6 31 7C BD F0 F9 E2 23 F9 CC 80 AF 54 00 04 F9 85 69 1C 77 89 C1 76 4E D6 AA BF 61 A6 C2 80 99 AB B6 5F 60 2F 40 A8 25 BE 32 A3 3D 9D 07 0C 79 68 98 D4 9D 63 49 AF 20 58 66 26 6F 98 6B 6D 32 34 CD 7D 08 15 5E 1A D0 00 09 57 AB 30 3B 20 60 C1 DC 12 87 D6 F3 E7 45 4F 70 67 09 36 31 55 F2 20 F6 6C A5 15 6F 2C 89 95 69 16 53 81 7D 31 F1 B6 BD 37 42 CC 11 0B B2 FC 2B 49 A5 85 B6 FC 76 74 44 93</hex>"
97//! }
98//! }
99//! ```
100//!
101//! The JSON object was generated with: <https://github.com/Chocobo1/bencode_online>
102//!
103//! As you can see, there is a `info` attribute:
104//!
105//! ```json
106//! {
107//! "length": 172204,
108//! "name": "mandelbrot_2048x2048.png",
109//! "piece length": 16384,
110//! "pieces": "<hex>7D 91 71 0D 9D 4D BA 88 9B 54 20 54 D5 26 72 8D 5A 86 3F E1 21 DF 77 C7 F7 BB 6C 77 96 21 66 25 38 C5 D9 CD AB 8B 08 EF 8C 24 9B B2 F5 C4 CD 2A DF 0B C0 0C F0 AD DF 72 90 E5 B6 41 4C 23 6C 47 9B 8E 9F 46 AA 0C 0D 8E D1 97 FF EE 68 8B 5F 34 A3 87 D7 71 C5 A6 F9 8E 2E A6 31 7C BD F0 F9 E2 23 F9 CC 80 AF 54 00 04 F9 85 69 1C 77 89 C1 76 4E D6 AA BF 61 A6 C2 80 99 AB B6 5F 60 2F 40 A8 25 BE 32 A3 3D 9D 07 0C 79 68 98 D4 9D 63 49 AF 20 58 66 26 6F 98 6B 6D 32 34 CD 7D 08 15 5E 1A D0 00 09 57 AB 30 3B 20 60 C1 DC 12 87 D6 F3 E7 45 4F 70 67 09 36 31 55 F2 20 F6 6C A5 15 6F 2C 89 95 69 16 53 81 7D 31 F1 B6 BD 37 42 CC 11 0B B2 FC 2B 49 A5 85 B6 FC 76 74 44 93</hex>"
111//! }
112//! ```
113//!
114//! The infohash is the [SHA1](https://en.wikipedia.org/wiki/SHA-1) hash
115//! of the `info` attribute. That is, the SHA1 hash of:
116//!
117//! ```text
118//! 64363a6c656e6774686931373232303465343a6e61
119//! d6532343a6d616e64656c62726f745f3230343878323034382e706e6731
120//! 23a7069656365206c656e67746869313633383465363a70696563657332
121//! 2303a7d91710d9d4dba889b542054d526728d5a863fe121df77c7f7bb6c
122//! 79621662538c5d9cdab8b08ef8c249bb2f5c4cd2adf0bc00cf0addf7290
123//! 5b6414c236c479b8e9f46aa0c0d8ed197ffee688b5f34a387d771c5a6f9
124//! e2ea6317cbdf0f9e223f9cc80af540004f985691c7789c1764ed6aabf61
125//! 6c28099abb65f602f40a825be32a33d9d070c796898d49d6349af205866
126//! 66f986b6d3234cd7d08155e1ad0000957ab303b2060c1dc1287d6f3e745
127//! f706709363155f220f66ca5156f2c8995691653817d31f1b6bd3742cc11
128//! bb2fc2b49a585b6fc7674449365
129//! ```
130//!
131//! You can hash that byte string with <https://www.pelock.com/products/hash-calculator>
132//!
133//! The result is a 20-char string: `5452869BE36F9F3350CCEE6B4544E7E76CAAADAB`
134
135use std::hash::{DefaultHasher, Hash, Hasher};
136use std::panic::Location;
137
138use thiserror::Error;
139
140/// `BitTorrent` Info Hash v1
141#[derive(Default, PartialEq, Eq, Hash, Clone, Copy, Debug)]
142pub struct InfoHash(pub [u8; 20]);
143
144pub const INFO_HASH_BYTES_LEN: usize = 20;
145
146impl InfoHash {
147 /// Create a new `InfoHash` from a byte slice.
148 ///
149 /// # Panics
150 ///
151 /// Creates an `InfoHash` from a 20-byte slice.
152 ///
153 /// # Panics
154 ///
155 /// Panics if the slice length is not exactly 20 bytes.
156 #[must_use]
157 pub fn from_bytes(bytes: &[u8]) -> Self {
158 assert_eq!(bytes.len(), INFO_HASH_BYTES_LEN, "expected 20 bytes for InfoHash");
159 let mut data = [0u8; INFO_HASH_BYTES_LEN];
160 data.copy_from_slice(bytes);
161 Self(data)
162 }
163
164 /// Returns the `InfoHash` internal byte array.
165 #[must_use]
166 pub const fn bytes(&self) -> [u8; 20] {
167 self.0
168 }
169
170 /// Returns the `InfoHash` as a hex string.
171 #[must_use]
172 pub fn to_hex_string(&self) -> String {
173 self.to_string()
174 }
175}
176
177impl Ord for InfoHash {
178 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
179 self.0.cmp(&other.0)
180 }
181}
182
183impl PartialOrd<Self> for InfoHash {
184 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
185 Some(self.cmp(other))
186 }
187}
188
189impl std::fmt::Display for InfoHash {
190 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191 let mut chars = [0u8; 40];
192
193 let hex_result = binascii::bin2hex(&self.0, &mut chars).map_err(|_| std::fmt::Error)?;
194 let hex_str = std::str::from_utf8(hex_result).map_err(|_| std::fmt::Error)?;
195 write!(f, "{hex_str}")
196 }
197}
198
199impl std::str::FromStr for InfoHash {
200 type Err = binascii::ConvertError;
201
202 fn from_str(s: &str) -> Result<Self, Self::Err> {
203 let mut i = Self::default();
204
205 if s.len() != 40 {
206 return Err(binascii::ConvertError::InvalidInputLength);
207 }
208
209 binascii::hex2bin(s.as_bytes(), &mut i.0)?;
210
211 Ok(i)
212 }
213}
214
215impl From<&[u8]> for InfoHash {
216 fn from(data: &[u8]) -> Self {
217 assert_eq!(data.len(), 20);
218 let mut ret = Self::default();
219 ret.0.clone_from_slice(data);
220 ret
221 }
222}
223
224/// for testing
225impl From<&DefaultHasher> for InfoHash {
226 fn from(data: &DefaultHasher) -> Self {
227 let n = data.finish().to_le_bytes();
228 let bytes = [
229 n[0], n[1], n[2], n[3], n[4], n[5], n[6], n[7], n[0], n[1], n[2], n[3], n[4], n[5], n[6], n[7], n[0], n[1], n[2],
230 n[3],
231 ];
232 Self(bytes)
233 }
234}
235
236impl From<&i32> for InfoHash {
237 fn from(n: &i32) -> Self {
238 let n = n.to_le_bytes();
239 let bytes = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, n[0], n[1], n[2], n[3]];
240 Self(bytes)
241 }
242}
243
244impl From<[u8; 20]> for InfoHash {
245 fn from(bytes: [u8; 20]) -> Self {
246 Self(bytes)
247 }
248}
249
250/// Errors that can occur when converting from a `Vec<u8>` to an `InfoHash`.
251#[derive(Error, Debug)]
252pub enum ConversionError {
253 /// Not enough bytes for infohash. An infohash is 20 bytes.
254 #[error("not enough bytes for infohash: {message} {location}")]
255 NotEnoughBytes {
256 location: &'static Location<'static>,
257 message: String,
258 },
259 /// Too many bytes for infohash. An infohash is 20 bytes.
260 #[error("too many bytes for infohash: {message} {location}")]
261 TooManyBytes {
262 location: &'static Location<'static>,
263 message: String,
264 },
265}
266
267impl TryFrom<Vec<u8>> for InfoHash {
268 type Error = ConversionError;
269
270 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
271 if bytes.len() < INFO_HASH_BYTES_LEN {
272 return Err(ConversionError::NotEnoughBytes {
273 location: Location::caller(),
274 message: format!("got {} bytes, expected {}", bytes.len(), INFO_HASH_BYTES_LEN),
275 });
276 }
277 if bytes.len() > INFO_HASH_BYTES_LEN {
278 return Err(ConversionError::TooManyBytes {
279 location: Location::caller(),
280 message: format!("got {} bytes, expected {}", bytes.len(), INFO_HASH_BYTES_LEN),
281 });
282 }
283 Ok(Self::from_bytes(&bytes))
284 }
285}
286
287#[cfg(feature = "serde")]
288impl serde::ser::Serialize for InfoHash {
289 fn serialize<S: serde::ser::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
290 let mut buffer = [0u8; 40];
291 let bytes_out = binascii::bin2hex(&self.0, &mut buffer).map_err(|e| serde::ser::Error::custom(format!("{e:?}")))?;
292 let str_out = std::str::from_utf8(bytes_out).map_err(|e| serde::ser::Error::custom(format!("{e:?}")))?;
293 serializer.serialize_str(str_out)
294 }
295}
296
297#[cfg(feature = "serde")]
298impl<'de> serde::de::Deserialize<'de> for InfoHash {
299 fn deserialize<D: serde::de::Deserializer<'de>>(des: D) -> Result<Self, D::Error> {
300 des.deserialize_str(InfoHashVisitor)
301 }
302}
303
304#[cfg(feature = "serde")]
305struct InfoHashVisitor;
306
307#[cfg(feature = "serde")]
308impl serde::de::Visitor<'_> for InfoHashVisitor {
309 type Value = InfoHash;
310
311 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 write!(formatter, "a 40 character long hash")
313 }
314
315 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
316 if v.len() != 40 {
317 return Err(serde::de::Error::invalid_value(
318 serde::de::Unexpected::Str(v),
319 &"a 40 character long string",
320 ));
321 }
322
323 let mut res = InfoHash::default();
324
325 if binascii::hex2bin(v.as_bytes(), &mut res.0).is_err() {
326 return Err(serde::de::Error::invalid_value(
327 serde::de::Unexpected::Str(v),
328 &"a hexadecimal string",
329 ));
330 }
331 Ok(res)
332 }
333}
334
335pub mod fixture {
336 use std::hash::{DefaultHasher, Hash, Hasher};
337
338 use super::InfoHash;
339
340 /// Generate as semi-stable pseudo-random infohash
341 ///
342 /// Note: If the [`DefaultHasher`] implementation changes
343 /// so will the resulting info-hashes.
344 ///
345 /// The results should not be relied upon between versions.
346 #[must_use]
347 pub fn gen_seeded_infohash(seed: u64) -> InfoHash {
348 let mut buf_a: [[u8; 8]; 4] = Default::default();
349 let mut buf_b = InfoHash::default();
350
351 let mut hasher = DefaultHasher::new();
352 seed.hash(&mut hasher);
353
354 for u in &mut buf_a {
355 seed.hash(&mut hasher);
356 *u = hasher.finish().to_le_bytes();
357 }
358
359 for (a, b) in buf_a.iter().flat_map(|a| a.iter()).zip(buf_b.0.iter_mut()) {
360 *b = *a;
361 }
362
363 buf_b
364 }
365}
366
367#[cfg(test)]
368mod tests {
369
370 use std::str::FromStr;
371
372 use serde::{Deserialize, Serialize};
373 use serde_json::json;
374
375 use super::InfoHash;
376
377 #[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)]
378 struct ContainingInfoHash {
379 pub info_hash: InfoHash,
380 }
381
382 #[test]
383 fn an_info_hash_can_be_created_from_a_valid_40_utf8_char_string_representing_an_hexadecimal_value() {
384 let info_hash = InfoHash::from_str("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF");
385 assert!(info_hash.is_ok());
386 }
387
388 #[test]
389 fn an_info_hash_can_not_be_created_from_a_utf8_string_representing_a_not_valid_hexadecimal_value() {
390 let info_hash = InfoHash::from_str("GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG");
391 assert!(info_hash.is_err());
392 }
393
394 #[test]
395 fn an_info_hash_can_only_be_created_from_a_40_utf8_char_string() {
396 let info_hash = InfoHash::from_str(&"F".repeat(39));
397 assert!(info_hash.is_err());
398
399 let info_hash = InfoHash::from_str(&"F".repeat(41));
400 assert!(info_hash.is_err());
401 }
402
403 #[test]
404 fn an_info_hash_should_by_displayed_like_a_40_utf8_lowercased_char_hex_string() {
405 let info_hash = InfoHash::from_str("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap();
406
407 let output = format!("{info_hash}");
408
409 assert_eq!(output, "ffffffffffffffffffffffffffffffffffffffff");
410 }
411
412 #[test]
413 fn an_info_hash_should_return_its_a_40_utf8_lowercased_char_hex_representations_as_string() {
414 let info_hash = InfoHash::from_str("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap();
415
416 assert_eq!(info_hash.to_hex_string(), "ffffffffffffffffffffffffffffffffffffffff");
417 }
418
419 #[test]
420 fn an_info_hash_can_be_created_from_a_valid_20_byte_array_slice() {
421 let info_hash: InfoHash = [255u8; 20].as_slice().into();
422
423 assert_eq!(
424 info_hash,
425 InfoHash::from_str("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap()
426 );
427 }
428
429 #[test]
430 fn an_info_hash_can_be_created_from_a_valid_20_byte_array() {
431 let info_hash: InfoHash = [255u8; 20].into();
432
433 assert_eq!(
434 info_hash,
435 InfoHash::from_str("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap()
436 );
437 }
438
439 #[test]
440 fn an_info_hash_can_be_created_from_a_byte_vector() {
441 let info_hash: InfoHash = [255u8; 20].to_vec().try_into().unwrap();
442
443 assert_eq!(
444 info_hash,
445 InfoHash::from_str("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap()
446 );
447 }
448
449 #[test]
450 fn it_should_fail_trying_to_create_an_info_hash_from_a_byte_vector_with_less_than_20_bytes() {
451 assert!(InfoHash::try_from([255u8; 19].to_vec()).is_err());
452 }
453
454 #[test]
455 fn it_should_fail_trying_to_create_an_info_hash_from_a_byte_vector_with_more_than_20_bytes() {
456 assert!(InfoHash::try_from([255u8; 21].to_vec()).is_err());
457 }
458
459 #[test]
460 fn an_info_hash_can_be_serialized() {
461 let s = ContainingInfoHash {
462 info_hash: InfoHash::from_str("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap(),
463 };
464
465 let json_serialized_value = serde_json::to_string(&s).unwrap();
466
467 assert_eq!(
468 json_serialized_value,
469 r#"{"info_hash":"ffffffffffffffffffffffffffffffffffffffff"}"#
470 );
471 }
472
473 #[test]
474 fn an_info_hash_can_be_deserialized() {
475 let json = json!({
476 "info_hash": "ffffffffffffffffffffffffffffffffffffffff",
477 });
478
479 let s: ContainingInfoHash = serde_json::from_value(json).unwrap();
480
481 assert_eq!(
482 s,
483 ContainingInfoHash {
484 info_hash: InfoHash::from_str("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap()
485 }
486 );
487 }
488}