torrust_tracker/servers/http/v1/requests/
scrape.rs

1//! `Scrape` request for the HTTP tracker.
2//!
3//! Data structures and logic for parsing the `scrape` request.
4use std::panic::Location;
5
6use thiserror::Error;
7use torrust_tracker_located_error::{Located, LocatedError};
8use torrust_tracker_primitives::info_hash::{self, InfoHash};
9
10use crate::servers::http::percent_encoding::percent_decode_info_hash;
11use crate::servers::http::v1::query::Query;
12use crate::servers::http::v1::responses;
13
14// Query param names
15const INFO_HASH: &str = "info_hash";
16
17#[derive(Debug, PartialEq)]
18pub struct Scrape {
19    pub info_hashes: Vec<InfoHash>,
20}
21
22#[derive(Error, Debug)]
23pub enum ParseScrapeQueryError {
24    #[error("missing query params for scrape request in {location}")]
25    MissingParams { location: &'static Location<'static> },
26    #[error("missing param {param_name} in {location}")]
27    MissingParam {
28        location: &'static Location<'static>,
29        param_name: String,
30    },
31    #[error("invalid param value {param_value} for {param_name} in {source}")]
32    InvalidInfoHashParam {
33        param_name: String,
34        param_value: String,
35        source: LocatedError<'static, info_hash::ConversionError>,
36    },
37}
38
39impl From<ParseScrapeQueryError> for responses::error::Error {
40    fn from(err: ParseScrapeQueryError) -> Self {
41        responses::error::Error {
42            failure_reason: format!("Cannot parse query params for scrape request: {err}"),
43        }
44    }
45}
46
47impl TryFrom<Query> for Scrape {
48    type Error = ParseScrapeQueryError;
49
50    fn try_from(query: Query) -> Result<Self, Self::Error> {
51        Ok(Self {
52            info_hashes: extract_info_hashes(&query)?,
53        })
54    }
55}
56
57fn extract_info_hashes(query: &Query) -> Result<Vec<InfoHash>, ParseScrapeQueryError> {
58    match query.get_param_vec(INFO_HASH) {
59        Some(raw_params) => {
60            let mut info_hashes = vec![];
61
62            for raw_param in raw_params {
63                let info_hash =
64                    percent_decode_info_hash(&raw_param).map_err(|err| ParseScrapeQueryError::InvalidInfoHashParam {
65                        param_name: INFO_HASH.to_owned(),
66                        param_value: raw_param.clone(),
67                        source: Located(err).into(),
68                    })?;
69
70                info_hashes.push(info_hash);
71            }
72
73            Ok(info_hashes)
74        }
75        None => Err(ParseScrapeQueryError::MissingParam {
76            location: Location::caller(),
77            param_name: INFO_HASH.to_owned(),
78        }),
79    }
80}
81
82#[cfg(test)]
83mod tests {
84
85    mod scrape_request {
86
87        use torrust_tracker_primitives::info_hash::InfoHash;
88
89        use crate::servers::http::v1::query::Query;
90        use crate::servers::http::v1::requests::scrape::{Scrape, INFO_HASH};
91
92        #[test]
93        fn should_be_instantiated_from_the_url_query_with_only_one_infohash() {
94            let raw_query = Query::from(vec![(INFO_HASH, "%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0")]).to_string();
95
96            let query = raw_query.parse::<Query>().unwrap();
97
98            let scrape_request = Scrape::try_from(query).unwrap();
99
100            assert_eq!(
101                scrape_request,
102                Scrape {
103                    info_hashes: vec!["3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap()],
104                }
105            );
106        }
107
108        mod when_it_is_instantiated_from_the_url_query_params {
109
110            use crate::servers::http::v1::query::Query;
111            use crate::servers::http::v1::requests::scrape::{Scrape, INFO_HASH};
112
113            #[test]
114            fn it_should_fail_if_the_query_does_not_include_the_info_hash_param() {
115                let raw_query_without_info_hash = "another_param=NOT_RELEVANT";
116
117                assert!(Scrape::try_from(raw_query_without_info_hash.parse::<Query>().unwrap()).is_err());
118            }
119
120            #[test]
121            fn it_should_fail_if_the_info_hash_param_is_invalid() {
122                let raw_query = Query::from(vec![(INFO_HASH, "INVALID_INFO_HASH_VALUE")]).to_string();
123
124                assert!(Scrape::try_from(raw_query.parse::<Query>().unwrap()).is_err());
125            }
126        }
127    }
128}