1use std::fmt;
2
3#[derive(Debug, PartialEq, Clone)]
4pub struct UserAgentInfo {
5 pub browser: Option<String>,
6 pub browser_version: Option<String>,
7 pub os: Option<String>,
8 pub os_version: Option<String>,
9 pub device: Option<String>,
10 pub engine: Option<String>,
11 pub engine_version: Option<String>,
12}
13
14impl UserAgentInfo {
15 pub fn parse(ua: &str) -> Self {
16 let mut info = UserAgentInfo {
17 browser: None,
18 browser_version: None,
19 os: None,
20 os_version: None,
21 device: None,
22 engine: None,
23 engine_version: None,
24 };
25
26 let ua_lower = ua.to_lowercase();
27
28 Self::extract_browser(&mut info, &ua_lower);
30
31 Self::extract_os(&mut info, &ua_lower);
33
34 Self::extract_device(&mut info, &ua_lower);
36
37 Self::extract_engine(&mut info, &ua_lower);
39
40 Self::post_process(&mut info);
42
43 info
44 }
45
46 fn extract_browser(info: &mut UserAgentInfo, ua_lower: &str) {
47 let browsers = [
48 "firefox", "chrome", "safari", "opera", "msie", "trident", "edge", "edg",
49 "netscape", "maxthon", "konqueror", "lynx", "ucbrowser",
50 ];
51
52 for browser in browsers {
53 if let Some(pos) = ua_lower.find(browser) {
54 info.browser = Some(browser.to_string());
55 info.browser_version = Self::extract_version(ua_lower, pos + browser.len());
56 break;
57 }
58 }
59
60 if info.browser.is_none() {
62 let bots = [
63 "facebookexternalhit",
64 "twitterbot",
65 "googlebot",
66 "bingbot",
67 "yandexbot",
68 "slurp",
69 "duckduckbot",
70 "baiduspider",
71 ];
72
73 for bot in bots {
74 if ua_lower.contains(bot) {
75 info.browser = Some(bot.to_string());
76 break;
77 }
78 }
79 }
80 }
81
82 fn extract_os(info: &mut UserAgentInfo, ua_lower: &str) {
83 let os_list = [
84 ("windows nt", "windows nt"),
85 ("windows", "windows"),
86 ("mac os x", "mac os x"),
87 ("macintosh", "macintosh"),
88 ("linux", "linux"),
89 ("ubuntu", "ubuntu"),
90 ("android", "android"),
91 ("iphone os", "iphone os"),
92 ("ios", "ios"),
93 ("blackberry", "blackberry"),
94 ("symbianos", "symbianos"),
95 ("webos", "webos"),
96 ];
97
98 for (os_key, os_name) in os_list {
99 if let Some(pos) = ua_lower.find(os_key) {
100 info.os = Some(os_name.to_string());
101 info.os_version = Self::extract_version(ua_lower, pos + os_key.len());
102 break;
103 }
104 }
105 }
106
107 fn extract_device(info: &mut UserAgentInfo, ua_lower: &str) {
108 let devices = [
109 "iphone", "ipad", "ipod", "blackberry", "htc", "samsung", "nokia", "nexus",
110 "kindle", "playbook", "xbox", "playstation", "smart-tv",
111 ];
112
113 for device in devices {
114 if ua_lower.contains(device) {
115 info.device = Some(device.to_string());
116 break;
117 }
118 }
119 }
120
121 fn extract_engine(info: &mut UserAgentInfo, ua_lower: &str) {
122 let engines = [
123 "webkit", "gecko", "trident", "presto", "blink", "khtml"
124 ];
125
126 for engine in engines {
127 if let Some(pos) = ua_lower.find(engine) {
128 info.engine = Some(engine.to_string());
129 info.engine_version = Self::extract_version(ua_lower, pos + engine.len());
130 break;
131 }
132 }
133 }
134
135 fn extract_version(ua_lower: &str, start_pos: usize) -> Option<String> {
136 let remaining = &ua_lower[start_pos..];
137
138 let version_start = remaining.find(|c: char| c == '/' || c == ' ' || c == ';')? + 1;
140 let version_end = remaining[version_start..]
141 .find(|c: char| !(c.is_ascii_digit() || c == '.'))
142 .unwrap_or(remaining.len() - version_start);
143
144 let version_str = &remaining[version_start..version_start + version_end];
145 if !version_str.is_empty() {
146 Some(version_str.to_string())
147 } else {
148 None
149 }
150 }
151
152 fn post_process(info: &mut UserAgentInfo) {
153 if let Some(browser) = &info.browser {
154 if browser == "trident" {
156 info.browser = Some("internet explorer".to_string());
157 if let Some(version) = &info.browser_version {
158 let ie_version = match version.as_str() {
160 "7.0" => "11.0",
161 "6.0" => "10.0",
162 "5.0" => "9.0",
163 "4.0" => "8.0",
164 _ => version.as_str(),
165 };
166 info.browser_version = Some(ie_version.to_string());
167 }
168 } else if browser == "edg" {
169 info.browser = Some("microsoft edge".to_string());
170 }
171 }
172 }
173}
174
175impl fmt::Display for UserAgentInfo {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 write!(
178 f,
179 "Browser: {}\nVersion: {}\nOS: {}\nOS Version: {}\nDevice: {}\nEngine: {}\nEngine Version: {}",
180 self.browser.as_deref().unwrap_or("Unknown"),
181 self.browser_version.as_deref().unwrap_or("Unknown"),
182 self.os.as_deref().unwrap_or("Unknown"),
183 self.os_version.as_deref().unwrap_or("Unknown"),
184 self.device.as_deref().unwrap_or("Unknown"),
185 self.engine.as_deref().unwrap_or("Unknown"),
186 self.engine_version.as_deref().unwrap_or("Unknown"),
187 )
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn test_parse_user_agent() {
197 let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36";
198 let info = UserAgentInfo::parse(ua);
199
200 assert_eq!(info.browser, Some("chrome".to_string()));
201 assert_eq!(info.browser_version, Some("91.0.4472.124".to_string()));
202 assert_eq!(info.os, Some("windows nt".to_string()));
203 assert_eq!(info.os_version, Some("10.0".to_string()));
204 assert_eq!(info.engine, Some("webkit".to_string()));
205 assert_eq!(info.engine_version, Some("537.36".to_string()));
206 }
207
208 #[test]
209 fn test_parse_internet_explorer() {
210 let ua = "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko";
211 let info = UserAgentInfo::parse(ua);
212
213 assert_eq!(info.browser, Some("internet explorer".to_string()));
214 assert_eq!(info.browser_version, Some("11.0".to_string()));
215 assert_eq!(info.os, Some("windows nt".to_string()));
216 assert_eq!(info.os_version, Some("6.1".to_string()));
217 }
218
219 #[test]
220 fn test_parse_safari() {
221 let ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Safari/605.1.15";
222 let info = UserAgentInfo::parse(ua);
223
224 assert_eq!(info.browser, Some("safari".to_string()));
225 assert_eq!(info.browser_version, Some("605.1.15".to_string()));
226 assert_eq!(info.os, Some("mac os x".to_string()));
227 assert_eq!(info.engine, Some("webkit".to_string()));
228 assert_eq!(info.engine_version, Some("605.1.15".to_string()));
229 }
230}