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}