mimic_rs/
lib.rs

1//! # mimic-rs
2//!
3//! High-performance User-Agent to Sec-CH-UA Client Hints converter.
4//!
5//! This crate provides functionality to parse User-Agent strings and generate
6//! corresponding Sec-CH-UA (Client Hints) headers as used by Chromium-based browsers.
7//!
8//! ## Features
9//!
10//! - Zero-copy parsing where possible
11//! - Support for Chrome, Edge, Brave, Opera, and other Chromium-based browsers
12//! - Accurate greased brand generation matching Chromium's algorithm
13//! - Platform and mobile detection
14//! - Optional serde support
15//!
16//! ## Example
17//!
18//! ```rust
19//! use mimic_rs::ClientHints;
20//!
21//! 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";
22//! let hints = ClientHints::from_ua(ua).unwrap();
23//!
24//! println!("sec-ch-ua: {}", hints.sec_ch_ua());
25//! println!("sec-ch-ua-mobile: {}", hints.sec_ch_ua_mobile());
26//! println!("sec-ch-ua-platform: {}", hints.sec_ch_ua_platform());
27//! ```
28
29#![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/// Represents the platform/operating system.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub enum Platform {
44    /// Windows operating system
45    Windows,
46    /// macOS operating system
47    MacOS,
48    /// Linux operating system
49    Linux,
50    /// Android operating system
51    Android,
52    /// Chrome OS
53    ChromeOS,
54    /// Unknown platform
55    Unknown,
56}
57
58impl Platform {
59    /// Returns the platform string as used in sec-ch-ua-platform header.
60    #[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/// Client Hints generated from a User-Agent string.
80///
81/// This struct contains all the information needed to construct
82/// Sec-CH-UA headers for HTTP requests.
83#[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    /// Parse a User-Agent string and generate corresponding Client Hints.
95    ///
96    /// # Arguments
97    ///
98    /// * `user_agent` - The User-Agent string to parse
99    ///
100    /// # Returns
101    ///
102    /// Returns `Ok(ClientHints)` if parsing succeeds, or `Err(ParseError)` if
103    /// the User-Agent string cannot be parsed or is not from a Chromium-based browser.
104    ///
105    /// # Example
106    ///
107    /// ```rust
108    /// use mimic_rs::ClientHints;
109    ///
110    /// let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36";
111    /// let hints = ClientHints::from_ua(ua).unwrap();
112    /// assert_eq!(hints.major_version(), 120);
113    /// ```
114    pub fn from_ua(user_agent: &str) -> Result<Self, ParseError> {
115        parser::parse(user_agent)
116    }
117
118    /// Create ClientHints with explicit values.
119    ///
120    /// Use this when you already know the browser details and don't need to parse a UA string.
121    ///
122    /// # Example
123    ///
124    /// ```rust
125    /// use mimic_rs::{ClientHints, Brand, Platform};
126    ///
127    /// let hints = ClientHints::new(Brand::Chrome, 120, "120.0.0.0", Platform::Windows, false);
128    /// ```
129    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    /// Returns the detected browser brand.
146    #[inline]
147    pub const fn brand(&self) -> Brand {
148        self.brand
149    }
150
151    /// Returns the major version number.
152    #[inline]
153    pub const fn major_version(&self) -> u32 {
154        self.major_version
155    }
156
157    /// Returns the full version string.
158    #[inline]
159    pub fn full_version(&self) -> &str {
160        &self.full_version
161    }
162
163    /// Returns the detected platform.
164    #[inline]
165    pub const fn platform(&self) -> Platform {
166        self.platform
167    }
168
169    /// Returns whether this is a mobile browser.
170    #[inline]
171    pub const fn is_mobile(&self) -> bool {
172        self.is_mobile
173    }
174
175    /// Generate the `sec-ch-ua` header value.
176    ///
177    /// This includes the greased brand, Chromium brand, and the actual browser brand
178    /// in a randomized order based on the version number.
179    ///
180    /// # Example
181    ///
182    /// ```rust
183    /// use mimic_rs::ClientHints;
184    ///
185    /// let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36";
186    /// let hints = ClientHints::from_ua(ua).unwrap();
187    /// let sec_ch_ua = hints.sec_ch_ua();
188    /// // Output: "Not A(Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"
189    /// ```
190    pub fn sec_ch_ua(&self) -> String {
191        brands::generate_sec_ch_ua(self.brand, self.major_version)
192    }
193
194    /// Generate the `sec-ch-ua-full-version-list` header value.
195    ///
196    /// Similar to `sec_ch_ua()` but uses the full version string instead of just the major version.
197    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    /// Generate the `sec-ch-ua-mobile` header value.
202    ///
203    /// Returns `?1` for mobile browsers, `?0` for desktop browsers.
204    #[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    /// Generate the `sec-ch-ua-platform` header value.
214    ///
215    /// Returns the platform name in quotes (e.g., `"Windows"`).
216    #[inline]
217    pub fn sec_ch_ua_platform(&self) -> String {
218        format!("\"{}\"", self.platform.as_str())
219    }
220
221    /// Generate all standard Client Hints headers.
222    ///
223    /// Returns a tuple of (sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform).
224    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        // Should contain Chromium and Google Chrome
301        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}