oxihuman_core/
user_agent_parser.rs1#![allow(dead_code)]
4
5#[derive(Clone, Debug, PartialEq, Eq)]
9pub enum BrowserFamily {
10 Chrome,
11 Firefox,
12 Safari,
13 Edge,
14 Opera,
15 InternetExplorer,
16 Bot,
17 Unknown,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum OsFamily {
23 Windows,
24 MacOs,
25 Linux,
26 Android,
27 Ios,
28 Unknown,
29}
30
31#[derive(Clone, Debug)]
33pub struct UserAgent {
34 pub raw: String,
35 pub browser: BrowserFamily,
36 pub os: OsFamily,
37 pub version: Option<String>,
38 pub is_mobile: bool,
39 pub is_bot: bool,
40}
41
42pub fn parse_user_agent(ua: &str) -> UserAgent {
44 let lower = ua.to_lowercase();
45 let is_bot = is_bot_agent(&lower);
46 let browser = detect_browser(&lower);
47 let os = detect_os(&lower);
48 let is_mobile =
49 lower.contains("mobile") || lower.contains("android") || lower.contains("iphone");
50 let version = extract_version(ua);
51
52 UserAgent {
53 raw: ua.into(),
54 browser,
55 os,
56 version,
57 is_mobile,
58 is_bot,
59 }
60}
61
62fn is_bot_agent(lower: &str) -> bool {
63 lower.contains("bot")
64 || lower.contains("crawler")
65 || lower.contains("spider")
66 || lower.contains("slurp")
67 || lower.contains("googlebot")
68}
69
70fn detect_browser(lower: &str) -> BrowserFamily {
71 if lower.contains("edg/") || lower.contains("edge/") {
72 BrowserFamily::Edge
73 } else if lower.contains("opr/") || lower.contains("opera") {
74 BrowserFamily::Opera
75 } else if lower.contains("firefox") {
76 BrowserFamily::Firefox
77 } else if lower.contains("chrome") {
78 BrowserFamily::Chrome
79 } else if lower.contains("safari") {
80 BrowserFamily::Safari
81 } else if lower.contains("msie") || lower.contains("trident") {
82 BrowserFamily::InternetExplorer
83 } else if lower.contains("bot") || lower.contains("crawler") {
84 BrowserFamily::Bot
85 } else {
86 BrowserFamily::Unknown
87 }
88}
89
90fn detect_os(lower: &str) -> OsFamily {
91 if lower.contains("android") {
92 OsFamily::Android
93 } else if lower.contains("iphone") || lower.contains("ipad") {
94 OsFamily::Ios
95 } else if lower.contains("windows") {
96 OsFamily::Windows
97 } else if lower.contains("mac os") || lower.contains("macos") {
98 OsFamily::MacOs
99 } else if lower.contains("linux") {
100 OsFamily::Linux
101 } else {
102 OsFamily::Unknown
103 }
104}
105
106fn extract_version(ua: &str) -> Option<String> {
107 for prefix in &["Version/", "Firefox/", "Chrome/", "OPR/", "Edg/"] {
109 if let Some(pos) = ua.find(prefix) {
110 let rest = &ua[pos + prefix.len()..];
111 let end = rest
112 .find(|c: char| !c.is_ascii_digit() && c != '.')
113 .unwrap_or(rest.len());
114 if end > 0 {
115 return Some(rest[..end].into());
116 }
117 }
118 }
119 None
120}
121
122pub fn browser_name(family: &BrowserFamily) -> &'static str {
124 match family {
125 BrowserFamily::Chrome => "Chrome",
126 BrowserFamily::Firefox => "Firefox",
127 BrowserFamily::Safari => "Safari",
128 BrowserFamily::Edge => "Edge",
129 BrowserFamily::Opera => "Opera",
130 BrowserFamily::InternetExplorer => "Internet Explorer",
131 BrowserFamily::Bot => "Bot",
132 BrowserFamily::Unknown => "Unknown",
133 }
134}
135
136pub fn os_name(family: &OsFamily) -> &'static str {
138 match family {
139 OsFamily::Windows => "Windows",
140 OsFamily::MacOs => "macOS",
141 OsFamily::Linux => "Linux",
142 OsFamily::Android => "Android",
143 OsFamily::Ios => "iOS",
144 OsFamily::Unknown => "Unknown",
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_detect_firefox() {
154 let ua = parse_user_agent(
155 "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0",
156 );
157 assert_eq!(ua.browser, BrowserFamily::Firefox);
158 }
159
160 #[test]
161 fn test_detect_chrome() {
162 let ua = parse_user_agent("Mozilla/5.0 Chrome/112.0.0.0 Safari/537.36");
163 assert_eq!(ua.browser, BrowserFamily::Chrome);
164 }
165
166 #[test]
167 fn test_detect_edge() {
168 let ua = parse_user_agent("Mozilla/5.0 Edg/112.0.1722.58");
169 assert_eq!(ua.browser, BrowserFamily::Edge);
170 }
171
172 #[test]
173 fn test_detect_windows() {
174 let ua = parse_user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
175 assert_eq!(ua.os, OsFamily::Windows);
176 }
177
178 #[test]
179 fn test_detect_linux() {
180 let ua = parse_user_agent("Mozilla/5.0 (X11; Linux x86_64)");
181 assert_eq!(ua.os, OsFamily::Linux);
182 }
183
184 #[test]
185 fn test_detect_android_mobile() {
186 let ua = parse_user_agent("Mozilla/5.0 (Linux; Android 13) Mobile");
187 assert_eq!(ua.os, OsFamily::Android);
188 assert!(ua.is_mobile);
189 }
190
191 #[test]
192 fn test_detect_bot() {
193 let ua = parse_user_agent("Googlebot/2.1 (+http://www.google.com/bot.html)");
194 assert!(ua.is_bot);
195 assert_eq!(ua.browser, BrowserFamily::Bot);
196 }
197
198 #[test]
199 fn test_extract_firefox_version() {
200 let ua = parse_user_agent("Mozilla/5.0 Firefox/109.0");
201 assert_eq!(ua.version.as_deref(), Some("109.0"));
202 }
203
204 #[test]
205 fn test_browser_name_display() {
206 assert_eq!(browser_name(&BrowserFamily::Safari), "Safari");
207 assert_eq!(os_name(&OsFamily::Ios), "iOS");
208 }
209
210 #[test]
211 fn test_unknown_user_agent() {
212 let ua = parse_user_agent("CustomClient/1.0");
213 assert_eq!(ua.browser, BrowserFamily::Unknown);
214 assert_eq!(ua.os, OsFamily::Unknown);
215 assert!(!ua.is_mobile);
216 assert!(!ua.is_bot);
217 }
218}