1use std::{
4 convert::Infallible,
5 fmt::{Display, Formatter, Result as FmtResult},
6 str::FromStr,
7 sync::Arc,
8};
9
10use hyper::Version;
11use links_id::Id;
12use links_normalized::Normalized;
13use serde::{Deserialize, Serialize};
14use tokio_rustls::rustls::{ProtocolVersion, SupportedCipherSuite};
15
16use super::StatisticType;
17
18#[derive(Debug, Clone, Default)]
21pub struct ExtraStatisticInfo {
22 pub tls_sni: Option<Arc<str>>,
24 pub tls_version: Option<ProtocolVersion>,
26 pub tls_cipher_suite: Option<SupportedCipherSuite>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[serde(untagged)]
33pub enum IdOrVanity {
34 Id(Id),
36 Vanity(Normalized),
38}
39
40impl Display for IdOrVanity {
41 fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
42 fmt.write_str(&match self {
43 Self::Id(id) => id.to_string(),
44 Self::Vanity(vanity) => vanity.to_string(),
45 })
46 }
47}
48
49impl From<&str> for IdOrVanity {
50 fn from(s: &str) -> Self {
51 Id::from_str(s).map_or_else(|_| Self::Vanity(Normalized::new(s)), Self::Id)
52 }
53}
54
55impl From<String> for IdOrVanity {
56 fn from(s: String) -> Self {
57 Id::from_str(s.as_str()).map_or_else(|_| Self::Vanity(Normalized::from(s)), Self::Id)
58 }
59}
60
61impl From<Id> for IdOrVanity {
62 fn from(id: Id) -> Self {
63 Self::Id(id)
64 }
65}
66
67impl From<Normalized> for IdOrVanity {
68 fn from(vanity: Normalized) -> Self {
69 Self::Vanity(vanity)
70 }
71}
72
73impl From<&IdOrVanity> for IdOrVanity {
74 fn from(iov: &Self) -> Self {
75 iov.clone()
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(from = "Vec<&str>", into = "Vec<&'static str>")]
82#[non_exhaustive]
83#[expect(
84 clippy::struct_excessive_bools,
85 reason = "using bitflags to save 3 bytes is a bit excessive for this"
86)]
87pub struct StatisticCategories {
88 pub redirect: bool,
90 pub basic: bool,
93 pub protocol: bool,
96 pub user_agent: bool,
100}
101
102impl StatisticCategories {
103 pub const ALL: Self = Self {
105 redirect: true,
106 basic: true,
107 protocol: true,
108 user_agent: true,
109 };
110 pub const NONE: Self = Self {
112 redirect: false,
113 basic: false,
114 protocol: false,
115 user_agent: false,
116 };
117
118 #[must_use]
121 pub const fn specifies(self, stat_type: StatisticType) -> bool {
122 #[allow(clippy::enum_glob_use, reason = "scoped to this 6-line function")]
123 use StatisticType::*;
124
125 match stat_type {
126 Request => self.redirect,
127 HostRequest | SniRequest | StatusCode => self.basic,
128 HttpVersion | TlsVersion | TlsCipherSuite => self.protocol,
129 UserAgent | UserAgentMobile | UserAgentPlatform => self.user_agent,
130 }
131 }
132
133 #[must_use]
149 pub fn to_names(self) -> Vec<&'static str> {
150 let mut names = Vec::with_capacity(4);
151
152 if self.redirect {
153 names.push("redirect");
154 }
155
156 if self.basic {
157 names.push("basic");
158 }
159
160 if self.protocol {
161 names.push("protocol");
162 }
163
164 if self.user_agent {
165 names.push("user-agent");
166 }
167
168 names
169 }
170
171 #[must_use]
186 pub fn from_names<L, T>(categories: L) -> Self
187 where
188 L: AsRef<[T]>,
189 T: AsRef<str>,
190 {
191 let mut cats = Self::NONE;
192
193 for cat in categories.as_ref() {
194 match cat.as_ref() {
195 "redirect" => cats.redirect = true,
196 "basic" => cats.basic = true,
197 "protocol" => cats.protocol = true,
198 "user-agent" => cats.user_agent = true,
199 _ => (),
200 }
201 }
202
203 cats
204 }
205}
206
207impl Default for StatisticCategories {
208 fn default() -> Self {
209 Self {
210 redirect: true,
211 basic: true,
212 protocol: true,
213 user_agent: false,
214 }
215 }
216}
217
218impl From<Vec<&str>> for StatisticCategories {
219 fn from(names: Vec<&str>) -> Self {
220 Self::from_names(names)
221 }
222}
223
224impl From<StatisticCategories> for Vec<&'static str> {
225 fn from(cats: StatisticCategories) -> Self {
226 cats.to_names()
227 }
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
232#[serde(from = "&str")]
233#[non_exhaustive]
234pub enum HttpVersion {
235 V09,
237 V10,
239 V11,
241 V2,
243 V3,
245 Unknown,
247}
248
249impl HttpVersion {
250 #[must_use]
252 pub const fn as_str(self) -> &'static str {
253 match self {
254 HttpVersion::V09 => "HTTP/0.9",
255 HttpVersion::V10 => "HTTP/1.0",
256 HttpVersion::V11 => "HTTP/1.1",
257 HttpVersion::V2 => "HTTP/2",
258 HttpVersion::V3 => "HTTP/3",
259 HttpVersion::Unknown => "HTTP/???",
260 }
261 }
262}
263
264impl FromStr for HttpVersion {
265 type Err = Infallible;
266
267 fn from_str(s: &str) -> Result<Self, Self::Err> {
268 Ok(s.into())
269 }
270}
271
272impl From<&str> for HttpVersion {
273 fn from(s: &str) -> Self {
274 match s.trim_start_matches("HTTP/").trim_start_matches("http/") {
275 "0.9" => Self::V09,
276 "1.0" => Self::V10,
277 "1.1" => Self::V11,
278 "2" => Self::V2,
279 "3" => Self::V3,
280 _ => Self::Unknown,
281 }
282 }
283}
284
285impl Serialize for HttpVersion {
286 fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
287 where
288 S: serde::Serializer,
289 {
290 ser.serialize_str(self.as_str())
291 }
292}
293
294impl From<Version> for HttpVersion {
295 fn from(v: Version) -> Self {
296 match v {
297 Version::HTTP_09 => Self::V09,
298 Version::HTTP_10 => Self::V10,
299 Version::HTTP_11 => Self::V11,
300 Version::HTTP_2 => Self::V2,
301 Version::HTTP_3 => Self::V3,
302 _ => Self::Unknown,
303 }
304 }
305}
306
307impl TryFrom<HttpVersion> for Version {
308 type Error = UnknownHttpVersionError;
309
310 fn try_from(v: HttpVersion) -> Result<Self, Self::Error> {
311 match v {
312 HttpVersion::V09 => Ok(Self::HTTP_09),
313 HttpVersion::V10 => Ok(Self::HTTP_10),
314 HttpVersion::V11 => Ok(Self::HTTP_11),
315 HttpVersion::V2 => Ok(Self::HTTP_2),
316 HttpVersion::V3 => Ok(Self::HTTP_3),
317 HttpVersion::Unknown => Err(UnknownHttpVersionError),
318 }
319 }
320}
321
322#[derive(Debug, Clone, Copy, thiserror::Error)]
325#[error("the HTTP version is unknown")]
326pub struct UnknownHttpVersionError;
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 #[expect(
334 clippy::unnecessary_fallible_conversions,
335 reason = "that's what this test is for"
336 )]
337 fn id_or_vanity() {
338 assert_eq!(
339 IdOrVanity::Id([0x11, 0x33, 0x55, 0x77, 0x99].into()),
340 IdOrVanity::try_from("0fXMgWQz").unwrap()
341 );
342
343 assert_eq!(
344 IdOrVanity::Vanity("example-test".into()),
345 IdOrVanity::try_from("example-test").unwrap()
346 );
347
348 assert_eq!(
349 IdOrVanity::Id([0x11, 0x33, 0x55, 0x77, 0x99].into()),
350 IdOrVanity::from("0fXMgWQz")
351 );
352
353 assert_eq!(
354 IdOrVanity::Vanity("example-test".into()),
355 IdOrVanity::from("example-test")
356 );
357 }
358
359 #[test]
360 fn statistic_categories() {
361 assert_eq!(Vec::<&str>::new(), StatisticCategories::NONE.to_names());
362
363 assert_eq!(
364 StatisticCategories::from_names(Vec::<&str>::new()),
365 StatisticCategories::NONE
366 );
367
368 assert_eq!(
369 vec!["redirect", "basic", "protocol"],
370 StatisticCategories::default().to_names()
371 );
372
373 let names = vec!["protocol", "user-agent"];
374 assert_eq!(names, StatisticCategories::from_names(&names).to_names());
375
376 let names = vec!["protocol", "user-agent"];
377 assert_eq!(
378 names,
379 Vec::<&str>::from(StatisticCategories::from(names.clone()))
380 );
381
382 let categories = StatisticCategories::default();
383 assert!(categories.specifies(StatisticType::Request));
384 assert!(categories.specifies(StatisticType::HostRequest));
385 assert!(categories.specifies(StatisticType::SniRequest));
386 assert!(categories.specifies(StatisticType::HttpVersion));
387 assert!(!categories.specifies(StatisticType::UserAgent));
388 assert!(!categories.specifies(StatisticType::UserAgentPlatform));
389
390 assert_eq!(
391 serde_json::from_str::<StatisticCategories>(r#"["redirect", "basic", "protocol"]"#)
392 .unwrap(),
393 StatisticCategories::default()
394 );
395
396 assert_eq!(
397 serde_json::from_str::<StatisticCategories>(
398 &serde_json::to_string(&StatisticCategories::ALL).unwrap()
399 )
400 .unwrap(),
401 StatisticCategories::ALL
402 );
403 }
404
405 #[test]
406 #[expect(
407 clippy::unnecessary_fallible_conversions,
408 reason = "that's what this test is for"
409 )]
410 fn http_version() {
411 assert_eq!(
412 HttpVersion::from_str("HTTP/1.1").unwrap().as_str(),
413 "HTTP/1.1"
414 );
415
416 assert_eq!(
417 HttpVersion::try_from(HttpVersion::V2.as_str()).unwrap(),
418 HttpVersion::V2
419 );
420
421 assert_eq!(HttpVersion::from(HttpVersion::V2.as_str()), HttpVersion::V2);
422
423 assert!(Version::try_from(HttpVersion::Unknown).is_err());
424
425 assert_eq!(
426 Version::try_from(HttpVersion::V10).unwrap(),
427 Version::HTTP_10
428 );
429 }
430}