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