1use std::collections::HashMap;
4
5use once_cell::sync::Lazy;
6use regex::Regex;
7use relay_event_schema::protocol::{
8 BrowserContext, Context, Cookies, OsContext, ResponseContext, RuntimeContext,
9};
10use relay_protocol::{Annotated, Empty, Value};
11
12static OS_WINDOWS_REGEX1: Lazy<Regex> = Lazy::new(|| {
14 Regex::new(r"^(Microsoft\s+)?Windows\s+(NT\s+)?(?P<version>\d+\.\d+\.(?P<build_number>\d+)).*$")
15 .unwrap()
16});
17static OS_WINDOWS_REGEX2: Lazy<Regex> = Lazy::new(|| {
18 Regex::new(r"^Windows\s+\d+\s+\((?P<version>\d+\.\d+\.(?P<build_number>\d+)).*$").unwrap()
19});
20
21static OS_ANDROID_REGEX: Lazy<Regex> = Lazy::new(|| {
22 Regex::new(r"^Android (OS )?(?P<version>\d+(\.\d+){0,2}) / API-(?P<api>(\d+))").unwrap()
23});
24
25static OS_MACOS_REGEX: Lazy<Regex> = Lazy::new(|| {
27 Regex::new(r"^Mac OS X (?P<version>\d+\.\d+\.\d+)( \((?P<build>[a-fA-F0-9]+)\))?$").unwrap()
28});
29
30static OS_IOS_REGEX: Lazy<Regex> =
32 Lazy::new(|| Regex::new(r"^iOS (?P<version>\d+\.\d+\.\d+)").unwrap());
33
34static OS_IPADOS_REGEX: Lazy<Regex> =
36 Lazy::new(|| Regex::new(r"^iPadOS (?P<version>\d+\.\d+\.\d+)").unwrap());
37
38static OS_LINUX_DISTRO_UNAME_REGEX: Lazy<Regex> = Lazy::new(|| {
40 Regex::new(r"^Linux (?P<kernel_version>\d+\.\d+(\.\d+(\.[1-9]+)?)?) (?P<name>[a-zA-Z]+) (?P<version>\d+(\.\d+){0,2})").unwrap()
41});
42
43static OS_UNAME_REGEX: Lazy<Regex> = Lazy::new(|| {
46 Regex::new(r"^(?P<name>[a-zA-Z]+) (?P<kernel_version>\d+\.\d+(\.\d+(\.[1-9]+)?)?)").unwrap()
47});
48
49static RUNTIME_DOTNET_REGEX: Lazy<Regex> =
51 Lazy::new(|| Regex::new(r"^(?P<name>.*) (?P<version>\d+\.\d+(\.\d+){0,2}).*$").unwrap());
52
53static ANDROID_MODEL_NAMES: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
56 let mut map = HashMap::new();
57 let android_str = include_str!("android_models.csv");
59
60 let mut lines = android_str.lines();
61
62 let header = lines.next().expect("CSV file should have a header");
63
64 let header_fields: Vec<&str> = header.split(',').collect();
65 let model_index = header_fields.iter().position(|&s| s.trim() == "Model");
66 let product_name_index = header_fields
67 .iter()
68 .position(|&s| s.trim() == "Marketing Name");
69
70 let (model_index, product_name_index) = match (model_index, product_name_index) {
71 (Some(model_index), Some(product_name_index)) => (model_index, product_name_index),
72 (_, _) => {
73 relay_log::error!(
74 "failed to find model and/or marketing name headers for android-model map",
75 );
76
77 return HashMap::new();
78 }
79 };
80
81 for line in lines {
82 let fields: Vec<&str> = line.split(',').collect();
83 if fields.len() > std::cmp::max(model_index, product_name_index) {
84 map.insert(
85 fields[model_index].trim(),
86 fields[product_name_index].trim(),
87 );
88 }
89 }
90 map
91});
92
93fn normalize_runtime_context(runtime: &mut RuntimeContext) {
94 if runtime.name.value().is_empty() && runtime.version.value().is_empty() {
95 if let Some(raw_description) = runtime.raw_description.as_str() {
96 if let Some(captures) = RUNTIME_DOTNET_REGEX.captures(raw_description) {
97 runtime.name = captures.name("name").map(|m| m.as_str().to_owned()).into();
98 runtime.version = captures
99 .name("version")
100 .map(|m| m.as_str().to_owned())
101 .into();
102 }
103 }
104 }
105
106 if let Some(name) = runtime.name.as_str() {
110 if let Some(build) = runtime.build.as_str() {
111 if name.starts_with(".NET Framework") {
112 let version = match build {
113 "378389" => Some("4.5".to_owned()),
114 "378675" => Some("4.5.1".to_owned()),
115 "378758" => Some("4.5.1".to_owned()),
116 "379893" => Some("4.5.2".to_owned()),
117 "393295" => Some("4.6".to_owned()),
118 "393297" => Some("4.6".to_owned()),
119 "394254" => Some("4.6.1".to_owned()),
120 "394271" => Some("4.6.1".to_owned()),
121 "394802" => Some("4.6.2".to_owned()),
122 "394806" => Some("4.6.2".to_owned()),
123 "460798" => Some("4.7".to_owned()),
124 "460805" => Some("4.7".to_owned()),
125 "461308" => Some("4.7.1".to_owned()),
126 "461310" => Some("4.7.1".to_owned()),
127 "461808" => Some("4.7.2".to_owned()),
128 "461814" => Some("4.7.2".to_owned()),
129 "528040" => Some("4.8".to_owned()),
130 "528049" => Some("4.8".to_owned()),
131 "528209" => Some("4.8".to_owned()),
132 "528372" => Some("4.8".to_owned()),
133 "528449" => Some("4.8".to_owned()),
134 _ => None,
135 };
136
137 if let Some(version) = version {
138 runtime.version = version.into();
139 }
140 }
141 }
142 }
143
144 if runtime.runtime.value().is_none() {
147 if let (Some(name), Some(version)) = (runtime.name.value(), runtime.version.value()) {
148 runtime.runtime = Annotated::from(format!("{name} {version}"));
149 }
150 }
151}
152
153fn get_windows_version(description: &str) -> Option<(&str, &str)> {
158 let captures = OS_WINDOWS_REGEX1
159 .captures(description)
160 .or_else(|| OS_WINDOWS_REGEX2.captures(description))?;
161
162 let full_version = captures.name("version")?.as_str();
163 let build_number_str = captures.name("build_number")?.as_str();
164 let build_number = build_number_str.parse::<u64>().ok()?;
165
166 let version_name = match build_number {
167 2600..=3790 => "XP",
169 6002 => "Vista",
170 7601 => "7",
171 9200 => "8",
172 9600 => "8.1",
173 10240..=19044 => "10",
174 22000..=22999 => "11",
175 _ => full_version,
177 };
178
179 Some((version_name, build_number_str))
180}
181
182fn get_marketing_name(description: &str) -> Option<(&str, &str)> {
184 let (name, version) = description.split_once(' ')?;
185 let name = name.trim();
186 let version = version.trim();
187
188 if name.bytes().any(|c| !c.is_ascii_alphabetic()) {
190 return None;
191 }
192
193 if version
195 .bytes()
196 .any(|c| !matches!(c, b'0'..=b'9' | b'.' | b'-'))
197 {
198 return None;
199 }
200
201 Some((name, version))
202}
203
204#[allow(dead_code)]
205pub fn get_android_api_version(description: &str) -> Option<&str> {
209 if let Some(captures) = OS_ANDROID_REGEX.captures(description) {
210 captures.name("api").map(|m| m.as_str())
211 } else {
212 None
213 }
214}
215
216fn normalize_os_context(os: &mut OsContext) {
217 if os.name.value().is_some() || os.version.value().is_some() {
218 compute_os_context(os);
219 return;
220 }
221
222 if let Some(raw_description) = os.raw_description.as_str() {
223 if let Some((version, build_number)) = get_windows_version(raw_description) {
224 os.name = "Windows".to_owned().into();
225 os.version = version.to_owned().into();
226 if os.build.is_empty() {
227 os.build.set_value(Some(build_number.to_owned().into()));
229 }
230 } else if let Some(captures) = OS_MACOS_REGEX.captures(raw_description) {
231 os.name = "macOS".to_owned().into();
232 os.version = captures
233 .name("version")
234 .map(|m| m.as_str().to_owned())
235 .into();
236 os.build = captures
237 .name("build")
238 .map(|m| m.as_str().to_owned().into())
239 .into();
240 } else if let Some(captures) = OS_IOS_REGEX.captures(raw_description) {
241 os.name = "iOS".to_owned().into();
242 os.version = captures
243 .name("version")
244 .map(|m| m.as_str().to_owned())
245 .into();
246 } else if let Some(captures) = OS_IPADOS_REGEX.captures(raw_description) {
247 os.name = "iPadOS".to_owned().into();
248 os.version = captures
249 .name("version")
250 .map(|m| m.as_str().to_owned())
251 .into();
252 } else if let Some(captures) = OS_LINUX_DISTRO_UNAME_REGEX.captures(raw_description) {
253 os.name = captures.name("name").map(|m| m.as_str().to_owned()).into();
254 os.version = captures
255 .name("version")
256 .map(|m| m.as_str().to_owned())
257 .into();
258 os.kernel_version = captures
259 .name("kernel_version")
260 .map(|m| m.as_str().to_owned())
261 .into();
262 } else if let Some(captures) = OS_UNAME_REGEX.captures(raw_description) {
263 os.name = captures.name("name").map(|m| m.as_str().to_owned()).into();
264 os.kernel_version = captures
265 .name("kernel_version")
266 .map(|m| m.as_str().to_owned())
267 .into();
268 } else if let Some(captures) = OS_ANDROID_REGEX.captures(raw_description) {
269 os.name = "Android".to_owned().into();
270 os.version = captures
271 .name("version")
272 .map(|m| m.as_str().to_owned())
273 .into();
274 } else if raw_description == "Nintendo Switch" {
275 os.name = "Nintendo OS".to_owned().into();
276 } else if let Some((name, version)) = get_marketing_name(raw_description) {
277 os.name = name.to_owned().into();
278 os.version = version.to_owned().into();
279 }
280 }
281
282 compute_os_context(os);
283}
284
285fn compute_os_context(os: &mut OsContext) {
286 if os.os.value().is_none() {
289 if let (Some(name), Some(version)) = (os.name.value(), os.version.value()) {
290 os.os = Annotated::from(format!("{name} {version}"));
291 }
292 }
293}
294
295fn normalize_browser_context(browser: &mut BrowserContext) {
296 if browser.browser.value().is_none() {
299 if let (Some(name), Some(version)) = (browser.name.value(), browser.version.value()) {
300 browser.browser = Annotated::from(format!("{name} {version}"));
301 }
302 }
303}
304
305fn parse_raw_response_data(response: &ResponseContext) -> Option<(&'static str, Value)> {
306 let raw = response.data.as_str()?;
307
308 serde_json::from_str(raw)
309 .ok()
310 .map(|value| ("application/json", value))
311}
312
313fn normalize_response_data(response: &mut ResponseContext) {
314 if let Some((content_type, parsed_data)) = parse_raw_response_data(response) {
319 response.data.set_value(Some(parsed_data));
322 response.inferred_content_type = Annotated::from(content_type.to_owned());
323 } else {
324 response.inferred_content_type = response
325 .headers
326 .value()
327 .and_then(|headers| headers.get_header("Content-Type"))
328 .map(|value| value.split(';').next().unwrap_or(value).to_owned())
329 .into();
330 }
331}
332
333fn normalize_response(response: &mut ResponseContext) {
334 normalize_response_data(response);
335
336 let headers = match response.headers.value_mut() {
337 Some(headers) => headers,
338 None => return,
339 };
340
341 if response.cookies.value().is_some() {
342 headers.remove("Set-Cookie");
343 return;
344 }
345
346 let cookie_header = match headers.get_header("Set-Cookie") {
347 Some(header) => header,
348 None => return,
349 };
350
351 if let Ok(new_cookies) = Cookies::parse(cookie_header) {
352 response.cookies = Annotated::from(new_cookies);
353 headers.remove("Set-Cookie");
354 }
355}
356
357pub fn normalize_context(context: &mut Context) {
359 match context {
360 Context::Runtime(runtime) => normalize_runtime_context(runtime),
361 Context::Os(os) => normalize_os_context(os),
362 Context::Browser(browser) => normalize_browser_context(browser),
363 Context::Response(response) => normalize_response(response),
364 Context::Device(device) => {
365 if let Some(product_name) = device
366 .as_ref()
367 .model
368 .value()
369 .and_then(|model| ANDROID_MODEL_NAMES.get(model.as_str()))
370 {
371 device.name.set_value(Some(product_name.to_string()))
372 }
373 }
374 _ => {}
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use relay_event_schema::protocol::{Headers, LenientString, PairList};
381 use relay_protocol::Object;
382 use similar_asserts::assert_eq;
383
384 use super::*;
385
386 #[test]
387 fn test_get_product_name() {
388 assert_eq!(
389 ANDROID_MODEL_NAMES.get("NE2211").unwrap(),
390 &"OnePlus 10 Pro 5G"
391 );
392
393 assert_eq!(
394 ANDROID_MODEL_NAMES.get("MP04").unwrap(),
395 &"A13 Pro Max 5G EEA"
396 );
397
398 assert_eq!(ANDROID_MODEL_NAMES.get("ZT216_7").unwrap(), &"zyrex");
399
400 assert!(ANDROID_MODEL_NAMES.get("foobar").is_none());
401 }
402
403 #[test]
404 fn test_dotnet_framework_48_without_build_id() {
405 let mut runtime = RuntimeContext {
406 raw_description: ".NET Framework 4.8.4250.0".to_owned().into(),
407 ..RuntimeContext::default()
408 };
409
410 normalize_runtime_context(&mut runtime);
411 assert_eq!(Some(".NET Framework"), runtime.name.as_str());
412 assert_eq!(Some("4.8.4250.0"), runtime.version.as_str());
413 }
414
415 #[test]
416 fn test_dotnet_framework_472() {
417 let mut runtime = RuntimeContext {
418 raw_description: ".NET Framework 4.7.3056.0".to_owned().into(),
419 build: LenientString("461814".to_owned()).into(),
420 ..RuntimeContext::default()
421 };
422
423 normalize_runtime_context(&mut runtime);
424 assert_eq!(Some(".NET Framework"), runtime.name.as_str());
425 assert_eq!(Some("4.7.2"), runtime.version.as_str());
426 }
427
428 #[test]
429 fn test_dotnet_framework_future_version() {
430 let mut runtime = RuntimeContext {
431 raw_description: ".NET Framework 200.0".to_owned().into(),
432 build: LenientString("999999".to_owned()).into(),
433 ..RuntimeContext::default()
434 };
435
436 normalize_runtime_context(&mut runtime);
438 assert_eq!(Some(".NET Framework"), runtime.name.as_str());
439 assert_eq!(Some("200.0"), runtime.version.as_str());
440 }
441
442 #[test]
443 fn test_dotnet_native() {
444 let mut runtime = RuntimeContext {
445 raw_description: ".NET Native 2.0".to_owned().into(),
446 ..RuntimeContext::default()
447 };
448
449 normalize_runtime_context(&mut runtime);
450 assert_eq!(Some(".NET Native"), runtime.name.as_str());
451 assert_eq!(Some("2.0"), runtime.version.as_str());
452 }
453
454 #[test]
455 fn test_dotnet_core() {
456 let mut runtime = RuntimeContext {
457 raw_description: ".NET Core 2.0".to_owned().into(),
458 ..RuntimeContext::default()
459 };
460
461 normalize_runtime_context(&mut runtime);
462 assert_eq!(Some(".NET Core"), runtime.name.as_str());
463 assert_eq!(Some("2.0"), runtime.version.as_str());
464 }
465
466 #[test]
467 fn test_windows_7_or_server_2008() {
468 let mut os = OsContext {
470 raw_description: "Microsoft Windows NT 6.1.7601 Service Pack 1"
471 .to_owned()
472 .into(),
473 ..OsContext::default()
474 };
475
476 normalize_os_context(&mut os);
477 assert_eq!(Some("Windows"), os.name.as_str());
478 assert_eq!(Some("7"), os.version.as_str());
479 }
480
481 #[test]
482 fn test_windows_8_or_server_2012_or_later() {
483 let mut os = OsContext {
488 raw_description: "Microsoft Windows NT 6.2.9200.0".to_owned().into(),
489 ..OsContext::default()
490 };
491
492 normalize_os_context(&mut os);
493 assert_eq!(Some("Windows"), os.name.as_str());
494 assert_eq!(Some("8"), os.version.as_str());
495 }
496
497 #[test]
498 fn test_windows_10() {
499 let mut os = OsContext {
502 raw_description: "Microsoft Windows 10.0.16299".to_owned().into(),
503 ..OsContext::default()
504 };
505
506 normalize_os_context(&mut os);
507 assert_eq!(Some("Windows"), os.name.as_str());
508 assert_eq!(Some("10"), os.version.as_str());
509 }
510
511 #[test]
512 fn test_windows_11() {
513 let mut os = OsContext {
515 raw_description: "Microsoft Windows 10.0.22000".to_owned().into(),
516 ..OsContext::default()
517 };
518
519 normalize_os_context(&mut os);
520 assert_eq!(Some("Windows"), os.name.as_str());
521 assert_eq!(Some("11"), os.version.as_str());
522 }
523
524 #[test]
525 fn test_windows_11_future1() {
526 let mut os = OsContext {
528 raw_description: "Microsoft Windows 10.0.22001".to_owned().into(),
529 ..OsContext::default()
530 };
531
532 normalize_os_context(&mut os);
533 assert_eq!(Some("Windows"), os.name.as_str());
534 assert_eq!(Some("11"), os.version.as_str());
535 }
536
537 #[test]
538 fn test_windows_11_future2() {
539 let mut os = OsContext {
541 raw_description: "Microsoft Windows 10.1.23456".to_owned().into(),
542 ..OsContext::default()
543 };
544
545 normalize_os_context(&mut os);
546 assert_eq!(Some("Windows"), os.name.as_str());
547 assert_eq!(Some("10.1.23456"), os.version.as_str());
548 }
549
550 #[test]
551 fn test_macos_os_version() {
552 let mut os = OsContext {
554 raw_description: "Unix 17.5.0.0".to_owned().into(),
555 ..OsContext::default()
556 };
557
558 normalize_os_context(&mut os);
559 assert_eq!(Some("Unix"), os.name.as_str());
560 assert_eq!(Some("17.5.0"), os.kernel_version.as_str());
561 }
562
563 #[test]
564 fn test_macos_runtime() {
565 let mut os = OsContext {
567 raw_description: "Darwin 17.5.0 Darwin Kernel Version 17.5.0: Mon Mar 5 22:24:32 PST 2018; root:xnu-4570.51.1~1/RELEASE_X86_64".to_owned().into(),
568 ..OsContext::default()
569 };
570
571 normalize_os_context(&mut os);
572 assert_eq!(Some("Darwin"), os.name.as_str());
573 assert_eq!(Some("17.5.0"), os.kernel_version.as_str());
574 }
575
576 #[test]
577 fn test_centos_os_version() {
578 let mut os = OsContext {
580 raw_description: "Unix 3.10.0.693".to_owned().into(),
581 ..OsContext::default()
582 };
583
584 normalize_os_context(&mut os);
585 assert_eq!(Some("Unix"), os.name.as_str());
586 assert_eq!(Some("3.10.0.693"), os.kernel_version.as_str());
587 }
588
589 #[test]
590 fn test_centos_runtime_info() {
591 let mut os = OsContext {
593 raw_description: "Linux 3.10.0-693.21.1.el7.x86_64 #1 SMP Wed Mar 7 19:03:37 UTC 2018"
594 .to_owned()
595 .into(),
596 ..OsContext::default()
597 };
598
599 normalize_os_context(&mut os);
600 assert_eq!(Some("Linux"), os.name.as_str());
601 assert_eq!(Some("3.10.0"), os.kernel_version.as_str());
602 }
603
604 #[test]
605 fn test_wsl_ubuntu() {
606 let mut os = OsContext {
609 raw_description: "Linux 4.4.0-43-Microsoft #1-Microsoft Wed Dec 31 14:42:53 PST 2014"
610 .to_owned()
611 .into(),
612 ..OsContext::default()
613 };
614
615 normalize_os_context(&mut os);
616 assert_eq!(Some("Linux"), os.name.as_str());
617 assert_eq!(Some("4.4.0"), os.kernel_version.as_str());
618 }
619
620 #[test]
621 fn test_macos_with_build() {
622 let mut os = OsContext {
623 raw_description: "Mac OS X 10.14.2 (18C54)".to_owned().into(),
624 ..OsContext::default()
625 };
626
627 normalize_os_context(&mut os);
628 assert_eq!(Some("macOS"), os.name.as_str());
629 assert_eq!(Some("10.14.2"), os.version.as_str());
630 assert_eq!(Some("18C54"), os.build.as_str());
631 }
632
633 #[test]
634 fn test_macos_without_build() {
635 let mut os = OsContext {
636 raw_description: "Mac OS X 10.14.2".to_owned().into(),
637 ..OsContext::default()
638 };
639
640 normalize_os_context(&mut os);
641 assert_eq!(Some("macOS"), os.name.as_str());
642 assert_eq!(Some("10.14.2"), os.version.as_str());
643 assert_eq!(None, os.build.value());
644 }
645
646 #[test]
647 fn test_name_not_overwritten() {
648 let mut os = OsContext {
649 name: "Properly defined name".to_owned().into(),
650 raw_description: "Linux 4.4.0".to_owned().into(),
651 ..OsContext::default()
652 };
653
654 normalize_os_context(&mut os);
655 assert_eq!(Some("Properly defined name"), os.name.as_str());
656 }
657
658 #[test]
659 fn test_version_not_overwritten() {
660 let mut os = OsContext {
661 version: "Properly defined version".to_owned().into(),
662 raw_description: "Linux 4.4.0".to_owned().into(),
663 ..OsContext::default()
664 };
665
666 normalize_os_context(&mut os);
667 assert_eq!(Some("Properly defined version"), os.version.as_str());
668 }
669
670 #[test]
671 fn test_no_name() {
672 let mut os = OsContext::default();
673
674 normalize_os_context(&mut os);
675 assert_eq!(None, os.name.value());
676 assert_eq!(None, os.version.value());
677 assert_eq!(None, os.kernel_version.value());
678 assert_eq!(None, os.raw_description.value());
679 }
680
681 #[test]
682 fn test_unity_mac_os() {
683 let mut os = OsContext {
684 raw_description: "Mac OS X 10.16.0".to_owned().into(),
685 ..OsContext::default()
686 };
687 normalize_os_context(&mut os);
688 assert_eq!(Some("macOS"), os.name.as_str());
689 assert_eq!(Some("10.16.0"), os.version.as_str());
690 assert_eq!(None, os.build.value());
691 }
692
693 #[test]
694 fn test_unity_ios() {
695 let mut os = OsContext {
696 raw_description: "iOS 17.5.1".to_owned().into(),
697 ..OsContext::default()
698 };
699
700 normalize_os_context(&mut os);
701 assert_eq!(Some("iOS"), os.name.as_str());
702 assert_eq!(Some("17.5.1"), os.version.as_str());
703 assert_eq!(None, os.build.value());
704 }
705
706 #[test]
707 fn test_unity_ipados() {
708 let mut os = OsContext {
709 raw_description: "iPadOS 17.5.1".to_owned().into(),
710 ..OsContext::default()
711 };
712
713 normalize_os_context(&mut os);
714 assert_eq!(Some("iPadOS"), os.name.as_str());
715 assert_eq!(Some("17.5.1"), os.version.as_str());
716 assert_eq!(None, os.build.value());
717 }
718
719 #[test]
721 fn test_unity_windows_os() {
722 let mut os = OsContext {
723 raw_description: "Windows 10 (10.0.19042) 64bit".to_owned().into(),
724 ..OsContext::default()
725 };
726 normalize_os_context(&mut os);
727 assert_eq!(Some("Windows"), os.name.as_str());
728 assert_eq!(Some("10"), os.version.as_str());
729 assert_eq!(Some(&LenientString("19042".to_owned())), os.build.value());
730 }
731
732 #[test]
733 fn test_unity_android_os() {
734 let mut os = OsContext {
735 raw_description: "Android OS 11 / API-30 (RP1A.201005.001/2107031736)"
736 .to_owned()
737 .into(),
738 ..OsContext::default()
739 };
740 normalize_os_context(&mut os);
741 assert_eq!(Some("Android"), os.name.as_str());
742 assert_eq!(Some("11"), os.version.as_str());
743 assert_eq!(None, os.build.value());
744 }
745
746 #[test]
747 fn test_unity_android_api_version() {
748 let description = "Android OS 11 / API-30 (RP1A.201005.001/2107031736)";
749 assert_eq!(Some("30"), get_android_api_version(description));
750 }
751
752 #[test]
753 fn test_unreal_windows_os() {
754 let mut os = OsContext {
755 raw_description: "Windows 10".to_owned().into(),
756 ..OsContext::default()
757 };
758 normalize_os_context(&mut os);
759 assert_eq!(Some("Windows"), os.name.as_str());
760 assert_eq!(Some("10"), os.version.as_str());
761 }
762
763 #[test]
764 fn test_linux_5_11() {
765 let mut os = OsContext {
766 raw_description: "Linux 5.11 Ubuntu 20.04 64bit".to_owned().into(),
767 ..OsContext::default()
768 };
769 normalize_os_context(&mut os);
770 assert_eq!(Some("Ubuntu"), os.name.as_str());
771 assert_eq!(Some("20.04"), os.version.as_str());
772 assert_eq!(Some("5.11"), os.kernel_version.as_str());
773 assert_eq!(None, os.build.value());
774 }
775
776 #[test]
777 fn test_unity_nintendo_switch() {
778 let mut os = OsContext {
780 raw_description: "Nintendo Switch".to_owned().into(),
781 ..OsContext::default()
782 };
783
784 normalize_os_context(&mut os);
785 assert_eq!(Some("Nintendo OS"), os.name.as_str());
786 assert_eq!(None, os.version.value());
787 assert_eq!(None, os.build.value());
788 }
789
790 #[test]
791 fn test_android_4_4_2() {
792 let mut os = OsContext {
793 raw_description: "Android OS 4.4.2 / API-19 (KOT49H/A536_S186_150813_ROW)"
794 .to_owned()
795 .into(),
796 ..OsContext::default()
797 };
798 normalize_os_context(&mut os);
799 assert_eq!(Some("Android"), os.name.as_str());
800 assert_eq!(Some("4.4.2"), os.version.as_str());
801 assert_eq!(None, os.build.value());
802 }
803
804 #[test]
805 fn test_infer_json() {
806 let mut response = ResponseContext {
807 data: Annotated::from(Value::String(r#"{"foo":"bar"}"#.to_owned())),
808 ..ResponseContext::default()
809 };
810
811 let mut expected_value = Object::new();
812 expected_value.insert(
813 "foo".to_owned(),
814 Annotated::from(Value::String("bar".into())),
815 );
816
817 normalize_response(&mut response);
818 assert_eq!(
819 response.inferred_content_type.as_str(),
820 Some("application/json")
821 );
822 assert_eq!(response.data.value(), Some(&Value::Object(expected_value)));
823 }
824
825 #[test]
826 fn test_broken_json_with_fallback() {
827 let mut response = ResponseContext {
828 data: Annotated::from(Value::String(r#"{"foo":"b"#.to_owned())),
829 headers: Annotated::from(Headers(PairList(vec![Annotated::new((
830 Annotated::new("Content-Type".to_owned().into()),
831 Annotated::new("text/plain; encoding=utf-8".to_owned().into()),
832 ))]))),
833 ..ResponseContext::default()
834 };
835
836 normalize_response(&mut response);
837 assert_eq!(response.inferred_content_type.as_str(), Some("text/plain"));
838 assert_eq!(response.data.as_str(), Some(r#"{"foo":"b"#));
839 }
840
841 #[test]
842 fn test_broken_json_without_fallback() {
843 let mut response = ResponseContext {
844 data: Annotated::from(Value::String(r#"{"foo":"b"#.to_owned())),
845 ..ResponseContext::default()
846 };
847
848 normalize_response(&mut response);
849 assert_eq!(response.inferred_content_type.value(), None);
850 assert_eq!(response.data.as_str(), Some(r#"{"foo":"b"#));
851 }
852
853 #[test]
854 fn test_os_computed_context() {
855 let mut os = OsContext {
856 name: "Windows".to_owned().into(),
857 version: "10".to_owned().into(),
858 ..OsContext::default()
859 };
860
861 normalize_os_context(&mut os);
862 assert_eq!(Some("Windows 10"), os.os.as_str());
863 }
864
865 #[test]
866 fn test_os_computed_context_missing_version() {
867 let mut os = OsContext {
868 name: "Windows".to_owned().into(),
869 ..OsContext::default()
870 };
871
872 normalize_os_context(&mut os);
873 assert_eq!(None, os.os.value());
874 }
875
876 #[test]
877 fn test_runtime_computed_context() {
878 let mut runtime = RuntimeContext {
879 name: "Python".to_owned().into(),
880 version: "3.9.0".to_owned().into(),
881 ..RuntimeContext::default()
882 };
883
884 normalize_runtime_context(&mut runtime);
885 assert_eq!(Some("Python 3.9.0"), runtime.runtime.as_str());
886 }
887
888 #[test]
889 fn test_runtime_computed_context_missing_version() {
890 let mut runtime = RuntimeContext {
891 name: "Python".to_owned().into(),
892 ..RuntimeContext::default()
893 };
894
895 normalize_runtime_context(&mut runtime);
896 assert_eq!(None, runtime.runtime.value());
897 }
898
899 #[test]
900 fn test_browser_computed_context() {
901 let mut browser = BrowserContext {
902 name: "Firefox".to_owned().into(),
903 version: "89.0".to_owned().into(),
904 ..BrowserContext::default()
905 };
906
907 normalize_browser_context(&mut browser);
908 assert_eq!(Some("Firefox 89.0"), browser.browser.as_str());
909 }
910
911 #[test]
912 fn test_browser_computed_context_missing_version() {
913 let mut browser = BrowserContext {
914 name: "Firefox".to_owned().into(),
915 ..BrowserContext::default()
916 };
917
918 normalize_browser_context(&mut browser);
919 assert_eq!(None, browser.browser.value());
920 }
921}