1use crate::identity::{Browser, Identity, Os};
2use crate::error::IdentityError;
3
4fn full_version(parts: &[u16]) -> String {
6 parts.iter().map(|v| v.to_string()).collect::<Vec<_>>().join(".")
7}
8
9fn reduced_version(parts: &[u16]) -> String {
17 format!("{}.0.0.0", parts.first().copied().unwrap_or(0))
18}
19
20pub fn parse_chrome_version(ua: &str) -> Option<Vec<u16>> {
29 parse_version_parts(ua.split("Chrome/").nth(1)?.split_whitespace().next()?)
30}
31
32pub fn parse_version_parts(version: &str) -> Option<Vec<u16>> {
40 let parts = version
41 .split('.')
42 .map(|p| p.parse::<u16>().ok())
43 .collect::<Option<Vec<_>>>()?;
44 if parts.is_empty() { None } else { Some(parts) }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::{parse_chrome_version, reduced_version};
50
51 #[test]
52 fn ua_version_is_reduced_to_the_major() {
53 assert_eq!(reduced_version(&[146, 0, 7680, 153]), "146.0.0.0");
56 }
57
58 #[test]
59 fn parses_a_desktop_chrome_ua() {
60 let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
61 (KHTML, like Gecko) Chrome/146.0.7444.60 Safari/537.36";
62 assert_eq!(parse_chrome_version(ua), Some(vec![146, 0, 7444, 60]));
63 }
64
65 #[test]
66 fn parses_an_edge_ua_using_the_chromium_part() {
67 let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \
68 (KHTML, like Gecko) Chrome/146.0.7444.60 Safari/537.36 Edg/146.0.3535.20";
69 assert_eq!(parse_chrome_version(ua), Some(vec![146, 0, 7444, 60]));
70 }
71
72 #[test]
73 fn rejects_a_ua_without_a_chromium_token() {
74 let ua = "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 \
75 (KHTML, like Gecko) Version/17.0 Safari/605.1.15";
76 assert_eq!(parse_chrome_version(ua), None);
77 }
78}
79
80pub fn build_user_agent(identity: &Identity) -> Result<String, IdentityError> {
82 let chrome_ua = reduced_version(&identity.browser_version);
86 let chrome_full = full_version(&identity.browser_version);
87
88 match (&identity.os, &identity.browser) {
89 (Os::Windows, Browser::Chrome) => {
91 let nt_version = windows_nt_version(&identity.os_version);
92 Ok(format!(
93 "Mozilla/5.0 (Windows NT {}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36",
94 nt_version, chrome_ua
95 ))
96 }
97
98 (Os::Macintosh, Browser::Chrome) => {
100 let osx_underscored = identity.os_version.replace('.', "_");
101 Ok(format!(
102 "Mozilla/5.0 (Macintosh; Intel Mac OS X {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36",
103 osx_underscored, chrome_ua
104 ))
105 }
106
107 (Os::Android, Browser::Chrome) => {
109 let device = identity.device_model.as_deref().unwrap_or("Pixel 7");
110 Ok(format!(
111 "Mozilla/5.0 (Linux; Android {}; {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Mobile Safari/537.36",
112 identity.os_version, device, chrome_ua
113 ))
114 }
115
116 (Os::Android, Browser::Edge) => {
118 let device = identity.device_model.as_deref().unwrap_or("Pixel 7");
119 Ok(format!(
120 "Mozilla/5.0 (Linux; Android {}; {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Mobile EdgA/{} Safari/537.36",
121 identity.os_version, device, chrome_ua, chrome_ua
122 ))
123 }
124
125 (Os::Ios, Browser::Safari) => {
127 let os_underscored = identity.os_version.replace('.', "_");
128 let nav_platform = identity.platform.navigator_platform.as_str();
129 Ok(format!(
130 "Mozilla/5.0 ({}; CPU iPhone OS {} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{} Mobile/15E148 Safari/604.1",
131 nav_platform, os_underscored, full_version(&identity.browser_version)
132 ))
133 }
134
135 (Os::Ios, Browser::Chrome) => {
137 let os_underscored = identity.os_version.replace('.', "_");
138 let nav_platform = identity.platform.navigator_platform.as_str();
139 Ok(format!(
140 "Mozilla/5.0 ({}; CPU iPhone OS {} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/{} Mobile/15E148 Safari/604.1",
141 nav_platform, os_underscored, chrome_full
142 ))
143 }
144
145 (Os::Ios, Browser::Edge) => {
147 let os_underscored = identity.os_version.replace('.', "_");
148 let nav_platform = identity.platform.navigator_platform.as_str();
149 Ok(format!(
150 "Mozilla/5.0 ({}; CPU iPhone OS {} like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{} EdgiOS/{} Mobile/15E148 Safari/604.1",
151 nav_platform, os_underscored, full_version(&identity.browser_version), chrome_full
152 ))
153 }
154
155 (Os::Macintosh, Browser::Safari) => {
157 let osx_underscored = identity.os_version.replace('.', "_");
158 Ok(format!(
159 "Mozilla/5.0 (Macintosh; Intel Mac OS X {}) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/{} Safari/605.1.15",
160 osx_underscored, full_version(&identity.browser_version)
161 ))
162 }
163
164 (Os::Linux, Browser::Chrome) => {
166 Ok(format!(
167 "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36",
168 chrome_ua
169 ))
170 }
171
172 (Os::Windows, Browser::Edge) => {
174 let nt_version = windows_nt_version(&identity.os_version);
175 Ok(format!(
176 "Mozilla/5.0 (Windows NT {}; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36 Edg/{}",
177 nt_version, chrome_ua, chrome_ua
178 ))
179 }
180
181 (Os::Macintosh, Browser::Edge) => {
183 let osx_underscored = identity.os_version.replace('.', "_");
184 Ok(format!(
185 "Mozilla/5.0 (Macintosh; Intel Mac OS X {}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{} Safari/537.36 Edg/{}",
186 osx_underscored, chrome_ua, chrome_ua
187 ))
188 }
189
190 (os, browser) => Err(IdentityError::UaError(format!(
191 "unsupported OS/browser combination: {:?}/{:?}",
192 os, browser
193 ))),
194 }
195}
196
197fn windows_nt_version(os_version: &str) -> &str {
199 match os_version {
200 "11" => "10.0",
201 "10" => "10.0",
202 "8.1" => "6.3",
203 "8" => "6.2",
204 "7" => "6.1",
205 _ => "10.0",
206 }
207}