torrust_tracker/servers/http/
percent_encoding.rs

1//! This module contains functions for percent decoding infohashes and peer IDs.
2//!
3//! Percent encoding is an encoding format used to encode arbitrary data in a
4//! format that is safe to use in URLs. It is used by the HTTP tracker protocol
5//! to encode infohashes and peer ids in the URLs of requests.
6//!
7//! `BitTorrent` infohashes and peer ids are percent encoded like any other
8//! arbitrary URL parameter. But they are encoded from binary data (byte arrays)
9//! which may not be valid UTF-8. That makes hard to use the `percent_encoding`
10//! crate to decode them because all of them expect a well-formed UTF-8 string.
11//! However, percent encoding is not limited to UTF-8 strings.
12//!
13//! More information about "Percent Encoding" can be found here:
14//!
15//! - <https://datatracker.ietf.org/doc/html/rfc3986#section-2.1>
16//! - <https://en.wikipedia.org/wiki/URL_encoding>
17//! - <https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding>
18use aquatic_udp_protocol::PeerId;
19use torrust_tracker_primitives::info_hash::{self, InfoHash};
20use torrust_tracker_primitives::peer;
21
22/// Percent decodes a percent encoded infohash. Internally an
23/// [`InfoHash`] is a 20-byte array.
24///
25/// For example, given the infohash `3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0`,
26/// it's percent encoded representation is `%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0`.
27///
28/// ```rust
29/// use std::str::FromStr;
30/// use torrust_tracker::servers::http::percent_encoding::percent_decode_info_hash;
31/// use torrust_tracker_primitives::info_hash::InfoHash;
32/// use torrust_tracker_primitives::peer;
33///
34/// let encoded_infohash = "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0";
35///
36/// let info_hash = percent_decode_info_hash(encoded_infohash).unwrap();
37///
38/// assert_eq!(
39///     info_hash,
40///     InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap()
41/// );
42/// ```
43///
44/// # Errors
45///
46/// Will return `Err` if the decoded bytes do not represent a valid
47/// [`InfoHash`].
48pub fn percent_decode_info_hash(raw_info_hash: &str) -> Result<InfoHash, info_hash::ConversionError> {
49    let bytes = percent_encoding::percent_decode_str(raw_info_hash).collect::<Vec<u8>>();
50    InfoHash::try_from(bytes)
51}
52
53/// Percent decodes a percent encoded peer id. Internally a peer [`Id`](PeerId)
54/// is a 20-byte array.
55///
56/// For example, given the peer id `*b"-qB00000000000000000"`,
57/// it's percent encoded representation is `%2DqB00000000000000000`.
58///
59/// ```rust
60/// use std::str::FromStr;
61///
62/// use aquatic_udp_protocol::PeerId;
63/// use torrust_tracker::servers::http::percent_encoding::percent_decode_peer_id;
64/// use torrust_tracker_primitives::info_hash::InfoHash;
65///
66/// let encoded_peer_id = "%2DqB00000000000000000";
67///
68/// let peer_id = percent_decode_peer_id(encoded_peer_id).unwrap();
69///
70/// assert_eq!(peer_id, PeerId(*b"-qB00000000000000000"));
71/// ```
72///
73/// # Errors
74///
75/// Will return `Err` if if the decoded bytes do not represent a valid [`PeerId`].
76pub fn percent_decode_peer_id(raw_peer_id: &str) -> Result<PeerId, peer::IdConversionError> {
77    let bytes = percent_encoding::percent_decode_str(raw_peer_id).collect::<Vec<u8>>();
78    Ok(*peer::Id::try_from(bytes)?)
79}
80
81#[cfg(test)]
82mod tests {
83    use std::str::FromStr;
84
85    use aquatic_udp_protocol::PeerId;
86    use torrust_tracker_primitives::info_hash::InfoHash;
87
88    use crate::servers::http::percent_encoding::{percent_decode_info_hash, percent_decode_peer_id};
89
90    #[test]
91    fn it_should_decode_a_percent_encoded_info_hash() {
92        let encoded_infohash = "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0";
93
94        let info_hash = percent_decode_info_hash(encoded_infohash).unwrap();
95
96        assert_eq!(
97            info_hash,
98            InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap()
99        );
100    }
101
102    #[test]
103    fn it_should_fail_decoding_an_invalid_percent_encoded_info_hash() {
104        let invalid_encoded_infohash = "invalid percent-encoded infohash";
105
106        let info_hash = percent_decode_info_hash(invalid_encoded_infohash);
107
108        assert!(info_hash.is_err());
109    }
110
111    #[test]
112    fn it_should_decode_a_percent_encoded_peer_id() {
113        let encoded_peer_id = "%2DqB00000000000000000";
114
115        let peer_id = percent_decode_peer_id(encoded_peer_id).unwrap();
116
117        assert_eq!(peer_id, PeerId(*b"-qB00000000000000000"));
118    }
119
120    #[test]
121    fn it_should_fail_decoding_an_invalid_percent_encoded_peer_id() {
122        let invalid_encoded_peer_id = "invalid percent-encoded peer id";
123
124        let peer_id = percent_decode_peer_id(invalid_encoded_peer_id);
125
126        assert!(peer_id.is_err());
127    }
128}