1#![forbid(unsafe_code)]
30#![warn(missing_docs, rust_2018_idioms)]
31
32mod brands;
33mod parser;
34
35pub use brands::Brand;
36pub use parser::ParseError;
37
38use std::fmt;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub enum Platform {
44 Windows,
46 MacOS,
48 Linux,
50 Android,
52 ChromeOS,
54 Unknown,
56}
57
58impl Platform {
59 #[inline]
61 pub const fn as_str(&self) -> &'static str {
62 match self {
63 Platform::Windows => "Windows",
64 Platform::MacOS => "macOS",
65 Platform::Linux => "Linux",
66 Platform::Android => "Android",
67 Platform::ChromeOS => "Chrome OS",
68 Platform::Unknown => "Unknown",
69 }
70 }
71}
72
73impl fmt::Display for Platform {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 write!(f, "\"{}\"", self.as_str())
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85pub struct ClientHints {
86 brand: Brand,
87 major_version: u32,
88 full_version: String,
89 platform: Platform,
90 is_mobile: bool,
91}
92
93impl ClientHints {
94 pub fn from_ua(user_agent: &str) -> Result<Self, ParseError> {
115 parser::parse(user_agent)
116 }
117
118 pub fn new(
130 brand: Brand,
131 major_version: u32,
132 full_version: impl Into<String>,
133 platform: Platform,
134 is_mobile: bool,
135 ) -> Self {
136 Self {
137 brand,
138 major_version,
139 full_version: full_version.into(),
140 platform,
141 is_mobile,
142 }
143 }
144
145 #[inline]
147 pub const fn brand(&self) -> Brand {
148 self.brand
149 }
150
151 #[inline]
153 pub const fn major_version(&self) -> u32 {
154 self.major_version
155 }
156
157 #[inline]
159 pub fn full_version(&self) -> &str {
160 &self.full_version
161 }
162
163 #[inline]
165 pub const fn platform(&self) -> Platform {
166 self.platform
167 }
168
169 #[inline]
171 pub const fn is_mobile(&self) -> bool {
172 self.is_mobile
173 }
174
175 pub fn sec_ch_ua(&self) -> String {
191 brands::generate_sec_ch_ua(self.brand, self.major_version)
192 }
193
194 pub fn sec_ch_ua_full_version_list(&self) -> String {
198 brands::generate_sec_ch_ua_full_version(self.brand, self.major_version, &self.full_version)
199 }
200
201 #[inline]
205 pub const fn sec_ch_ua_mobile(&self) -> &'static str {
206 if self.is_mobile {
207 "?1"
208 } else {
209 "?0"
210 }
211 }
212
213 #[inline]
217 pub fn sec_ch_ua_platform(&self) -> String {
218 format!("\"{}\"", self.platform.as_str())
219 }
220
221 pub fn all_headers(&self) -> (String, &'static str, String) {
225 (
226 self.sec_ch_ua(),
227 self.sec_ch_ua_mobile(),
228 self.sec_ch_ua_platform(),
229 )
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn test_chrome_windows() {
239 let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
240 let hints = ClientHints::from_ua(ua).unwrap();
241
242 assert_eq!(hints.brand(), Brand::Chrome);
243 assert_eq!(hints.major_version(), 120);
244 assert_eq!(hints.platform(), Platform::Windows);
245 assert!(!hints.is_mobile());
246 assert_eq!(hints.sec_ch_ua_mobile(), "?0");
247 }
248
249 #[test]
250 fn test_edge_windows() {
251 let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0";
252 let hints = ClientHints::from_ua(ua).unwrap();
253
254 assert_eq!(hints.brand(), Brand::Edge);
255 assert_eq!(hints.major_version(), 120);
256 assert_eq!(hints.platform(), Platform::Windows);
257 }
258
259 #[test]
260 fn test_brave() {
261 let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Brave/120";
262 let hints = ClientHints::from_ua(ua).unwrap();
263
264 assert_eq!(hints.brand(), Brand::Brave);
265 }
266
267 #[test]
268 fn test_chrome_android() {
269 let ua = "Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36";
270 let hints = ClientHints::from_ua(ua).unwrap();
271
272 assert_eq!(hints.brand(), Brand::Chrome);
273 assert_eq!(hints.platform(), Platform::Android);
274 assert!(hints.is_mobile());
275 assert_eq!(hints.sec_ch_ua_mobile(), "?1");
276 }
277
278 #[test]
279 fn test_chrome_macos() {
280 let ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
281 let hints = ClientHints::from_ua(ua).unwrap();
282
283 assert_eq!(hints.platform(), Platform::MacOS);
284 assert!(!hints.is_mobile());
285 }
286
287 #[test]
288 fn test_chrome_linux() {
289 let ua = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
290 let hints = ClientHints::from_ua(ua).unwrap();
291
292 assert_eq!(hints.platform(), Platform::Linux);
293 }
294
295 #[test]
296 fn test_sec_ch_ua_format() {
297 let hints = ClientHints::new(Brand::Chrome, 120, "120.0.0.0", Platform::Windows, false);
298 let sec_ch_ua = hints.sec_ch_ua();
299
300 assert!(sec_ch_ua.contains("Chromium"));
302 assert!(sec_ch_ua.contains("Google Chrome"));
303 assert!(sec_ch_ua.contains("120"));
304 }
305
306 #[test]
307 fn test_non_chromium_browser() {
308 let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0";
309 let result = ClientHints::from_ua(ua);
310 assert!(result.is_err());
311 }
312
313 #[test]
314 fn test_platform_display() {
315 assert_eq!(Platform::Windows.to_string(), "\"Windows\"");
316 assert_eq!(Platform::MacOS.to_string(), "\"macOS\"");
317 assert_eq!(Platform::Linux.to_string(), "\"Linux\"");
318 }
319}