1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! `Scrape` response for the HTTP tracker [`scrape`](crate::servers::http::v1::requests::scrape::Scrape) request.
//!
//! Data structures and logic to build the `scrape` response.
use std::borrow::Cow;

use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use bip_bencode::{ben_int, ben_map, BMutAccess};

use crate::tracker::ScrapeData;

/// The `Scrape` response for the HTTP tracker.
///
/// ```rust
/// use torrust_tracker::servers::http::v1::responses::scrape::Bencoded;
/// use torrust_tracker::shared::bit_torrent::info_hash::InfoHash;
/// use torrust_tracker::tracker::torrent::SwarmMetadata;
/// use torrust_tracker::tracker::ScrapeData;
///
/// let info_hash = InfoHash([0x69; 20]);
/// let mut scrape_data = ScrapeData::empty();
/// scrape_data.add_file(
///     &info_hash,
///     SwarmMetadata {
///         complete: 1,
///         downloaded: 2,
///         incomplete: 3,
///     },
/// );
///
/// let response = Bencoded::from(scrape_data);
///
/// let bytes = response.body();
///
/// // cspell:disable-next-line
/// let expected_bytes = b"d5:filesd20:iiiiiiiiiiiiiiiiiiiid8:completei1e10:downloadedi2e10:incompletei3eeee";
///
/// assert_eq!(
///     String::from_utf8(bytes).unwrap(),
///     String::from_utf8(expected_bytes.to_vec()).unwrap()
/// );
/// ```
#[derive(Debug, PartialEq, Default)]
pub struct Bencoded {
    /// The scrape data to be bencoded.
    scrape_data: ScrapeData,
}

impl Bencoded {
    /// Returns the bencoded representation of the `Scrape` struct.
    ///
    /// # Panics
    ///
    /// Will return an error if it can't access the bencode as a mutable `BDictAccess`.    
    #[must_use]
    pub fn body(&self) -> Vec<u8> {
        let mut scrape_list = ben_map!();

        let scrape_list_mut = scrape_list.dict_mut().unwrap();

        for (info_hash, value) in &self.scrape_data.files {
            scrape_list_mut.insert(
                Cow::from(info_hash.bytes().to_vec()),
                ben_map! {
                    "complete" => ben_int!(i64::from(value.complete)),
                    "downloaded" => ben_int!(i64::from(value.downloaded)),
                    "incomplete" => ben_int!(i64::from(value.incomplete))
                },
            );
        }

        (ben_map! {
            "files" => scrape_list
        })
        .encode()
    }
}

impl From<ScrapeData> for Bencoded {
    fn from(scrape_data: ScrapeData) -> Self {
        Self { scrape_data }
    }
}

impl IntoResponse for Bencoded {
    fn into_response(self) -> Response {
        (StatusCode::OK, self.body()).into_response()
    }
}

#[cfg(test)]
mod tests {

    mod scrape_response {
        use crate::servers::http::v1::responses::scrape::Bencoded;
        use crate::shared::bit_torrent::info_hash::InfoHash;
        use crate::tracker::torrent::SwarmMetadata;
        use crate::tracker::ScrapeData;

        fn sample_scrape_data() -> ScrapeData {
            let info_hash = InfoHash([0x69; 20]);
            let mut scrape_data = ScrapeData::empty();
            scrape_data.add_file(
                &info_hash,
                SwarmMetadata {
                    complete: 1,
                    downloaded: 2,
                    incomplete: 3,
                },
            );
            scrape_data
        }

        #[test]
        fn should_be_converted_from_scrape_data() {
            let response = Bencoded::from(sample_scrape_data());

            assert_eq!(
                response,
                Bencoded {
                    scrape_data: sample_scrape_data()
                }
            );
        }

        #[test]
        fn should_be_bencoded() {
            let response = Bencoded {
                scrape_data: sample_scrape_data(),
            };

            let bytes = response.body();

            // cspell:disable-next-line
            let expected_bytes = b"d5:filesd20:iiiiiiiiiiiiiiiiiiiid8:completei1e10:downloadedi2e10:incompletei3eeee";

            assert_eq!(
                String::from_utf8(bytes).unwrap(),
                String::from_utf8(expected_bytes.to_vec()).unwrap()
            );
        }
    }
}