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

1//! `Scrape` response for the HTTP tracker [`scrape`](crate::servers::http::v1::requests::scrape::Scrape) request.
2//!
3//! Data structures and logic to build the `scrape` response.
4use std::borrow::Cow;
5
6use axum::http::StatusCode;
7use axum::response::{IntoResponse, Response};
8use torrust_tracker_contrib_bencode::{ben_int, ben_map, BMutAccess};
9
10use crate::core::ScrapeData;
11
12/// The `Scrape` response for the HTTP tracker.
13///
14/// ```rust
15/// use torrust_tracker::servers::http::v1::responses::scrape::Bencoded;
16/// use torrust_tracker_primitives::info_hash::InfoHash;
17/// use torrust_tracker_primitives::swarm_metadata::SwarmMetadata;
18/// use torrust_tracker::core::ScrapeData;
19///
20/// let info_hash = InfoHash::from_bytes(&[0x69; 20]);
21/// let mut scrape_data = ScrapeData::empty();
22/// scrape_data.add_file(
23///     &info_hash,
24///     SwarmMetadata {
25///         complete: 1,
26///         downloaded: 2,
27///         incomplete: 3,
28///     },
29/// );
30///
31/// let response = Bencoded::from(scrape_data);
32///
33/// let bytes = response.body();
34///
35/// // cspell:disable-next-line
36/// let expected_bytes = b"d5:filesd20:iiiiiiiiiiiiiiiiiiiid8:completei1e10:downloadedi2e10:incompletei3eeee";
37///
38/// assert_eq!(
39///     String::from_utf8(bytes).unwrap(),
40///     String::from_utf8(expected_bytes.to_vec()).unwrap()
41/// );
42/// ```
43#[derive(Debug, PartialEq, Default)]
44pub struct Bencoded {
45    /// The scrape data to be bencoded.
46    scrape_data: ScrapeData,
47}
48
49impl Bencoded {
50    /// Returns the bencoded representation of the `Scrape` struct.
51    ///
52    /// # Panics
53    ///
54    /// Will return an error if it can't access the bencode as a mutable `BDictAccess`.    
55    #[must_use]
56    pub fn body(&self) -> Vec<u8> {
57        let mut scrape_list = ben_map!();
58
59        let scrape_list_mut = scrape_list.dict_mut().unwrap();
60
61        for (info_hash, value) in &self.scrape_data.files {
62            scrape_list_mut.insert(
63                Cow::from(info_hash.bytes().to_vec()),
64                ben_map! {
65                    "complete" => ben_int!(i64::from(value.complete)),
66                    "downloaded" => ben_int!(i64::from(value.downloaded)),
67                    "incomplete" => ben_int!(i64::from(value.incomplete))
68                },
69            );
70        }
71
72        (ben_map! {
73            "files" => scrape_list
74        })
75        .encode()
76    }
77}
78
79impl From<ScrapeData> for Bencoded {
80    fn from(scrape_data: ScrapeData) -> Self {
81        Self { scrape_data }
82    }
83}
84
85impl IntoResponse for Bencoded {
86    fn into_response(self) -> Response {
87        (StatusCode::OK, self.body()).into_response()
88    }
89}
90
91#[cfg(test)]
92mod tests {
93
94    mod scrape_response {
95        use torrust_tracker_primitives::info_hash::InfoHash;
96        use torrust_tracker_primitives::swarm_metadata::SwarmMetadata;
97
98        use crate::core::ScrapeData;
99        use crate::servers::http::v1::responses::scrape::Bencoded;
100
101        fn sample_scrape_data() -> ScrapeData {
102            let info_hash = InfoHash::from_bytes(&[0x69; 20]);
103            let mut scrape_data = ScrapeData::empty();
104            scrape_data.add_file(
105                &info_hash,
106                SwarmMetadata {
107                    complete: 1,
108                    downloaded: 2,
109                    incomplete: 3,
110                },
111            );
112            scrape_data
113        }
114
115        #[test]
116        fn should_be_converted_from_scrape_data() {
117            let response = Bencoded::from(sample_scrape_data());
118
119            assert_eq!(
120                response,
121                Bencoded {
122                    scrape_data: sample_scrape_data()
123                }
124            );
125        }
126
127        #[test]
128        fn should_be_bencoded() {
129            let response = Bencoded {
130                scrape_data: sample_scrape_data(),
131            };
132
133            let bytes = response.body();
134
135            // cspell:disable-next-line
136            let expected_bytes = b"d5:filesd20:iiiiiiiiiiiiiiiiiiiid8:completei1e10:downloadedi2e10:incompletei3eeee";
137
138            assert_eq!(
139                String::from_utf8(bytes).unwrap(),
140                String::from_utf8(expected_bytes.to_vec()).unwrap()
141            );
142        }
143    }
144}