Skip to content

Commit cffd071

Browse files
committed
refactor(http): [#160] extract functions for percent decoding
1 parent 0dc3050 commit cffd071

File tree

10 files changed

+241
-95
lines changed

10 files changed

+241
-95
lines changed

src/http/filters.rs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::sync::Arc;
77
use warp::{reject, Filter, Rejection};
88

99
use super::error::Error;
10+
use super::percent_encoding::{percent_decode_info_hash, percent_decode_peer_id};
1011
use super::{request, WebResult};
1112
use crate::protocol::common::MAX_SCRAPE_TORRENTS;
1213
use crate::protocol::info_hash::InfoHash;
@@ -78,9 +79,11 @@ fn info_hashes(raw_query: &String) -> WebResult<Vec<InfoHash>> {
7879

7980
for v in split_raw_query {
8081
if v.contains("info_hash") {
82+
// get raw percent encoded infohash
8183
let raw_info_hash = v.split('=').collect::<Vec<&str>>()[1];
82-
let info_hash_bytes = percent_encoding::percent_decode_str(raw_info_hash).collect::<Vec<u8>>();
83-
let info_hash = InfoHash::from_str(&hex::encode(info_hash_bytes));
84+
85+
let info_hash = percent_decode_info_hash(raw_info_hash);
86+
8487
if let Ok(ih) = info_hash {
8588
info_hashes.push(ih);
8689
}
@@ -112,24 +115,17 @@ fn peer_id(raw_query: &String) -> WebResult<peer::Id> {
112115
for v in split_raw_query {
113116
// look for the peer_id param
114117
if v.contains("peer_id") {
115-
// get raw percent_encoded peer_id
118+
// get raw percent encoded peer id
116119
let raw_peer_id = v.split('=').collect::<Vec<&str>>()[1];
117120

118-
// decode peer_id
119-
let peer_id_bytes = percent_encoding::percent_decode_str(raw_peer_id).collect::<Vec<u8>>();
120-
121-
// peer_id must be 20 bytes
122-
if peer_id_bytes.len() != 20 {
121+
if let Ok(id) = percent_decode_peer_id(raw_peer_id) {
122+
peer_id = Some(id);
123+
} else {
123124
return Err(reject::custom(Error::InvalidPeerId {
124125
location: Location::caller(),
125126
}));
126127
}
127128

128-
// clone peer_id_bytes into fixed length array
129-
let mut byte_arr: [u8; 20] = Default::default();
130-
byte_arr.clone_from_slice(peer_id_bytes.as_slice());
131-
132-
peer_id = Some(peer::Id(byte_arr));
133129
break;
134130
}
135131
}

src/http/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod axum;
1515
pub mod error;
1616
pub mod filters;
1717
pub mod handlers;
18+
pub mod percent_encoding;
1819
pub mod request;
1920
pub mod response;
2021
pub mod routes;

src/http/percent_encoding.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use std::str::FromStr;
2+
3+
use crate::protocol::info_hash::InfoHash;
4+
use crate::tracker::peer::{self, IdConversionError};
5+
6+
/// # Errors
7+
///
8+
/// Will return `Err` if if the decoded bytes do not represent a valid `InfoHash`.
9+
pub fn percent_decode_info_hash(raw_info_hash: &str) -> Result<InfoHash, binascii::ConvertError> {
10+
let bytes = percent_encoding::percent_decode_str(raw_info_hash).collect::<Vec<u8>>();
11+
InfoHash::from_str(&hex::encode(bytes))
12+
}
13+
14+
/// # Errors
15+
///
16+
/// Will return `Err` if if the decoded bytes do not represent a valid `peer::Id`.
17+
pub fn percent_decode_peer_id(raw_peer_id: &str) -> Result<peer::Id, IdConversionError> {
18+
let bytes = percent_encoding::percent_decode_str(raw_peer_id).collect::<Vec<u8>>();
19+
peer::Id::try_from(bytes)
20+
}
21+
22+
#[cfg(test)]
23+
mod tests {
24+
use std::str::FromStr;
25+
26+
use crate::http::percent_encoding::{percent_decode_info_hash, percent_decode_peer_id};
27+
use crate::protocol::info_hash::InfoHash;
28+
use crate::tracker::peer;
29+
30+
#[test]
31+
fn it_should_decode_a_percent_encoded_info_hash() {
32+
let encoded_infohash = "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0";
33+
34+
let info_hash = percent_decode_info_hash(encoded_infohash).unwrap();
35+
36+
assert_eq!(
37+
info_hash,
38+
InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap()
39+
);
40+
}
41+
42+
#[test]
43+
fn it_should_fail_decoding_an_invalid_percent_encoded_info_hash() {
44+
let invalid_encoded_infohash = "invalid percent-encoded infohash";
45+
46+
let info_hash = percent_decode_info_hash(invalid_encoded_infohash);
47+
48+
assert!(info_hash.is_err());
49+
}
50+
51+
#[test]
52+
fn it_should_decode_a_percent_encoded_peer_id() {
53+
let encoded_peer_id = "%2DqB00000000000000000";
54+
55+
let peer_id = percent_decode_peer_id(encoded_peer_id).unwrap();
56+
57+
assert_eq!(peer_id, peer::Id(*b"-qB00000000000000000"));
58+
}
59+
60+
#[test]
61+
fn it_should_fail_decoding_an_invalid_percent_encoded_peer_id() {
62+
let invalid_encoded_peer_id = "invalid percent-encoded peer id";
63+
64+
let peer_id = percent_decode_peer_id(invalid_encoded_peer_id);
65+
66+
assert!(peer_id.is_err());
67+
}
68+
}

src/tracker/peer.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use std::net::{IpAddr, SocketAddr};
2+
use std::panic::Location;
23

34
use aquatic_udp_protocol::{AnnounceEvent, NumberOfBytes};
45
use serde;
56
use serde::Serialize;
7+
use thiserror::Error;
68

79
use crate::http::request::Announce;
810
use crate::protocol::clock::{Current, DurationSinceUnixEpoch, Time};
@@ -91,6 +93,61 @@ impl Peer {
9193
#[derive(PartialEq, Eq, Hash, Clone, Debug, PartialOrd, Ord, Copy)]
9294
pub struct Id(pub [u8; 20]);
9395

96+
const INFO_HASH_BYTES_LEN: usize = 20;
97+
98+
#[derive(Error, Debug)]
99+
pub enum IdConversionError {
100+
#[error("not enough bytes for infohash: {message} {location}")]
101+
NotEnoughBytes {
102+
location: &'static Location<'static>,
103+
message: String,
104+
},
105+
#[error("too many bytes for infohash: {message} {location}")]
106+
TooManyBytes {
107+
location: &'static Location<'static>,
108+
message: String,
109+
},
110+
}
111+
112+
impl Id {
113+
/// # Panics
114+
///
115+
/// Will panic if byte slice does not contains the exact amount of bytes need for the `Id`.
116+
#[must_use]
117+
pub fn from_bytes(bytes: &[u8]) -> Self {
118+
assert_eq!(bytes.len(), INFO_HASH_BYTES_LEN);
119+
let mut ret = Id([0u8; INFO_HASH_BYTES_LEN]);
120+
ret.0.clone_from_slice(bytes);
121+
ret
122+
}
123+
}
124+
125+
impl From<[u8; 20]> for Id {
126+
fn from(bytes: [u8; 20]) -> Self {
127+
Id(bytes)
128+
}
129+
}
130+
131+
impl TryFrom<Vec<u8>> for Id {
132+
type Error = IdConversionError;
133+
134+
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
135+
if bytes.len() < INFO_HASH_BYTES_LEN {
136+
return Err(IdConversionError::NotEnoughBytes {
137+
location: Location::caller(),
138+
message: format! {"got {} bytes, expected {}", bytes.len(), INFO_HASH_BYTES_LEN},
139+
});
140+
}
141+
if bytes.len() > INFO_HASH_BYTES_LEN {
142+
return Err(IdConversionError::TooManyBytes {
143+
location: Location::caller(),
144+
message: format! {"got {} bytes, expected {}", bytes.len(), INFO_HASH_BYTES_LEN},
145+
});
146+
}
147+
Ok(Self::from_bytes(&bytes))
148+
}
149+
}
150+
94151
impl std::fmt::Display for Id {
95152
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96153
match self.to_hex_string() {
@@ -239,6 +296,75 @@ mod test {
239296
mod torrent_peer_id {
240297
use crate::tracker::peer;
241298

299+
#[test]
300+
fn should_be_instantiated_from_a_byte_slice() {
301+
let id = peer::Id::from_bytes(&[
302+
0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150,
303+
]);
304+
305+
let expected_id = peer::Id([
306+
0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150,
307+
]);
308+
309+
assert_eq!(id, expected_id);
310+
}
311+
312+
#[test]
313+
#[should_panic]
314+
fn should_fail_trying_to_instantiate_from_a_byte_slice_with_less_than_20_bytes() {
315+
let less_than_20_bytes = [0; 19];
316+
let _ = peer::Id::from_bytes(&less_than_20_bytes);
317+
}
318+
319+
#[test]
320+
#[should_panic]
321+
fn should_fail_trying_to_instantiate_from_a_byte_slice_with_more_than_20_bytes() {
322+
let more_than_20_bytes = [0; 21];
323+
let _ = peer::Id::from_bytes(&more_than_20_bytes);
324+
}
325+
326+
#[test]
327+
fn should_be_converted_from_a_20_byte_array() {
328+
let id = peer::Id::from([
329+
0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150,
330+
]);
331+
332+
let expected_id = peer::Id([
333+
0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150,
334+
]);
335+
336+
assert_eq!(id, expected_id);
337+
}
338+
339+
#[test]
340+
fn should_be_converted_from_a_byte_vector() {
341+
let id = peer::Id::try_from(
342+
[
343+
0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150,
344+
]
345+
.to_vec(),
346+
)
347+
.unwrap();
348+
349+
let expected_id = peer::Id([
350+
0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150, 0, 159, 146, 150,
351+
]);
352+
353+
assert_eq!(id, expected_id);
354+
}
355+
356+
#[test]
357+
#[should_panic]
358+
fn should_fail_trying_to_convert_from_a_byte_vector_with_less_than_20_bytes() {
359+
let _ = peer::Id::try_from([0; 19].to_vec()).unwrap();
360+
}
361+
362+
#[test]
363+
#[should_panic]
364+
fn should_fail_trying_to_convert_from_a_byte_vector_with_more_than_20_bytes() {
365+
let _ = peer::Id::try_from([0; 21].to_vec()).unwrap();
366+
}
367+
242368
#[test]
243369
fn should_be_converted_to_hex_string() {
244370
let id = peer::Id(*b"-qB00000000000000000");

tests/http/bencode.rs

Lines changed: 0 additions & 15 deletions
This file was deleted.

tests/http/mod.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,28 @@
11
pub mod asserts;
2-
pub mod bencode;
32
pub mod client;
43
pub mod connection_info;
54
pub mod requests;
65
pub mod responses;
76
pub mod server;
7+
8+
use percent_encoding::NON_ALPHANUMERIC;
9+
10+
pub type ByteArray20 = [u8; 20];
11+
12+
pub fn percent_encode_byte_array(bytes: &ByteArray20) -> String {
13+
percent_encoding::percent_encode(bytes, NON_ALPHANUMERIC).to_string()
14+
}
15+
16+
pub struct InfoHash(ByteArray20);
17+
18+
impl InfoHash {
19+
pub fn new(vec: &[u8]) -> Self {
20+
let mut byte_array_20: ByteArray20 = Default::default();
21+
byte_array_20.clone_from_slice(vec);
22+
Self(byte_array_20)
23+
}
24+
25+
pub fn bytes(&self) -> ByteArray20 {
26+
self.0
27+
}
28+
}

tests/http/requests/announce.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ use std::fmt;
22
use std::net::{IpAddr, Ipv4Addr};
33
use std::str::FromStr;
44

5-
use percent_encoding::NON_ALPHANUMERIC;
65
use serde_repr::Serialize_repr;
76
use torrust_tracker::protocol::info_hash::InfoHash;
87
use torrust_tracker::tracker::peer::Id;
98

10-
use crate::http::bencode::ByteArray20;
9+
use crate::http::{percent_encode_byte_array, ByteArray20};
1110

1211
pub struct Query {
1312
pub info_hash: ByteArray20,
@@ -211,11 +210,11 @@ impl QueryParams {
211210
let compact = announce_query.compact.as_ref().map(std::string::ToString::to_string);
212211

213212
Self {
214-
info_hash: Some(percent_encoding::percent_encode(&announce_query.info_hash, NON_ALPHANUMERIC).to_string()),
213+
info_hash: Some(percent_encode_byte_array(&announce_query.info_hash)),
215214
peer_addr: Some(announce_query.peer_addr.to_string()),
216215
downloaded: Some(announce_query.downloaded.to_string()),
217216
uploaded: Some(announce_query.uploaded.to_string()),
218-
peer_id: Some(percent_encoding::percent_encode(&announce_query.peer_id, NON_ALPHANUMERIC).to_string()),
217+
peer_id: Some(percent_encode_byte_array(&announce_query.peer_id)),
219218
port: Some(announce_query.port.to_string()),
220219
left: Some(announce_query.left.to_string()),
221220
event,

tests/http/requests/scrape.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
use std::fmt;
22
use std::str::FromStr;
33

4-
use percent_encoding::NON_ALPHANUMERIC;
54
use torrust_tracker::protocol::info_hash::InfoHash;
65

7-
use crate::http::bencode::ByteArray20;
6+
use crate::http::{percent_encode_byte_array, ByteArray20};
87

98
pub struct Query {
109
pub info_hash: Vec<ByteArray20>,
@@ -111,7 +110,7 @@ impl QueryParams {
111110
let info_hashes = scrape_query
112111
.info_hash
113112
.iter()
114-
.map(|info_hash_bytes| percent_encoding::percent_encode(info_hash_bytes, NON_ALPHANUMERIC).to_string())
113+
.map(percent_encode_byte_array)
115114
.collect::<Vec<String>>();
116115

117116
Self { info_hash: info_hashes }

tests/http/responses/scrape.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::str;
44
use serde::{self, Deserialize, Serialize};
55
use serde_bencode::value::Value;
66

7-
use crate::http::bencode::{ByteArray20, InfoHash};
7+
use crate::http::{ByteArray20, InfoHash};
88

99
#[derive(Debug, PartialEq, Default)]
1010
pub struct Response {

0 commit comments

Comments
 (0)