aquatic_peer_id/
lib.rs

1use std::{borrow::Cow, fmt::Display, sync::OnceLock};
2
3use compact_str::{format_compact, CompactString};
4use regex::bytes::Regex;
5use serde::{Deserialize, Serialize};
6use zerocopy::{AsBytes, FromBytes, FromZeroes};
7
8#[derive(
9    Debug,
10    Clone,
11    Copy,
12    PartialEq,
13    Eq,
14    PartialOrd,
15    Ord,
16    Hash,
17    Serialize,
18    Deserialize,
19    AsBytes,
20    FromBytes,
21    FromZeroes,
22)]
23#[repr(transparent)]
24pub struct PeerId(pub [u8; 20]);
25
26impl PeerId {
27    pub fn client(&self) -> PeerClient {
28        PeerClient::from_peer_id(self)
29    }
30    pub fn first_8_bytes_hex(&self) -> CompactString {
31        let mut buf = [0u8; 16];
32
33        hex::encode_to_slice(&self.0[..8], &mut buf)
34            .expect("PeerId.first_8_bytes_hex buffer too small");
35
36        CompactString::from_utf8_lossy(&buf)
37    }
38}
39
40#[non_exhaustive]
41#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
42pub enum PeerClient {
43    BitTorrent(CompactString),
44    Deluge(CompactString),
45    LibTorrentRakshasa(CompactString),
46    LibTorrentRasterbar(CompactString),
47    QBitTorrent(CompactString),
48    Transmission(CompactString),
49    UTorrent(CompactString),
50    UTorrentEmbedded(CompactString),
51    UTorrentMac(CompactString),
52    UTorrentWeb(CompactString),
53    Vuze(CompactString),
54    WebTorrent(CompactString),
55    WebTorrentDesktop(CompactString),
56    Mainline(CompactString),
57    OtherWithPrefixAndVersion {
58        prefix: CompactString,
59        version: CompactString,
60    },
61    OtherWithPrefix(CompactString),
62    Other,
63}
64
65impl PeerClient {
66    pub fn from_prefix_and_version(prefix: &[u8], version: &[u8]) -> Self {
67        fn three_digits_plus_prerelease(v1: char, v2: char, v3: char, v4: char) -> CompactString {
68            let prerelease: Cow<str> = match v4 {
69                'd' | 'D' => " dev".into(),
70                'a' | 'A' => " alpha".into(),
71                'b' | 'B' => " beta".into(),
72                'r' | 'R' => " rc".into(),
73                's' | 'S' => " stable".into(),
74                other => format_compact!("{}", other).into(),
75            };
76
77            format_compact!("{}.{}.{}{}", v1, v2, v3, prerelease)
78        }
79
80        fn webtorrent(v1: char, v2: char, v3: char, v4: char) -> CompactString {
81            let major = if v1 == '0' {
82                format_compact!("{}", v2)
83            } else {
84                format_compact!("{}{}", v1, v2)
85            };
86
87            let minor = if v3 == '0' {
88                format_compact!("{}", v4)
89            } else {
90                format_compact!("{}{}", v3, v4)
91            };
92
93            format_compact!("{}.{}", major, minor)
94        }
95
96        if let [v1, v2, v3, v4] = version {
97            let (v1, v2, v3, v4) = (*v1 as char, *v2 as char, *v3 as char, *v4 as char);
98
99            match prefix {
100                b"AZ" => Self::Vuze(format_compact!("{}.{}.{}.{}", v1, v2, v3, v4)),
101                b"BT" => Self::BitTorrent(three_digits_plus_prerelease(v1, v2, v3, v4)),
102                b"DE" => Self::Deluge(three_digits_plus_prerelease(v1, v2, v3, v4)),
103                b"lt" => Self::LibTorrentRakshasa(format_compact!("{}.{}{}.{}", v1, v2, v3, v4)),
104                b"LT" => Self::LibTorrentRasterbar(format_compact!("{}.{}{}.{}", v1, v2, v3, v4)),
105                b"qB" => Self::QBitTorrent(format_compact!("{}.{}.{}", v1, v2, v3)),
106                b"TR" => {
107                    let v = match (v1, v2, v3, v4) {
108                        ('0', '0', '0', v4) => format_compact!("0.{}", v4),
109                        ('0', '0', v3, v4) => format_compact!("0.{}{}", v3, v4),
110                        _ => format_compact!("{}.{}{}", v1, v2, v3),
111                    };
112
113                    Self::Transmission(v)
114                }
115                b"UE" => Self::UTorrentEmbedded(three_digits_plus_prerelease(v1, v2, v3, v4)),
116                b"UM" => Self::UTorrentMac(three_digits_plus_prerelease(v1, v2, v3, v4)),
117                b"UT" => Self::UTorrent(three_digits_plus_prerelease(v1, v2, v3, v4)),
118                b"UW" => Self::UTorrentWeb(three_digits_plus_prerelease(v1, v2, v3, v4)),
119                b"WD" => Self::WebTorrentDesktop(webtorrent(v1, v2, v3, v4)),
120                b"WW" => Self::WebTorrent(webtorrent(v1, v2, v3, v4)),
121                _ => Self::OtherWithPrefixAndVersion {
122                    prefix: CompactString::from_utf8_lossy(prefix),
123                    version: CompactString::from_utf8_lossy(version),
124                },
125            }
126        } else {
127            match (prefix, version) {
128                (b"M", &[major, b'-', minor, b'-', patch, b'-']) => Self::Mainline(
129                    format_compact!("{}.{}.{}", major as char, minor as char, patch as char),
130                ),
131                (b"M", &[major, b'-', minor1, minor2, b'-', patch]) => {
132                    Self::Mainline(format_compact!(
133                        "{}.{}{}.{}",
134                        major as char,
135                        minor1 as char,
136                        minor2 as char,
137                        patch as char
138                    ))
139                }
140                _ => Self::OtherWithPrefixAndVersion {
141                    prefix: CompactString::from_utf8_lossy(prefix),
142                    version: CompactString::from_utf8_lossy(version),
143                },
144            }
145        }
146    }
147
148    pub fn from_peer_id(peer_id: &PeerId) -> Self {
149        static AZ_RE: OnceLock<Regex> = OnceLock::new();
150
151        if let Some(caps) = AZ_RE
152            .get_or_init(|| {
153                Regex::new(r"^\-(?P<name>[a-zA-Z]{2})(?P<version>[0-9]{3}[0-9a-zA-Z])")
154                    .expect("compile AZ_RE regex")
155            })
156            .captures(&peer_id.0)
157        {
158            return Self::from_prefix_and_version(&caps["name"], &caps["version"]);
159        }
160
161        static MAINLINE_RE: OnceLock<Regex> = OnceLock::new();
162
163        if let Some(caps) = MAINLINE_RE
164            .get_or_init(|| {
165                Regex::new(r"^(?P<name>[a-zA-Z])(?P<version>[0-9\-]{6})\-")
166                    .expect("compile MAINLINE_RE regex")
167            })
168            .captures(&peer_id.0)
169        {
170            return Self::from_prefix_and_version(&caps["name"], &caps["version"]);
171        }
172
173        static PREFIX_RE: OnceLock<Regex> = OnceLock::new();
174
175        if let Some(caps) = PREFIX_RE
176            .get_or_init(|| {
177                Regex::new(r"^(?P<prefix>[a-zA-Z0-9\-]+)\-").expect("compile PREFIX_RE regex")
178            })
179            .captures(&peer_id.0)
180        {
181            return Self::OtherWithPrefix(CompactString::from_utf8_lossy(&caps["prefix"]));
182        }
183
184        Self::Other
185    }
186}
187
188impl Display for PeerClient {
189    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
190        match self {
191            Self::BitTorrent(v) => write!(f, "BitTorrent {}", v.as_str()),
192            Self::Deluge(v) => write!(f, "Deluge {}", v.as_str()),
193            Self::LibTorrentRakshasa(v) => write!(f, "lt (rakshasa) {}", v.as_str()),
194            Self::LibTorrentRasterbar(v) => write!(f, "lt (rasterbar) {}", v.as_str()),
195            Self::QBitTorrent(v) => write!(f, "QBitTorrent {}", v.as_str()),
196            Self::Transmission(v) => write!(f, "Transmission {}", v.as_str()),
197            Self::UTorrent(v) => write!(f, "µTorrent {}", v.as_str()),
198            Self::UTorrentEmbedded(v) => write!(f, "µTorrent Emb. {}", v.as_str()),
199            Self::UTorrentMac(v) => write!(f, "µTorrent Mac {}", v.as_str()),
200            Self::UTorrentWeb(v) => write!(f, "µTorrent Web {}", v.as_str()),
201            Self::Vuze(v) => write!(f, "Vuze {}", v.as_str()),
202            Self::WebTorrent(v) => write!(f, "WebTorrent {}", v.as_str()),
203            Self::WebTorrentDesktop(v) => write!(f, "WebTorrent Desktop {}", v.as_str()),
204            Self::Mainline(v) => write!(f, "Mainline {}", v.as_str()),
205            Self::OtherWithPrefixAndVersion { prefix, version } => {
206                write!(f, "Other ({}) ({})", prefix.as_str(), version.as_str())
207            }
208            Self::OtherWithPrefix(prefix) => write!(f, "Other ({})", prefix.as_str()),
209            Self::Other => f.write_str("Other"),
210        }
211    }
212}
213
214#[cfg(feature = "quickcheck")]
215impl quickcheck::Arbitrary for PeerId {
216    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
217        let mut bytes = [0u8; 20];
218
219        for byte in bytes.iter_mut() {
220            *byte = u8::arbitrary(g);
221        }
222
223        Self(bytes)
224    }
225}
226
227#[cfg(feature = "quickcheck")]
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    fn create_peer_id(bytes: &[u8]) -> PeerId {
233        let mut peer_id = PeerId([0; 20]);
234
235        let len = bytes.len();
236
237        peer_id.0[..len].copy_from_slice(bytes);
238
239        peer_id
240    }
241
242    #[test]
243    fn test_client_from_peer_id() {
244        assert_eq!(
245            PeerClient::from_peer_id(&create_peer_id(b"-lt1234-k/asdh3")),
246            PeerClient::LibTorrentRakshasa("1.23.4".into())
247        );
248        assert_eq!(
249            PeerClient::from_peer_id(&create_peer_id(b"-DE123s-k/asdh3")),
250            PeerClient::Deluge("1.2.3 stable".into())
251        );
252        assert_eq!(
253            PeerClient::from_peer_id(&create_peer_id(b"-DE123r-k/asdh3")),
254            PeerClient::Deluge("1.2.3 rc".into())
255        );
256        assert_eq!(
257            PeerClient::from_peer_id(&create_peer_id(b"-UT123A-k/asdh3")),
258            PeerClient::UTorrent("1.2.3 alpha".into())
259        );
260        assert_eq!(
261            PeerClient::from_peer_id(&create_peer_id(b"-TR0012-k/asdh3")),
262            PeerClient::Transmission("0.12".into())
263        );
264        assert_eq!(
265            PeerClient::from_peer_id(&create_peer_id(b"-TR1212-k/asdh3")),
266            PeerClient::Transmission("1.21".into())
267        );
268        assert_eq!(
269            PeerClient::from_peer_id(&create_peer_id(b"-WW0102-k/asdh3")),
270            PeerClient::WebTorrent("1.2".into())
271        );
272        assert_eq!(
273            PeerClient::from_peer_id(&create_peer_id(b"-WW1302-k/asdh3")),
274            PeerClient::WebTorrent("13.2".into())
275        );
276        assert_eq!(
277            PeerClient::from_peer_id(&create_peer_id(b"-WW1324-k/asdh3")),
278            PeerClient::WebTorrent("13.24".into())
279        );
280        assert_eq!(
281            PeerClient::from_peer_id(&create_peer_id(b"M1-2-3--k/asdh3")),
282            PeerClient::Mainline("1.2.3".into())
283        );
284        assert_eq!(
285            PeerClient::from_peer_id(&create_peer_id(b"M1-23-4-k/asdh3")),
286            PeerClient::Mainline("1.23.4".into())
287        );
288        assert_eq!(
289            PeerClient::from_peer_id(&create_peer_id(b"S3-k/asdh3")),
290            PeerClient::OtherWithPrefix("S3".into())
291        );
292    }
293}