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()
95 && runtime.version.value().is_empty()
96 && let Some(raw_description) = runtime.raw_description.as_str()
97 && let Some(captures) = RUNTIME_DOTNET_REGEX.captures(raw_description)
98 {
99 runtime.name = captures.name("name").map(|m| m.as_str().to_owned()).into();
100 runtime.version = captures
101 .name("version")
102 .map(|m| m.as_str().to_owned())
103 .into();
104 }
105
106 if let Some(name) = runtime.name.as_str()
110 && let Some(build) = runtime.build.as_str()
111 && name.starts_with(".NET Framework")
112 {
113 let version = match build {
114 "378389" => Some("4.5".to_owned()),
115 "378675" => Some("4.5.1".to_owned()),
116 "378758" => Some("4.5.1".to_owned()),
117 "379893" => Some("4.5.2".to_owned()),
118 "393295" => Some("4.6".to_owned()),
119 "393297" => Some("4.6".to_owned()),
120 "394254" => Some("4.6.1".to_owned()),
121 "394271" => Some("4.6.1".to_owned()),
122 "394802" => Some("4.6.2".to_owned()),
123 "394806" => Some("4.6.2".to_owned()),
124 "460798" => Some("4.7".to_owned()),
125 "460805" => Some("4.7".to_owned()),
126 "461308" => Some("4.7.1".to_owned()),
127 "461310" => Some("4.7.1".to_owned()),
128 "461808" => Some("4.7.2".to_owned()),
129 "461814" => Some("4.7.2".to_owned()),
130 "528040" => Some("4.8".to_owned()),
131 "528049" => Some("4.8".to_owned()),
132 "528209" => Some("4.8".to_owned()),
133 "528372" => Some("4.8".to_owned()),
134 "528449" => Some("4.8".to_owned()),
135 _ => None,
136 };
137
138 if let Some(version) = version {
139 runtime.version = version.into();
140 }
141 }
142
143 if runtime.runtime.value().is_none()
146 && let (Some(name), Some(version)) = (runtime.name.value(), runtime.version.value())
147 {
148 runtime.runtime = Annotated::from(format!("{name} {version}"));
149 }
150}
151
152fn get_windows_version(description: &str) -> Option<(&str, &str)> {
157 let captures = OS_WINDOWS_REGEX1
158 .captures(description)
159 .or_else(|| OS_WINDOWS_REGEX2.captures(description))?;
160
161 let full_version = captures.name("version")?.as_str();
162 let build_number_str = captures.name("build_number")?.as_str();
163 let build_number = build_number_str.parse::<u64>().ok()?;
164
165 let version_name = match build_number {
166 2600..=3790 => "XP",
168 6002 => "Vista",
169 7601 => "7",
170 9200 => "8",
171 9600 => "8.1",
172 10240..=19044 => "10",
173 22000..=22999 => "11",
174 _ => full_version,
176 };
177
178 Some((version_name, build_number_str))
179}
180
181fn get_marketing_name(description: &str) -> Option<(&str, &str)> {
183 let (name, version) = description.split_once(' ')?;
184 let name = name.trim();
185 let version = version.trim();
186
187 if name.bytes().any(|c| !c.is_ascii_alphabetic()) {
189 return None;
190 }
191
192 if version
194 .bytes()
195 .any(|c| !matches!(c, b'0'..=b'9' | b'.' | b'-'))
196 {
197 return None;
198 }
199
200 Some((name, version))
201}
202
203#[allow(dead_code)]
204pub fn get_android_api_version(description: &str) -> Option<&str> {
208 if let Some(captures) = OS_ANDROID_REGEX.captures(description) {
209 captures.name("api").map(|m| m.as_str())
210 } else {
211 None
212 }
213}
214
215fn normalize_os_context(os: &mut OsContext) {
216 if os.name.value().is_some() || os.version.value().is_some() {
217 compute_os_context(os);
218 return;
219 }
220
221 if let Some(raw_description) = os.raw_description.as_str() {
222 if let Some((version, build_number)) = get_windows_version(raw_description) {
223 os.name = "Windows".to_owned().into();
224 os.version = version.to_owned().into();
225 if os.build.is_empty() {
226 os.build.set_value(Some(build_number.to_owned().into()));
228 }
229 } else if let Some(captures) = OS_MACOS_REGEX.captures(raw_description) {
230 os.name = "macOS".to_owned().into();
231 os.version = captures
232 .name("version")
233 .map(|m| m.as_str().to_owned())
234 .into();
235 os.build = captures
236 .name("build")
237 .map(|m| m.as_str().to_owned().into())
238 .into();
239 } else if let Some(captures) = OS_IOS_REGEX.captures(raw_description) {
240 os.name = "iOS".to_owned().into();
241 os.version = captures
242 .name("version")
243 .map(|m| m.as_str().to_owned())
244 .into();
245 } else if let Some(captures) = OS_IPADOS_REGEX.captures(raw_description) {
246 os.name = "iPadOS".to_owned().into();
247 os.version = captures
248 .name("version")
249 .map(|m| m.as_str().to_owned())
250 .into();
251 } else if let Some(captures) = OS_LINUX_DISTRO_UNAME_REGEX.captures(raw_description) {
252 os.name = captures.name("name").map(|m| m.as_str().to_owned()).into();
253 os.version = captures
254 .name("version")
255 .map(|m| m.as_str().to_owned())
256 .into();
257 os.kernel_version = captures
258 .name("kernel_version")
259 .map(|m| m.as_str().to_owned())
260 .into();
261 } else if let Some(captures) = OS_UNAME_REGEX.captures(raw_description) {
262 os.name = captures.name("name").map(|m| m.as_str().to_owned()).into();
263 os.kernel_version = captures
264 .name("kernel_version")
265 .map(|m| m.as_str().to_owned())
266 .into();
267 } else if let Some(captures) = OS_ANDROID_REGEX.captures(raw_description) {
268 os.name = "Android".to_owned().into();
269 os.version = captures
270 .name("version")
271 .map(|m| m.as_str().to_owned())
272 .into();
273 } else if raw_description == "Nintendo Switch" {
274 os.name = "Nintendo OS".to_owned().into();
275 } else if let Some((name, version)) = get_marketing_name(raw_description) {
276 os.name = name.to_owned().into();
277 os.version = version.to_owned().into();
278 }
279 }
280
281 compute_os_context(os);
282}
283
284fn compute_os_context(os: &mut OsContext) {
285 if os.os.value().is_none() {
288 let name = match (os.name.value(), os.version.value()) {
289 (Some(name), Some(version)) => Some(format!("{name} {version}")),
290 (Some(name), _) => Some(name.to_owned()),
291 _ => None,
292 };
293
294 if let Some(name) = name {
295 os.os = Annotated::new(name);
296 }
297 }
298}
299
300fn normalize_browser_context(browser: &mut BrowserContext) {
301 if browser.browser.value().is_none() {
304 let name = match (browser.name.value(), browser.version.value()) {
305 (Some(name), Some(version)) => Some(format!("{name} {version}")),
306 (Some(name), _) => Some(name.to_owned()),
307 _ => None,
308 };
309
310 if let Some(name) = name {
311 browser.browser = Annotated::new(name);
312 }
313 }
314}
315
316fn parse_raw_response_data(response: &ResponseContext) -> Option<(&'static str, Value)> {
317 let raw = response.data.as_str()?;
318
319 serde_json::from_str(raw)
320 .ok()
321 .map(|value| ("application/json", value))
322}
323
324fn normalize_response_data(response: &mut ResponseContext) {
325 if let Some((content_type, parsed_data)) = parse_raw_response_data(response) {
330 response.data.set_value(Some(parsed_data));
333 response.inferred_content_type = Annotated::from(content_type.to_owned());
334 } else {
335 response.inferred_content_type = response
336 .headers
337 .value()
338 .and_then(|headers| headers.get_header("Content-Type"))
339 .map(|value| value.split(';').next().unwrap_or(value).to_owned())
340 .into();
341 }
342}
343
344fn normalize_response(response: &mut ResponseContext) {
345 normalize_response_data(response);
346
347 let headers = match response.headers.value_mut() {
348 Some(headers) => headers,
349 None => return,
350 };
351
352 if response.cookies.value().is_some() {
353 headers.remove("Set-Cookie");
354 return;
355 }
356
357 let cookie_header = match headers.get_header("Set-Cookie") {
358 Some(header) => header,
359 None => return,
360 };
361
362 if let Ok(new_cookies) = Cookies::parse(cookie_header) {
363 response.cookies = Annotated::from(new_cookies);
364 headers.remove("Set-Cookie");
365 }
366}
367
368pub fn normalize_context(context: &mut Context) {
370 match context {
371 Context::Runtime(runtime) => normalize_runtime_context(runtime),
372 Context::Os(os) => normalize_os_context(os),
373 Context::Browser(browser) => normalize_browser_context(browser),
374 Context::Response(response) => normalize_response(response),
375 Context::Device(device) => {
376 if let Some(product_name) = device
377 .as_ref()
378 .model
379 .value()
380 .and_then(|model| ANDROID_MODEL_NAMES.get(model.as_str()))
381 {
382 device.name.set_value(Some(product_name.to_string()))
383 }
384 }
385 _ => {}
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use relay_event_schema::protocol::{Headers, LenientString, PairList};
392 use relay_protocol::SerializableAnnotated;
393 use similar_asserts::assert_eq;
394
395 use super::*;
396
397 macro_rules! assert_json_context {
398 ($ctx:expr, $($tt:tt)*) => {
399 insta::assert_json_snapshot!(SerializableAnnotated(&Annotated::new($ctx)), $($tt)*)
400
401 };
402 }
403
404 #[test]
405 fn test_get_product_name() {
406 assert_eq!(
407 ANDROID_MODEL_NAMES.get("NE2211").unwrap(),
408 &"OnePlus 10 Pro 5G"
409 );
410
411 assert_eq!(
412 ANDROID_MODEL_NAMES.get("MP04").unwrap(),
413 &"A13 Pro Max 5G EEA"
414 );
415
416 assert_eq!(ANDROID_MODEL_NAMES.get("ZT216_7").unwrap(), &"zyrex");
417
418 assert!(ANDROID_MODEL_NAMES.get("foobar").is_none());
419 }
420
421 #[test]
422 fn test_dotnet_framework_48_without_build_id() {
423 let mut runtime = RuntimeContext {
424 raw_description: ".NET Framework 4.8.4250.0".to_owned().into(),
425 ..RuntimeContext::default()
426 };
427
428 normalize_runtime_context(&mut runtime);
429 assert_json_context!(runtime, @r###"
430 {
431 "runtime": ".NET Framework 4.8.4250.0",
432 "name": ".NET Framework",
433 "version": "4.8.4250.0",
434 "raw_description": ".NET Framework 4.8.4250.0"
435 }
436 "###);
437 }
438
439 #[test]
440 fn test_dotnet_framework_472() {
441 let mut runtime = RuntimeContext {
442 raw_description: ".NET Framework 4.7.3056.0".to_owned().into(),
443 build: LenientString("461814".to_owned()).into(),
444 ..RuntimeContext::default()
445 };
446
447 normalize_runtime_context(&mut runtime);
448 assert_json_context!(runtime, @r###"
449 {
450 "runtime": ".NET Framework 4.7.2",
451 "name": ".NET Framework",
452 "version": "4.7.2",
453 "build": "461814",
454 "raw_description": ".NET Framework 4.7.3056.0"
455 }
456 "###);
457 }
458
459 #[test]
460 fn test_dotnet_framework_future_version() {
461 let mut runtime = RuntimeContext {
462 raw_description: ".NET Framework 200.0".to_owned().into(),
463 build: LenientString("999999".to_owned()).into(),
464 ..RuntimeContext::default()
465 };
466
467 normalize_runtime_context(&mut runtime);
469 assert_json_context!(runtime, @r###"
470 {
471 "runtime": ".NET Framework 200.0",
472 "name": ".NET Framework",
473 "version": "200.0",
474 "build": "999999",
475 "raw_description": ".NET Framework 200.0"
476 }
477 "###);
478 }
479
480 #[test]
481 fn test_dotnet_native() {
482 let mut runtime = RuntimeContext {
483 raw_description: ".NET Native 2.0".to_owned().into(),
484 ..RuntimeContext::default()
485 };
486
487 normalize_runtime_context(&mut runtime);
488 assert_json_context!(runtime, @r###"
489 {
490 "runtime": ".NET Native 2.0",
491 "name": ".NET Native",
492 "version": "2.0",
493 "raw_description": ".NET Native 2.0"
494 }
495 "###);
496 }
497
498 #[test]
499 fn test_dotnet_core() {
500 let mut runtime = RuntimeContext {
501 raw_description: ".NET Core 2.0".to_owned().into(),
502 ..RuntimeContext::default()
503 };
504
505 normalize_runtime_context(&mut runtime);
506 assert_json_context!(runtime, @r###"
507 {
508 "runtime": ".NET Core 2.0",
509 "name": ".NET Core",
510 "version": "2.0",
511 "raw_description": ".NET Core 2.0"
512 }
513 "###);
514 }
515
516 #[test]
517 fn test_windows_7_or_server_2008() {
518 let mut os = OsContext {
520 raw_description: "Microsoft Windows NT 6.1.7601 Service Pack 1"
521 .to_owned()
522 .into(),
523 ..OsContext::default()
524 };
525
526 normalize_os_context(&mut os);
527 assert_json_context!(os, @r###"
528 {
529 "os": "Windows 7",
530 "name": "Windows",
531 "version": "7",
532 "build": "7601",
533 "raw_description": "Microsoft Windows NT 6.1.7601 Service Pack 1"
534 }
535 "###);
536 }
537
538 #[test]
539 fn test_windows_8_or_server_2012_or_later() {
540 let mut os = OsContext {
545 raw_description: "Microsoft Windows NT 6.2.9200.0".to_owned().into(),
546 ..OsContext::default()
547 };
548
549 normalize_os_context(&mut os);
550 assert_json_context!(os, @r###"
551 {
552 "os": "Windows 8",
553 "name": "Windows",
554 "version": "8",
555 "build": "9200",
556 "raw_description": "Microsoft Windows NT 6.2.9200.0"
557 }
558 "###);
559 }
560
561 #[test]
562 fn test_windows_10() {
563 let mut os = OsContext {
566 raw_description: "Microsoft Windows 10.0.16299".to_owned().into(),
567 ..OsContext::default()
568 };
569
570 normalize_os_context(&mut os);
571 assert_json_context!(os, @r###"
572 {
573 "os": "Windows 10",
574 "name": "Windows",
575 "version": "10",
576 "build": "16299",
577 "raw_description": "Microsoft Windows 10.0.16299"
578 }
579 "###);
580 }
581
582 #[test]
583 fn test_windows_11() {
584 let mut os = OsContext {
586 raw_description: "Microsoft Windows 10.0.22000".to_owned().into(),
587 ..OsContext::default()
588 };
589
590 normalize_os_context(&mut os);
591 assert_json_context!(os, @r###"
592 {
593 "os": "Windows 11",
594 "name": "Windows",
595 "version": "11",
596 "build": "22000",
597 "raw_description": "Microsoft Windows 10.0.22000"
598 }
599 "###);
600 }
601
602 #[test]
603 fn test_windows_11_future1() {
604 let mut os = OsContext {
606 raw_description: "Microsoft Windows 10.0.22001".to_owned().into(),
607 ..OsContext::default()
608 };
609
610 normalize_os_context(&mut os);
611 assert_json_context!(os, @r###"
612 {
613 "os": "Windows 11",
614 "name": "Windows",
615 "version": "11",
616 "build": "22001",
617 "raw_description": "Microsoft Windows 10.0.22001"
618 }
619 "###);
620 }
621
622 #[test]
623 fn test_windows_11_future2() {
624 let mut os = OsContext {
626 raw_description: "Microsoft Windows 10.1.23456".to_owned().into(),
627 ..OsContext::default()
628 };
629
630 normalize_os_context(&mut os);
631 assert_json_context!(os, @r###"
632 {
633 "os": "Windows 10.1.23456",
634 "name": "Windows",
635 "version": "10.1.23456",
636 "build": "23456",
637 "raw_description": "Microsoft Windows 10.1.23456"
638 }
639 "###);
640 }
641
642 #[test]
643 fn test_macos_os_version() {
644 let mut os = OsContext {
646 raw_description: "Unix 17.5.0.0".to_owned().into(),
647 ..OsContext::default()
648 };
649
650 normalize_os_context(&mut os);
651 assert_json_context!(os, @r###"
652 {
653 "os": "Unix",
654 "name": "Unix",
655 "kernel_version": "17.5.0",
656 "raw_description": "Unix 17.5.0.0"
657 }
658 "###);
659 }
660
661 #[test]
662 fn test_macos_runtime() {
663 let mut os = OsContext {
665 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(),
666 ..OsContext::default()
667 };
668
669 normalize_os_context(&mut os);
670 assert_json_context!(os, @r###"
671 {
672 "os": "Darwin",
673 "name": "Darwin",
674 "kernel_version": "17.5.0",
675 "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"
676 }
677 "###);
678 }
679
680 #[test]
681 fn test_centos_os_version() {
682 let mut os = OsContext {
684 raw_description: "Unix 3.10.0.693".to_owned().into(),
685 ..OsContext::default()
686 };
687
688 normalize_os_context(&mut os);
689 assert_json_context!(os, @r###"
690 {
691 "os": "Unix",
692 "name": "Unix",
693 "kernel_version": "3.10.0.693",
694 "raw_description": "Unix 3.10.0.693"
695 }
696 "###);
697 }
698
699 #[test]
700 fn test_centos_runtime_info() {
701 let mut os = OsContext {
703 raw_description: "Linux 3.10.0-693.21.1.el7.x86_64 #1 SMP Wed Mar 7 19:03:37 UTC 2018"
704 .to_owned()
705 .into(),
706 ..OsContext::default()
707 };
708
709 normalize_os_context(&mut os);
710 assert_json_context!(os, @r###"
711 {
712 "os": "Linux",
713 "name": "Linux",
714 "kernel_version": "3.10.0",
715 "raw_description": "Linux 3.10.0-693.21.1.el7.x86_64 #1 SMP Wed Mar 7 19:03:37 UTC 2018"
716 }
717 "###);
718 }
719
720 #[test]
721 fn test_wsl_ubuntu() {
722 let mut os = OsContext {
725 raw_description: "Linux 4.4.0-43-Microsoft #1-Microsoft Wed Dec 31 14:42:53 PST 2014"
726 .to_owned()
727 .into(),
728 ..OsContext::default()
729 };
730
731 normalize_os_context(&mut os);
732 assert_json_context!(os, @r###"
733 {
734 "os": "Linux",
735 "name": "Linux",
736 "kernel_version": "4.4.0",
737 "raw_description": "Linux 4.4.0-43-Microsoft #1-Microsoft Wed Dec 31 14:42:53 PST 2014"
738 }
739 "###);
740 }
741
742 #[test]
743 fn test_macos_with_build() {
744 let mut os = OsContext {
745 raw_description: "Mac OS X 10.14.2 (18C54)".to_owned().into(),
746 ..OsContext::default()
747 };
748
749 normalize_os_context(&mut os);
750 assert_json_context!(os, @r###"
751 {
752 "os": "macOS 10.14.2",
753 "name": "macOS",
754 "version": "10.14.2",
755 "build": "18C54",
756 "raw_description": "Mac OS X 10.14.2 (18C54)"
757 }
758 "###);
759 }
760
761 #[test]
762 fn test_macos_without_build() {
763 let mut os = OsContext {
764 raw_description: "Mac OS X 10.14.2".to_owned().into(),
765 ..OsContext::default()
766 };
767
768 normalize_os_context(&mut os);
769 assert_json_context!(os, @r###"
770 {
771 "os": "macOS 10.14.2",
772 "name": "macOS",
773 "version": "10.14.2",
774 "raw_description": "Mac OS X 10.14.2"
775 }
776 "###);
777 }
778
779 #[test]
780 fn test_name_not_overwritten() {
781 let mut os = OsContext {
782 name: "Properly defined name".to_owned().into(),
783 raw_description: "Linux 4.4.0".to_owned().into(),
784 ..OsContext::default()
785 };
786
787 normalize_os_context(&mut os);
788 assert_json_context!(os, @r###"
789 {
790 "os": "Properly defined name",
791 "name": "Properly defined name",
792 "raw_description": "Linux 4.4.0"
793 }
794 "###);
795 }
796
797 #[test]
798 fn test_version_not_overwritten() {
799 let mut os = OsContext {
800 version: "Properly defined version".to_owned().into(),
801 raw_description: "Linux 4.4.0".to_owned().into(),
802 ..OsContext::default()
803 };
804
805 normalize_os_context(&mut os);
806 assert_json_context!(os, @r###"
807 {
808 "version": "Properly defined version",
809 "raw_description": "Linux 4.4.0"
810 }
811 "###);
812 }
813
814 #[test]
815 fn test_no_name() {
816 let mut os = OsContext::default();
817
818 normalize_os_context(&mut os);
819 assert_json_context!(os, @"{}");
820 }
821
822 #[test]
823 fn test_unity_mac_os() {
824 let mut os = OsContext {
825 raw_description: "Mac OS X 10.16.0".to_owned().into(),
826 ..OsContext::default()
827 };
828 normalize_os_context(&mut os);
829 assert_json_context!(os, @r###"
830 {
831 "os": "macOS 10.16.0",
832 "name": "macOS",
833 "version": "10.16.0",
834 "raw_description": "Mac OS X 10.16.0"
835 }
836 "###);
837 }
838
839 #[test]
840 fn test_unity_ios() {
841 let mut os = OsContext {
842 raw_description: "iOS 17.5.1".to_owned().into(),
843 ..OsContext::default()
844 };
845
846 normalize_os_context(&mut os);
847 assert_json_context!(os, @r###"
848 {
849 "os": "iOS 17.5.1",
850 "name": "iOS",
851 "version": "17.5.1",
852 "raw_description": "iOS 17.5.1"
853 }
854 "###);
855 }
856
857 #[test]
858 fn test_unity_ipados() {
859 let mut os = OsContext {
860 raw_description: "iPadOS 17.5.1".to_owned().into(),
861 ..OsContext::default()
862 };
863
864 normalize_os_context(&mut os);
865 assert_json_context!(os, @r###"
866 {
867 "os": "iPadOS 17.5.1",
868 "name": "iPadOS",
869 "version": "17.5.1",
870 "raw_description": "iPadOS 17.5.1"
871 }
872 "###);
873 }
874
875 #[test]
877 fn test_unity_windows_os() {
878 let mut os = OsContext {
879 raw_description: "Windows 10 (10.0.19042) 64bit".to_owned().into(),
880 ..OsContext::default()
881 };
882
883 normalize_os_context(&mut os);
884 assert_json_context!(os, @r###"
885 {
886 "os": "Windows 10",
887 "name": "Windows",
888 "version": "10",
889 "build": "19042",
890 "raw_description": "Windows 10 (10.0.19042) 64bit"
891 }
892 "###);
893 }
894
895 #[test]
896 fn test_unity_android_os() {
897 let mut os = OsContext {
898 raw_description: "Android OS 11 / API-30 (RP1A.201005.001/2107031736)"
899 .to_owned()
900 .into(),
901 ..OsContext::default()
902 };
903
904 normalize_os_context(&mut os);
905 assert_json_context!(os, @r###"
906 {
907 "os": "Android 11",
908 "name": "Android",
909 "version": "11",
910 "raw_description": "Android OS 11 / API-30 (RP1A.201005.001/2107031736)"
911 }
912 "###);
913 }
914
915 #[test]
916 fn test_unity_android_api_version() {
917 let description = "Android OS 11 / API-30 (RP1A.201005.001/2107031736)";
918 assert_eq!(Some("30"), get_android_api_version(description));
919 }
920
921 #[test]
922 fn test_unreal_windows_os() {
923 let mut os = OsContext {
924 raw_description: "Windows 10".to_owned().into(),
925 ..OsContext::default()
926 };
927
928 normalize_os_context(&mut os);
929 assert_json_context!(os, @r###"
930 {
931 "os": "Windows 10",
932 "name": "Windows",
933 "version": "10",
934 "raw_description": "Windows 10"
935 }
936 "###);
937 }
938
939 #[test]
940 fn test_linux_5_11() {
941 let mut os = OsContext {
942 raw_description: "Linux 5.11 Ubuntu 20.04 64bit".to_owned().into(),
943 ..OsContext::default()
944 };
945
946 normalize_os_context(&mut os);
947 assert_json_context!(os, @r###"
948 {
949 "os": "Ubuntu 20.04",
950 "name": "Ubuntu",
951 "version": "20.04",
952 "kernel_version": "5.11",
953 "raw_description": "Linux 5.11 Ubuntu 20.04 64bit"
954 }
955 "###);
956 }
957
958 #[test]
959 fn test_unity_nintendo_switch() {
960 let mut os = OsContext {
962 raw_description: "Nintendo Switch".to_owned().into(),
963 ..OsContext::default()
964 };
965
966 normalize_os_context(&mut os);
967 assert_json_context!(os, @r###"
968 {
969 "os": "Nintendo OS",
970 "name": "Nintendo OS",
971 "raw_description": "Nintendo Switch"
972 }
973 "###);
974 }
975
976 #[test]
977 fn test_android_4_4_2() {
978 let mut os = OsContext {
979 raw_description: "Android OS 4.4.2 / API-19 (KOT49H/A536_S186_150813_ROW)"
980 .to_owned()
981 .into(),
982 ..OsContext::default()
983 };
984
985 normalize_os_context(&mut os);
986 assert_json_context!(os, @r###"
987 {
988 "os": "Android 4.4.2",
989 "name": "Android",
990 "version": "4.4.2",
991 "raw_description": "Android OS 4.4.2 / API-19 (KOT49H/A536_S186_150813_ROW)"
992 }
993 "###);
994 }
995
996 #[test]
997 fn test_infer_json() {
998 let mut response = ResponseContext {
999 data: Annotated::from(Value::String(r#"{"foo":"bar"}"#.to_owned())),
1000 ..ResponseContext::default()
1001 };
1002
1003 normalize_response(&mut response);
1004 assert_json_context!(response, @r###"
1005 {
1006 "data": {
1007 "foo": "bar"
1008 },
1009 "inferred_content_type": "application/json"
1010 }
1011 "###);
1012 }
1013
1014 #[test]
1015 fn test_broken_json_with_fallback() {
1016 let mut response = ResponseContext {
1017 data: Annotated::from(Value::String(r#"{"foo":"b"#.to_owned())),
1018 headers: Annotated::from(Headers(PairList(vec![Annotated::new((
1019 Annotated::new("Content-Type".to_owned().into()),
1020 Annotated::new("text/plain; encoding=utf-8".to_owned().into()),
1021 ))]))),
1022 ..ResponseContext::default()
1023 };
1024
1025 normalize_response(&mut response);
1026 assert_json_context!(response, @r###"
1027 {
1028 "headers": [
1029 [
1030 "Content-Type",
1031 "text/plain; encoding=utf-8"
1032 ]
1033 ],
1034 "data": "{\"foo\":\"b",
1035 "inferred_content_type": "text/plain"
1036 }
1037 "###);
1038 }
1039
1040 #[test]
1041 fn test_broken_json_without_fallback() {
1042 let mut response = ResponseContext {
1043 data: Annotated::from(Value::String(r#"{"foo":"b"#.to_owned())),
1044 ..ResponseContext::default()
1045 };
1046
1047 normalize_response(&mut response);
1048 assert_json_context!(response, @r###"
1049 {
1050 "data": "{\"foo\":\"b"
1051 }
1052 "###);
1053 }
1054
1055 #[test]
1056 fn test_os_computed_context() {
1057 let mut os = OsContext {
1058 name: "Windows".to_owned().into(),
1059 version: "10".to_owned().into(),
1060 ..OsContext::default()
1061 };
1062
1063 normalize_os_context(&mut os);
1064 assert_json_context!(os, @r###"
1065 {
1066 "os": "Windows 10",
1067 "name": "Windows",
1068 "version": "10"
1069 }
1070 "###);
1071 }
1072
1073 #[test]
1074 fn test_os_computed_context_missing_version() {
1075 let mut os = OsContext {
1076 name: "Windows".to_owned().into(),
1077 ..OsContext::default()
1078 };
1079
1080 normalize_os_context(&mut os);
1081 assert_json_context!(os, @r###"
1082 {
1083 "os": "Windows",
1084 "name": "Windows"
1085 }
1086 "###);
1087 }
1088
1089 #[test]
1090 fn test_runtime_computed_context() {
1091 let mut runtime = RuntimeContext {
1092 name: "Python".to_owned().into(),
1093 version: "3.9.0".to_owned().into(),
1094 ..RuntimeContext::default()
1095 };
1096
1097 normalize_runtime_context(&mut runtime);
1098 assert_json_context!(runtime, @r###"
1099 {
1100 "runtime": "Python 3.9.0",
1101 "name": "Python",
1102 "version": "3.9.0"
1103 }
1104 "###);
1105 }
1106
1107 #[test]
1108 fn test_runtime_computed_context_missing_version() {
1109 let mut runtime = RuntimeContext {
1110 name: "Python".to_owned().into(),
1111 ..RuntimeContext::default()
1112 };
1113
1114 normalize_runtime_context(&mut runtime);
1115 assert_json_context!(runtime, @r###"
1116 {
1117 "name": "Python"
1118 }
1119 "###);
1120 }
1121
1122 #[test]
1123 fn test_browser_computed_context() {
1124 let mut browser = BrowserContext {
1125 name: "Firefox".to_owned().into(),
1126 version: "89.0".to_owned().into(),
1127 ..BrowserContext::default()
1128 };
1129
1130 normalize_browser_context(&mut browser);
1131 assert_json_context!(browser, @r###"
1132 {
1133 "browser": "Firefox 89.0",
1134 "name": "Firefox",
1135 "version": "89.0"
1136 }
1137 "###);
1138 }
1139
1140 #[test]
1141 fn test_browser_computed_context_missing_version() {
1142 let mut browser = BrowserContext {
1143 name: "Firefox".to_owned().into(),
1144 ..BrowserContext::default()
1145 };
1146
1147 normalize_browser_context(&mut browser);
1148 assert_json_context!(browser, @r###"
1149 {
1150 "browser": "Firefox",
1151 "name": "Firefox"
1152 }
1153 "###);
1154 }
1155}