next_web_utils/common/
user_agent.rs

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        // 浏览器识别
29        Self::extract_browser(&mut info, &ua_lower);
30
31        // 操作系统识别
32        Self::extract_os(&mut info, &ua_lower);
33
34        // 设备识别
35        Self::extract_device(&mut info, &ua_lower);
36
37        // 渲染引擎识别
38        Self::extract_engine(&mut info, &ua_lower);
39
40        // 后处理
41        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        // 如果没找到浏览器信息,尝试其他模式
61        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        // 查找版本号通常跟在 '/' 或 ' ' 后面
139        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            // 处理IE/Trident的特殊情况
155            if browser == "trident" {
156                info.browser = Some("internet explorer".to_string());
157                if let Some(version) = &info.browser_version {
158                    // Trident版本到IE版本的映射
159                    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}