mas_data_model/
user_agent.rs1use serde::Serialize;
8use woothee::{parser::Parser, woothee::VALUE_UNKNOWN};
9
10#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
11#[serde(rename_all = "snake_case")]
12pub enum DeviceType {
13 Pc,
14 Mobile,
15 Tablet,
16 Unknown,
17}
18
19#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
20pub struct UserAgent {
21 pub name: Option<String>,
22 pub version: Option<String>,
23 pub os: Option<String>,
24 pub os_version: Option<String>,
25 pub model: Option<String>,
26 pub device_type: DeviceType,
27 pub raw: String,
28}
29
30impl std::ops::Deref for UserAgent {
31 type Target = str;
32
33 fn deref(&self) -> &Self::Target {
34 &self.raw
35 }
36}
37
38impl UserAgent {
39 fn parse_custom(user_agent: &str) -> Option<(&str, &str, &str, &str, Option<&str>)> {
40 let regex = regex::Regex::new(r"^(?P<name>[^/]+)/(?P<version>[^ ]+) \((?P<segments>.+)\)$")
41 .unwrap();
42
43 let captures = regex.captures(user_agent)?;
44 let name = captures.name("name")?.as_str();
45 let version = captures.name("version")?.as_str();
46 let segments: Vec<&str> = captures
47 .name("segments")?
48 .as_str()
49 .split(';')
50 .map(str::trim)
51 .collect();
52
53 match segments[..] {
54 ["Linux", "U", os, model, ..] | [model, os, ..] => {
55 let model = model.split_once('/').map_or(model, |(model, _)| model);
57 let model = model.strip_suffix("Build").unwrap_or(model);
60 let model = model.trim();
62
63 let (os, os_version) = if let Some((os, version)) = os.split_once(' ') {
64 (os, Some(version))
65 } else {
66 (os, None)
67 };
68
69 Some((name, version, model, os, os_version))
70 }
71 _ => None,
72 }
73 }
74
75 fn parse_electron(user_agent: &str) -> Option<(&str, &str)> {
76 let regex = regex::Regex::new(r"(?m)\w+/[\w.]+").unwrap();
77 let omit_keys = ["Mozilla", "AppleWebKit", "Chrome", "Electron", "Safari"];
78 return regex
79 .find_iter(user_agent)
80 .map(|caps| caps.as_str().split_once('/').unwrap())
81 .find(|pair| !omit_keys.contains(&pair.0));
82 }
83
84 #[must_use]
85 pub fn parse(user_agent: String) -> Self {
86 if !user_agent.contains("Mozilla/") {
87 if let Some((name, version, model, os, os_version)) =
88 UserAgent::parse_custom(&user_agent)
89 {
90 let mut device_type = DeviceType::Unknown;
91
92 if os == "Android" || os == "iOS" {
94 device_type = DeviceType::Mobile;
95 }
96
97 if model.contains("iPad") {
99 device_type = DeviceType::Tablet;
100 }
101
102 return Self {
103 name: Some(name.to_owned()),
104 version: Some(version.to_owned()),
105 os: Some(os.to_owned()),
106 os_version: os_version.map(std::borrow::ToOwned::to_owned),
107 model: Some(model.to_owned()),
108 device_type,
109 raw: user_agent,
110 };
111 }
112 }
113
114 let mut model = None;
115 let Some(mut result) = Parser::new().parse(&user_agent) else {
116 return Self {
117 raw: user_agent,
118 name: None,
119 version: None,
120 os: None,
121 os_version: None,
122 model: None,
123 device_type: DeviceType::Unknown,
124 };
125 };
126
127 let mut device_type = match result.category {
128 "pc" => DeviceType::Pc,
129 "smartphone" | "mobilephone" => DeviceType::Mobile,
130 _ => DeviceType::Unknown,
131 };
132
133 match (result.os, &*result.os_version) {
136 ("Windows 10", "NT 10.0") if user_agent.contains("Windows NT 10.0; Win64; x64") => {
139 result.os = "Windows";
140 result.os_version = VALUE_UNKNOWN.into();
141 }
142
143 ("Linux", _) if user_agent.contains("X11; Linux x86_64") => {
146 result.os = "Linux";
147 result.os_version = VALUE_UNKNOWN.into();
148 }
149
150 ("ChromeOS", _) if user_agent.contains("X11; CrOS x86_64 14541.0.0") => {
153 result.os = "Chrome OS";
154 result.os_version = VALUE_UNKNOWN.into();
155 }
156
157 ("Android", "10") if user_agent.contains("Linux; Android 10; K") => {
160 result.os = "Android";
161 result.os_version = VALUE_UNKNOWN.into();
162 }
163
164 ("Mac OSX", "10.15.7") if user_agent.contains("Macintosh; Intel Mac OS X 10_15_7") => {
170 result.os = "macOS";
171 result.os_version = VALUE_UNKNOWN.into();
172 }
173
174 ("iPhone" | "iPod", _) => {
177 model = Some(result.os.to_owned());
178 result.os = "iOS";
179 }
180
181 ("iPad", _) => {
182 model = Some(result.os.to_owned());
183 device_type = DeviceType::Tablet;
184 result.os = "iPadOS";
185 }
186
187 ("Mac OSX", _) => {
189 result.os = "macOS";
190 }
191
192 _ => {}
193 }
194
195 if let Some(version) = result.os.strip_prefix("Windows ") {
198 result.os = "Windows";
199 result.os_version = version.into();
200 }
201
202 if user_agent.contains("Electron/") {
204 if let Some(app) = UserAgent::parse_electron(&user_agent) {
205 result.name = app.0;
206 result.version = app.1;
207 }
208 }
209
210 Self {
211 name: (result.name != VALUE_UNKNOWN).then(|| result.name.to_owned()),
212 version: (result.version != VALUE_UNKNOWN).then(|| result.version.to_owned()),
213 os: (result.os != VALUE_UNKNOWN).then(|| result.os.to_owned()),
214 os_version: (result.os_version != VALUE_UNKNOWN)
215 .then(|| result.os_version.into_owned()),
216 device_type,
217 model,
218 raw: user_agent,
219 }
220 }
221}