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 let name = match (os.name.value(), os.version.value()) {
290 (Some(name), Some(version)) => Some(format!("{name} {version}")),
291 (Some(name), _) => Some(name.to_owned()),
292 _ => None,
293 };
294
295 if let Some(name) = name {
296 os.os = Annotated::new(name);
297 }
298 }
299}
300
301fn normalize_browser_context(browser: &mut BrowserContext) {
302 if browser.browser.value().is_none() {
305 let name = match (browser.name.value(), browser.version.value()) {
306 (Some(name), Some(version)) => Some(format!("{name} {version}")),
307 (Some(name), _) => Some(name.to_owned()),
308 _ => None,
309 };
310
311 if let Some(name) = name {
312 browser.browser = Annotated::new(name);
313 }
314 }
315}
316
317fn parse_raw_response_data(response: &ResponseContext) -> Option<(&'static str, Value)> {
318 let raw = response.data.as_str()?;
319
320 serde_json::from_str(raw)
321 .ok()
322 .map(|value| ("application/json", value))
323}
324
325fn normalize_response_data(response: &mut ResponseContext) {
326 if let Some((content_type, parsed_data)) = parse_raw_response_data(response) {
331 response.data.set_value(Some(parsed_data));
334 response.inferred_content_type = Annotated::from(content_type.to_owned());
335 } else {
336 response.inferred_content_type = response
337 .headers
338 .value()
339 .and_then(|headers| headers.get_header("Content-Type"))
340 .map(|value| value.split(';').next().unwrap_or(value).to_owned())
341 .into();
342 }
343}
344
345fn normalize_response(response: &mut ResponseContext) {
346 normalize_response_data(response);
347
348 let headers = match response.headers.value_mut() {
349 Some(headers) => headers,
350 None => return,
351 };
352
353 if response.cookies.value().is_some() {
354 headers.remove("Set-Cookie");
355 return;
356 }
357
358 let cookie_header = match headers.get_header("Set-Cookie") {
359 Some(header) => header,
360 None => return,
361 };
362
363 if let Ok(new_cookies) = Cookies::parse(cookie_header) {
364 response.cookies = Annotated::from(new_cookies);
365 headers.remove("Set-Cookie");
366 }
367}
368
369pub fn normalize_context(context: &mut Context) {
371 match context {
372 Context::Runtime(runtime) => normalize_runtime_context(runtime),
373 Context::Os(os) => normalize_os_context(os),
374 Context::Browser(browser) => normalize_browser_context(browser),
375 Context::Response(response) => normalize_response(response),
376 Context::Device(device) => {
377 if let Some(product_name) = device
378 .as_ref()
379 .model
380 .value()
381 .and_then(|model| ANDROID_MODEL_NAMES.get(model.as_str()))
382 {
383 device.name.set_value(Some(product_name.to_string()))
384 }
385 }
386 _ => {}
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use relay_event_schema::protocol::{Headers, LenientString, PairList};
393 use relay_protocol::SerializableAnnotated;
394 use similar_asserts::assert_eq;
395
396 use super::*;
397
398 macro_rules! assert_json_context {
399 ($ctx:expr, $($tt:tt)*) => {
400 insta::assert_json_snapshot!(SerializableAnnotated(&Annotated::new($ctx)), $($tt)*)
401
402 };
403 }
404
405 #[test]
406 fn test_get_product_name() {
407 assert_eq!(
408 ANDROID_MODEL_NAMES.get("NE2211").unwrap(),
409 &"OnePlus 10 Pro 5G"
410 );
411
412 assert_eq!(
413 ANDROID_MODEL_NAMES.get("MP04").unwrap(),
414 &"A13 Pro Max 5G EEA"
415 );
416
417 assert_eq!(ANDROID_MODEL_NAMES.get("ZT216_7").unwrap(), &"zyrex");
418
419 assert!(ANDROID_MODEL_NAMES.get("foobar").is_none());
420 }
421
422 #[test]
423 fn test_dotnet_framework_48_without_build_id() {
424 let mut runtime = RuntimeContext {
425 raw_description: ".NET Framework 4.8.4250.0".to_owned().into(),
426 ..RuntimeContext::default()
427 };
428
429 normalize_runtime_context(&mut runtime);
430 assert_json_context!(runtime, @r###"
431 {
432 "runtime": ".NET Framework 4.8.4250.0",
433 "name": ".NET Framework",
434 "version": "4.8.4250.0",
435 "raw_description": ".NET Framework 4.8.4250.0"
436 }
437 "###);
438 }
439
440 #[test]
441 fn test_dotnet_framework_472() {
442 let mut runtime = RuntimeContext {
443 raw_description: ".NET Framework 4.7.3056.0".to_owned().into(),
444 build: LenientString("461814".to_owned()).into(),
445 ..RuntimeContext::default()
446 };
447
448 normalize_runtime_context(&mut runtime);
449 assert_json_context!(runtime, @r###"
450 {
451 "runtime": ".NET Framework 4.7.2",
452 "name": ".NET Framework",
453 "version": "4.7.2",
454 "build": "461814",
455 "raw_description": ".NET Framework 4.7.3056.0"
456 }
457 "###);
458 }
459
460 #[test]
461 fn test_dotnet_framework_future_version() {
462 let mut runtime = RuntimeContext {
463 raw_description: ".NET Framework 200.0".to_owned().into(),
464 build: LenientString("999999".to_owned()).into(),
465 ..RuntimeContext::default()
466 };
467
468 normalize_runtime_context(&mut runtime);
470 assert_json_context!(runtime, @r###"
471 {
472 "runtime": ".NET Framework 200.0",
473 "name": ".NET Framework",
474 "version": "200.0",
475 "build": "999999",
476 "raw_description": ".NET Framework 200.0"
477 }
478 "###);
479 }
480
481 #[test]
482 fn test_dotnet_native() {
483 let mut runtime = RuntimeContext {
484 raw_description: ".NET Native 2.0".to_owned().into(),
485 ..RuntimeContext::default()
486 };
487
488 normalize_runtime_context(&mut runtime);
489 assert_json_context!(runtime, @r###"
490 {
491 "runtime": ".NET Native 2.0",
492 "name": ".NET Native",
493 "version": "2.0",
494 "raw_description": ".NET Native 2.0"
495 }
496 "###);
497 }
498
499 #[test]
500 fn test_dotnet_core() {
501 let mut runtime = RuntimeContext {
502 raw_description: ".NET Core 2.0".to_owned().into(),
503 ..RuntimeContext::default()
504 };
505
506 normalize_runtime_context(&mut runtime);
507 assert_json_context!(runtime, @r###"
508 {
509 "runtime": ".NET Core 2.0",
510 "name": ".NET Core",
511 "version": "2.0",
512 "raw_description": ".NET Core 2.0"
513 }
514 "###);
515 }
516
517 #[test]
518 fn test_windows_7_or_server_2008() {
519 let mut os = OsContext {
521 raw_description: "Microsoft Windows NT 6.1.7601 Service Pack 1"
522 .to_owned()
523 .into(),
524 ..OsContext::default()
525 };
526
527 normalize_os_context(&mut os);
528 assert_json_context!(os, @r###"
529 {
530 "os": "Windows 7",
531 "name": "Windows",
532 "version": "7",
533 "build": "7601",
534 "raw_description": "Microsoft Windows NT 6.1.7601 Service Pack 1"
535 }
536 "###);
537 }
538
539 #[test]
540 fn test_windows_8_or_server_2012_or_later() {
541 let mut os = OsContext {
546 raw_description: "Microsoft Windows NT 6.2.9200.0".to_owned().into(),
547 ..OsContext::default()
548 };
549
550 normalize_os_context(&mut os);
551 assert_json_context!(os, @r###"
552 {
553 "os": "Windows 8",
554 "name": "Windows",
555 "version": "8",
556 "build": "9200",
557 "raw_description": "Microsoft Windows NT 6.2.9200.0"
558 }
559 "###);
560 }
561
562 #[test]
563 fn test_windows_10() {
564 let mut os = OsContext {
567 raw_description: "Microsoft Windows 10.0.16299".to_owned().into(),
568 ..OsContext::default()
569 };
570
571 normalize_os_context(&mut os);
572 assert_json_context!(os, @r###"
573 {
574 "os": "Windows 10",
575 "name": "Windows",
576 "version": "10",
577 "build": "16299",
578 "raw_description": "Microsoft Windows 10.0.16299"
579 }
580 "###);
581 }
582
583 #[test]
584 fn test_windows_11() {
585 let mut os = OsContext {
587 raw_description: "Microsoft Windows 10.0.22000".to_owned().into(),
588 ..OsContext::default()
589 };
590
591 normalize_os_context(&mut os);
592 assert_json_context!(os, @r###"
593 {
594 "os": "Windows 11",
595 "name": "Windows",
596 "version": "11",
597 "build": "22000",
598 "raw_description": "Microsoft Windows 10.0.22000"
599 }
600 "###);
601 }
602
603 #[test]
604 fn test_windows_11_future1() {
605 let mut os = OsContext {
607 raw_description: "Microsoft Windows 10.0.22001".to_owned().into(),
608 ..OsContext::default()
609 };
610
611 normalize_os_context(&mut os);
612 assert_json_context!(os, @r###"
613 {
614 "os": "Windows 11",
615 "name": "Windows",
616 "version": "11",
617 "build": "22001",
618 "raw_description": "Microsoft Windows 10.0.22001"
619 }
620 "###);
621 }
622
623 #[test]
624 fn test_windows_11_future2() {
625 let mut os = OsContext {
627 raw_description: "Microsoft Windows 10.1.23456".to_owned().into(),
628 ..OsContext::default()
629 };
630
631 normalize_os_context(&mut os);
632 assert_json_context!(os, @r###"
633 {
634 "os": "Windows 10.1.23456",
635 "name": "Windows",
636 "version": "10.1.23456",
637 "build": "23456",
638 "raw_description": "Microsoft Windows 10.1.23456"
639 }
640 "###);
641 }
642
643 #[test]
644 fn test_macos_os_version() {
645 let mut os = OsContext {
647 raw_description: "Unix 17.5.0.0".to_owned().into(),
648 ..OsContext::default()
649 };
650
651 normalize_os_context(&mut os);
652 assert_json_context!(os, @r###"
653 {
654 "os": "Unix",
655 "name": "Unix",
656 "kernel_version": "17.5.0",
657 "raw_description": "Unix 17.5.0.0"
658 }
659 "###);
660 }
661
662 #[test]
663 fn test_macos_runtime() {
664 let mut os = OsContext {
666 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(),
667 ..OsContext::default()
668 };
669
670 normalize_os_context(&mut os);
671 assert_json_context!(os, @r###"
672 {
673 "os": "Darwin",
674 "name": "Darwin",
675 "kernel_version": "17.5.0",
676 "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"
677 }
678 "###);
679 }
680
681 #[test]
682 fn test_centos_os_version() {
683 let mut os = OsContext {
685 raw_description: "Unix 3.10.0.693".to_owned().into(),
686 ..OsContext::default()
687 };
688
689 normalize_os_context(&mut os);
690 assert_json_context!(os, @r###"
691 {
692 "os": "Unix",
693 "name": "Unix",
694 "kernel_version": "3.10.0.693",
695 "raw_description": "Unix 3.10.0.693"
696 }
697 "###);
698 }
699
700 #[test]
701 fn test_centos_runtime_info() {
702 let mut os = OsContext {
704 raw_description: "Linux 3.10.0-693.21.1.el7.x86_64 #1 SMP Wed Mar 7 19:03:37 UTC 2018"
705 .to_owned()
706 .into(),
707 ..OsContext::default()
708 };
709
710 normalize_os_context(&mut os);
711 assert_json_context!(os, @r###"
712 {
713 "os": "Linux",
714 "name": "Linux",
715 "kernel_version": "3.10.0",
716 "raw_description": "Linux 3.10.0-693.21.1.el7.x86_64 #1 SMP Wed Mar 7 19:03:37 UTC 2018"
717 }
718 "###);
719 }
720
721 #[test]
722 fn test_wsl_ubuntu() {
723 let mut os = OsContext {
726 raw_description: "Linux 4.4.0-43-Microsoft #1-Microsoft Wed Dec 31 14:42:53 PST 2014"
727 .to_owned()
728 .into(),
729 ..OsContext::default()
730 };
731
732 normalize_os_context(&mut os);
733 assert_json_context!(os, @r###"
734 {
735 "os": "Linux",
736 "name": "Linux",
737 "kernel_version": "4.4.0",
738 "raw_description": "Linux 4.4.0-43-Microsoft #1-Microsoft Wed Dec 31 14:42:53 PST 2014"
739 }
740 "###);
741 }
742
743 #[test]
744 fn test_macos_with_build() {
745 let mut os = OsContext {
746 raw_description: "Mac OS X 10.14.2 (18C54)".to_owned().into(),
747 ..OsContext::default()
748 };
749
750 normalize_os_context(&mut os);
751 assert_json_context!(os, @r###"
752 {
753 "os": "macOS 10.14.2",
754 "name": "macOS",
755 "version": "10.14.2",
756 "build": "18C54",
757 "raw_description": "Mac OS X 10.14.2 (18C54)"
758 }
759 "###);
760 }
761
762 #[test]
763 fn test_macos_without_build() {
764 let mut os = OsContext {
765 raw_description: "Mac OS X 10.14.2".to_owned().into(),
766 ..OsContext::default()
767 };
768
769 normalize_os_context(&mut os);
770 assert_json_context!(os, @r###"
771 {
772 "os": "macOS 10.14.2",
773 "name": "macOS",
774 "version": "10.14.2",
775 "raw_description": "Mac OS X 10.14.2"
776 }
777 "###);
778 }
779
780 #[test]
781 fn test_name_not_overwritten() {
782 let mut os = OsContext {
783 name: "Properly defined name".to_owned().into(),
784 raw_description: "Linux 4.4.0".to_owned().into(),
785 ..OsContext::default()
786 };
787
788 normalize_os_context(&mut os);
789 assert_json_context!(os, @r###"
790 {
791 "os": "Properly defined name",
792 "name": "Properly defined name",
793 "raw_description": "Linux 4.4.0"
794 }
795 "###);
796 }
797
798 #[test]
799 fn test_version_not_overwritten() {
800 let mut os = OsContext {
801 version: "Properly defined version".to_owned().into(),
802 raw_description: "Linux 4.4.0".to_owned().into(),
803 ..OsContext::default()
804 };
805
806 normalize_os_context(&mut os);
807 assert_json_context!(os, @r###"
808 {
809 "version": "Properly defined version",
810 "raw_description": "Linux 4.4.0"
811 }
812 "###);
813 }
814
815 #[test]
816 fn test_no_name() {
817 let mut os = OsContext::default();
818
819 normalize_os_context(&mut os);
820 assert_json_context!(os, @"{}");
821 }
822
823 #[test]
824 fn test_unity_mac_os() {
825 let mut os = OsContext {
826 raw_description: "Mac OS X 10.16.0".to_owned().into(),
827 ..OsContext::default()
828 };
829 normalize_os_context(&mut os);
830 assert_json_context!(os, @r###"
831 {
832 "os": "macOS 10.16.0",
833 "name": "macOS",
834 "version": "10.16.0",
835 "raw_description": "Mac OS X 10.16.0"
836 }
837 "###);
838 }
839
840 #[test]
841 fn test_unity_ios() {
842 let mut os = OsContext {
843 raw_description: "iOS 17.5.1".to_owned().into(),
844 ..OsContext::default()
845 };
846
847 normalize_os_context(&mut os);
848 assert_json_context!(os, @r###"
849 {
850 "os": "iOS 17.5.1",
851 "name": "iOS",
852 "version": "17.5.1",
853 "raw_description": "iOS 17.5.1"
854 }
855 "###);
856 }
857
858 #[test]
859 fn test_unity_ipados() {
860 let mut os = OsContext {
861 raw_description: "iPadOS 17.5.1".to_owned().into(),
862 ..OsContext::default()
863 };
864
865 normalize_os_context(&mut os);
866 assert_json_context!(os, @r###"
867 {
868 "os": "iPadOS 17.5.1",
869 "name": "iPadOS",
870 "version": "17.5.1",
871 "raw_description": "iPadOS 17.5.1"
872 }
873 "###);
874 }
875
876 #[test]
878 fn test_unity_windows_os() {
879 let mut os = OsContext {
880 raw_description: "Windows 10 (10.0.19042) 64bit".to_owned().into(),
881 ..OsContext::default()
882 };
883
884 normalize_os_context(&mut os);
885 assert_json_context!(os, @r###"
886 {
887 "os": "Windows 10",
888 "name": "Windows",
889 "version": "10",
890 "build": "19042",
891 "raw_description": "Windows 10 (10.0.19042) 64bit"
892 }
893 "###);
894 }
895
896 #[test]
897 fn test_unity_android_os() {
898 let mut os = OsContext {
899 raw_description: "Android OS 11 / API-30 (RP1A.201005.001/2107031736)"
900 .to_owned()
901 .into(),
902 ..OsContext::default()
903 };
904
905 normalize_os_context(&mut os);
906 assert_json_context!(os, @r###"
907 {
908 "os": "Android 11",
909 "name": "Android",
910 "version": "11",
911 "raw_description": "Android OS 11 / API-30 (RP1A.201005.001/2107031736)"
912 }
913 "###);
914 }
915
916 #[test]
917 fn test_unity_android_api_version() {
918 let description = "Android OS 11 / API-30 (RP1A.201005.001/2107031736)";
919 assert_eq!(Some("30"), get_android_api_version(description));
920 }
921
922 #[test]
923 fn test_unreal_windows_os() {
924 let mut os = OsContext {
925 raw_description: "Windows 10".to_owned().into(),
926 ..OsContext::default()
927 };
928
929 normalize_os_context(&mut os);
930 assert_json_context!(os, @r###"
931 {
932 "os": "Windows 10",
933 "name": "Windows",
934 "version": "10",
935 "raw_description": "Windows 10"
936 }
937 "###);
938 }
939
940 #[test]
941 fn test_linux_5_11() {
942 let mut os = OsContext {
943 raw_description: "Linux 5.11 Ubuntu 20.04 64bit".to_owned().into(),
944 ..OsContext::default()
945 };
946
947 normalize_os_context(&mut os);
948 assert_json_context!(os, @r###"
949 {
950 "os": "Ubuntu 20.04",
951 "name": "Ubuntu",
952 "version": "20.04",
953 "kernel_version": "5.11",
954 "raw_description": "Linux 5.11 Ubuntu 20.04 64bit"
955 }
956 "###);
957 }
958
959 #[test]
960 fn test_unity_nintendo_switch() {
961 let mut os = OsContext {
963 raw_description: "Nintendo Switch".to_owned().into(),
964 ..OsContext::default()
965 };
966
967 normalize_os_context(&mut os);
968 assert_json_context!(os, @r###"
969 {
970 "os": "Nintendo OS",
971 "name": "Nintendo OS",
972 "raw_description": "Nintendo Switch"
973 }
974 "###);
975 }
976
977 #[test]
978 fn test_android_4_4_2() {
979 let mut os = OsContext {
980 raw_description: "Android OS 4.4.2 / API-19 (KOT49H/A536_S186_150813_ROW)"
981 .to_owned()
982 .into(),
983 ..OsContext::default()
984 };
985
986 normalize_os_context(&mut os);
987 assert_json_context!(os, @r###"
988 {
989 "os": "Android 4.4.2",
990 "name": "Android",
991 "version": "4.4.2",
992 "raw_description": "Android OS 4.4.2 / API-19 (KOT49H/A536_S186_150813_ROW)"
993 }
994 "###);
995 }
996
997 #[test]
998 fn test_infer_json() {
999 let mut response = ResponseContext {
1000 data: Annotated::from(Value::String(r#"{"foo":"bar"}"#.to_owned())),
1001 ..ResponseContext::default()
1002 };
1003
1004 normalize_response(&mut response);
1005 assert_json_context!(response, @r###"
1006 {
1007 "data": {
1008 "foo": "bar"
1009 },
1010 "inferred_content_type": "application/json"
1011 }
1012 "###);
1013 }
1014
1015 #[test]
1016 fn test_broken_json_with_fallback() {
1017 let mut response = ResponseContext {
1018 data: Annotated::from(Value::String(r#"{"foo":"b"#.to_owned())),
1019 headers: Annotated::from(Headers(PairList(vec![Annotated::new((
1020 Annotated::new("Content-Type".to_owned().into()),
1021 Annotated::new("text/plain; encoding=utf-8".to_owned().into()),
1022 ))]))),
1023 ..ResponseContext::default()
1024 };
1025
1026 normalize_response(&mut response);
1027 assert_json_context!(response, @r###"
1028 {
1029 "headers": [
1030 [
1031 "Content-Type",
1032 "text/plain; encoding=utf-8"
1033 ]
1034 ],
1035 "data": "{\"foo\":\"b",
1036 "inferred_content_type": "text/plain"
1037 }
1038 "###);
1039 }
1040
1041 #[test]
1042 fn test_broken_json_without_fallback() {
1043 let mut response = ResponseContext {
1044 data: Annotated::from(Value::String(r#"{"foo":"b"#.to_owned())),
1045 ..ResponseContext::default()
1046 };
1047
1048 normalize_response(&mut response);
1049 assert_json_context!(response, @r###"
1050 {
1051 "data": "{\"foo\":\"b"
1052 }
1053 "###);
1054 }
1055
1056 #[test]
1057 fn test_os_computed_context() {
1058 let mut os = OsContext {
1059 name: "Windows".to_owned().into(),
1060 version: "10".to_owned().into(),
1061 ..OsContext::default()
1062 };
1063
1064 normalize_os_context(&mut os);
1065 assert_json_context!(os, @r###"
1066 {
1067 "os": "Windows 10",
1068 "name": "Windows",
1069 "version": "10"
1070 }
1071 "###);
1072 }
1073
1074 #[test]
1075 fn test_os_computed_context_missing_version() {
1076 let mut os = OsContext {
1077 name: "Windows".to_owned().into(),
1078 ..OsContext::default()
1079 };
1080
1081 normalize_os_context(&mut os);
1082 assert_json_context!(os, @r###"
1083 {
1084 "os": "Windows",
1085 "name": "Windows"
1086 }
1087 "###);
1088 }
1089
1090 #[test]
1091 fn test_runtime_computed_context() {
1092 let mut runtime = RuntimeContext {
1093 name: "Python".to_owned().into(),
1094 version: "3.9.0".to_owned().into(),
1095 ..RuntimeContext::default()
1096 };
1097
1098 normalize_runtime_context(&mut runtime);
1099 assert_json_context!(runtime, @r###"
1100 {
1101 "runtime": "Python 3.9.0",
1102 "name": "Python",
1103 "version": "3.9.0"
1104 }
1105 "###);
1106 }
1107
1108 #[test]
1109 fn test_runtime_computed_context_missing_version() {
1110 let mut runtime = RuntimeContext {
1111 name: "Python".to_owned().into(),
1112 ..RuntimeContext::default()
1113 };
1114
1115 normalize_runtime_context(&mut runtime);
1116 assert_json_context!(runtime, @r###"
1117 {
1118 "name": "Python"
1119 }
1120 "###);
1121 }
1122
1123 #[test]
1124 fn test_browser_computed_context() {
1125 let mut browser = BrowserContext {
1126 name: "Firefox".to_owned().into(),
1127 version: "89.0".to_owned().into(),
1128 ..BrowserContext::default()
1129 };
1130
1131 normalize_browser_context(&mut browser);
1132 assert_json_context!(browser, @r###"
1133 {
1134 "browser": "Firefox 89.0",
1135 "name": "Firefox",
1136 "version": "89.0"
1137 }
1138 "###);
1139 }
1140
1141 #[test]
1142 fn test_browser_computed_context_missing_version() {
1143 let mut browser = BrowserContext {
1144 name: "Firefox".to_owned().into(),
1145 ..BrowserContext::default()
1146 };
1147
1148 normalize_browser_context(&mut browser);
1149 assert_json_context!(browser, @r###"
1150 {
1151 "browser": "Firefox",
1152 "name": "Firefox"
1153 }
1154 "###);
1155 }
1156}