1use std::collections::HashMap;
4use std::sync::LazyLock;
5
6use regex::Regex;
7use relay_base_schema::spans::SpanStatus;
8use relay_event_schema::protocol::{
9 BrowserContext, Context, Cookies, OsContext, ResponseContext, RuntimeContext,
10};
11use relay_protocol::{Annotated, Empty, Value};
12
13static OS_WINDOWS_REGEX1: LazyLock<Regex> = LazyLock::new(|| {
15 Regex::new(r"^(Microsoft\s+)?Windows\s+(NT\s+)?(?P<version>\d+\.\d+\.(?P<build_number>\d+)).*$")
16 .unwrap()
17});
18static OS_WINDOWS_REGEX2: LazyLock<Regex> = LazyLock::new(|| {
19 Regex::new(r"^Windows\s+\d+\s+\((?P<version>\d+\.\d+\.(?P<build_number>\d+)).*$").unwrap()
20});
21
22static OS_ANDROID_REGEX: LazyLock<Regex> = LazyLock::new(|| {
23 Regex::new(r"^Android (OS )?(?P<version>\d+(\.\d+){0,2}) / API-(?P<api>(\d+))").unwrap()
24});
25
26static OS_MACOS_REGEX: LazyLock<Regex> = LazyLock::new(|| {
28 Regex::new(r"^Mac OS X (?P<version>\d+\.\d+\.\d+)( \((?P<build>[a-fA-F0-9]+)\))?$").unwrap()
29});
30
31static OS_IOS_REGEX: LazyLock<Regex> =
33 LazyLock::new(|| Regex::new(r"^iOS (?P<version>\d+\.\d+\.\d+)").unwrap());
34
35static OS_IPADOS_REGEX: LazyLock<Regex> =
37 LazyLock::new(|| Regex::new(r"^iPadOS (?P<version>\d+\.\d+\.\d+)").unwrap());
38
39static OS_LINUX_DISTRO_UNAME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
41 Regex::new(r"^Linux (?P<kernel_version>\d+\.\d+(\.\d+(\.[1-9]+)?)?) (?P<name>[a-zA-Z]+) (?P<version>\d+(\.\d+){0,2})").unwrap()
42});
43
44static OS_UNAME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
47 Regex::new(r"^(?P<name>[a-zA-Z]+) (?P<kernel_version>\d+\.\d+(\.\d+(\.[1-9]+)?)?)").unwrap()
48});
49
50static RUNTIME_DOTNET_REGEX: LazyLock<Regex> =
52 LazyLock::new(|| Regex::new(r"^(?P<name>.*) (?P<version>\d+\.\d+(\.\d+){0,2}).*$").unwrap());
53
54static ANDROID_MODEL_NAMES: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
57 let mut map = HashMap::new();
58 let android_str = include_str!("android_models.csv");
60
61 let mut lines = android_str.lines();
62
63 let header = lines.next().expect("CSV file should have a header");
64
65 let header_fields: Vec<&str> = header.split(',').collect();
66 let model_index = header_fields.iter().position(|&s| s.trim() == "Model");
67 let product_name_index = header_fields
68 .iter()
69 .position(|&s| s.trim() == "Marketing Name");
70
71 let (model_index, product_name_index) = match (model_index, product_name_index) {
72 (Some(model_index), Some(product_name_index)) => (model_index, product_name_index),
73 (_, _) => {
74 relay_log::error!(
75 "failed to find model and/or marketing name headers for android-model map",
76 );
77
78 return HashMap::new();
79 }
80 };
81
82 for line in lines {
83 let fields: Vec<&str> = line.split(',').collect();
84 if fields.len() > std::cmp::max(model_index, product_name_index) {
85 map.insert(
86 fields[model_index].trim(),
87 fields[product_name_index].trim(),
88 );
89 }
90 }
91 map
92});
93
94fn normalize_runtime_context(runtime: &mut RuntimeContext) {
95 if runtime.name.value().is_empty()
96 && runtime.version.value().is_empty()
97 && let Some(raw_description) = runtime.raw_description.as_str()
98 && let Some(captures) = RUNTIME_DOTNET_REGEX.captures(raw_description)
99 {
100 runtime.name = captures.name("name").map(|m| m.as_str().to_owned()).into();
101 runtime.version = captures
102 .name("version")
103 .map(|m| m.as_str().to_owned())
104 .into();
105 }
106
107 if let Some(name) = runtime.name.as_str()
111 && let Some(build) = runtime.build.as_str()
112 && name.starts_with(".NET Framework")
113 {
114 let version = match build {
115 "378389" => Some("4.5".to_owned()),
116 "378675" => Some("4.5.1".to_owned()),
117 "378758" => Some("4.5.1".to_owned()),
118 "379893" => Some("4.5.2".to_owned()),
119 "393295" => Some("4.6".to_owned()),
120 "393297" => Some("4.6".to_owned()),
121 "394254" => Some("4.6.1".to_owned()),
122 "394271" => Some("4.6.1".to_owned()),
123 "394802" => Some("4.6.2".to_owned()),
124 "394806" => Some("4.6.2".to_owned()),
125 "460798" => Some("4.7".to_owned()),
126 "460805" => Some("4.7".to_owned()),
127 "461308" => Some("4.7.1".to_owned()),
128 "461310" => Some("4.7.1".to_owned()),
129 "461808" => Some("4.7.2".to_owned()),
130 "461814" => Some("4.7.2".to_owned()),
131 "528040" => Some("4.8".to_owned()),
132 "528049" => Some("4.8".to_owned()),
133 "528209" => Some("4.8".to_owned()),
134 "528372" => Some("4.8".to_owned()),
135 "528449" => Some("4.8".to_owned()),
136 _ => None,
137 };
138
139 if let Some(version) = version {
140 runtime.version = version.into();
141 }
142 }
143
144 if runtime.runtime.value().is_none()
147 && let (Some(name), Some(version)) = (runtime.name.value(), runtime.version.value())
148 {
149 runtime.runtime = Annotated::from(format!("{name} {version}"));
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 Context::Trace(trace) => {
387 trace.status.get_or_insert_with(|| SpanStatus::Unknown);
388 }
389 _ => {}
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use relay_event_schema::protocol::{Headers, LenientString, PairList};
396 use relay_protocol::SerializableAnnotated;
397 use similar_asserts::assert_eq;
398
399 use super::*;
400
401 macro_rules! assert_json_context {
402 ($ctx:expr, $($tt:tt)*) => {
403 insta::assert_json_snapshot!(SerializableAnnotated(&Annotated::new($ctx)), $($tt)*)
404
405 };
406 }
407
408 #[test]
409 fn test_get_product_name() {
410 assert_eq!(
411 ANDROID_MODEL_NAMES.get("NE2211").unwrap(),
412 &"OnePlus 10 Pro 5G"
413 );
414
415 assert_eq!(
416 ANDROID_MODEL_NAMES.get("MP04").unwrap(),
417 &"A13 Pro Max 5G EEA"
418 );
419
420 assert_eq!(ANDROID_MODEL_NAMES.get("ZT216_7").unwrap(), &"zyrex");
421
422 assert!(ANDROID_MODEL_NAMES.get("foobar").is_none());
423 }
424
425 #[test]
426 fn test_dotnet_framework_48_without_build_id() {
427 let mut runtime = RuntimeContext {
428 raw_description: ".NET Framework 4.8.4250.0".to_owned().into(),
429 ..RuntimeContext::default()
430 };
431
432 normalize_runtime_context(&mut runtime);
433 assert_json_context!(runtime, @r###"
434 {
435 "runtime": ".NET Framework 4.8.4250.0",
436 "name": ".NET Framework",
437 "version": "4.8.4250.0",
438 "raw_description": ".NET Framework 4.8.4250.0"
439 }
440 "###);
441 }
442
443 #[test]
444 fn test_dotnet_framework_472() {
445 let mut runtime = RuntimeContext {
446 raw_description: ".NET Framework 4.7.3056.0".to_owned().into(),
447 build: LenientString("461814".to_owned()).into(),
448 ..RuntimeContext::default()
449 };
450
451 normalize_runtime_context(&mut runtime);
452 assert_json_context!(runtime, @r###"
453 {
454 "runtime": ".NET Framework 4.7.2",
455 "name": ".NET Framework",
456 "version": "4.7.2",
457 "build": "461814",
458 "raw_description": ".NET Framework 4.7.3056.0"
459 }
460 "###);
461 }
462
463 #[test]
464 fn test_dotnet_framework_future_version() {
465 let mut runtime = RuntimeContext {
466 raw_description: ".NET Framework 200.0".to_owned().into(),
467 build: LenientString("999999".to_owned()).into(),
468 ..RuntimeContext::default()
469 };
470
471 normalize_runtime_context(&mut runtime);
473 assert_json_context!(runtime, @r###"
474 {
475 "runtime": ".NET Framework 200.0",
476 "name": ".NET Framework",
477 "version": "200.0",
478 "build": "999999",
479 "raw_description": ".NET Framework 200.0"
480 }
481 "###);
482 }
483
484 #[test]
485 fn test_dotnet_native() {
486 let mut runtime = RuntimeContext {
487 raw_description: ".NET Native 2.0".to_owned().into(),
488 ..RuntimeContext::default()
489 };
490
491 normalize_runtime_context(&mut runtime);
492 assert_json_context!(runtime, @r###"
493 {
494 "runtime": ".NET Native 2.0",
495 "name": ".NET Native",
496 "version": "2.0",
497 "raw_description": ".NET Native 2.0"
498 }
499 "###);
500 }
501
502 #[test]
503 fn test_dotnet_core() {
504 let mut runtime = RuntimeContext {
505 raw_description: ".NET Core 2.0".to_owned().into(),
506 ..RuntimeContext::default()
507 };
508
509 normalize_runtime_context(&mut runtime);
510 assert_json_context!(runtime, @r###"
511 {
512 "runtime": ".NET Core 2.0",
513 "name": ".NET Core",
514 "version": "2.0",
515 "raw_description": ".NET Core 2.0"
516 }
517 "###);
518 }
519
520 #[test]
521 fn test_windows_7_or_server_2008() {
522 let mut os = OsContext {
524 raw_description: "Microsoft Windows NT 6.1.7601 Service Pack 1"
525 .to_owned()
526 .into(),
527 ..OsContext::default()
528 };
529
530 normalize_os_context(&mut os);
531 assert_json_context!(os, @r###"
532 {
533 "os": "Windows 7",
534 "name": "Windows",
535 "version": "7",
536 "build": "7601",
537 "raw_description": "Microsoft Windows NT 6.1.7601 Service Pack 1"
538 }
539 "###);
540 }
541
542 #[test]
543 fn test_windows_8_or_server_2012_or_later() {
544 let mut os = OsContext {
549 raw_description: "Microsoft Windows NT 6.2.9200.0".to_owned().into(),
550 ..OsContext::default()
551 };
552
553 normalize_os_context(&mut os);
554 assert_json_context!(os, @r###"
555 {
556 "os": "Windows 8",
557 "name": "Windows",
558 "version": "8",
559 "build": "9200",
560 "raw_description": "Microsoft Windows NT 6.2.9200.0"
561 }
562 "###);
563 }
564
565 #[test]
566 fn test_windows_10() {
567 let mut os = OsContext {
570 raw_description: "Microsoft Windows 10.0.16299".to_owned().into(),
571 ..OsContext::default()
572 };
573
574 normalize_os_context(&mut os);
575 assert_json_context!(os, @r###"
576 {
577 "os": "Windows 10",
578 "name": "Windows",
579 "version": "10",
580 "build": "16299",
581 "raw_description": "Microsoft Windows 10.0.16299"
582 }
583 "###);
584 }
585
586 #[test]
587 fn test_windows_11() {
588 let mut os = OsContext {
590 raw_description: "Microsoft Windows 10.0.22000".to_owned().into(),
591 ..OsContext::default()
592 };
593
594 normalize_os_context(&mut os);
595 assert_json_context!(os, @r###"
596 {
597 "os": "Windows 11",
598 "name": "Windows",
599 "version": "11",
600 "build": "22000",
601 "raw_description": "Microsoft Windows 10.0.22000"
602 }
603 "###);
604 }
605
606 #[test]
607 fn test_windows_11_future1() {
608 let mut os = OsContext {
610 raw_description: "Microsoft Windows 10.0.22001".to_owned().into(),
611 ..OsContext::default()
612 };
613
614 normalize_os_context(&mut os);
615 assert_json_context!(os, @r###"
616 {
617 "os": "Windows 11",
618 "name": "Windows",
619 "version": "11",
620 "build": "22001",
621 "raw_description": "Microsoft Windows 10.0.22001"
622 }
623 "###);
624 }
625
626 #[test]
627 fn test_windows_11_future2() {
628 let mut os = OsContext {
630 raw_description: "Microsoft Windows 10.1.23456".to_owned().into(),
631 ..OsContext::default()
632 };
633
634 normalize_os_context(&mut os);
635 assert_json_context!(os, @r###"
636 {
637 "os": "Windows 10.1.23456",
638 "name": "Windows",
639 "version": "10.1.23456",
640 "build": "23456",
641 "raw_description": "Microsoft Windows 10.1.23456"
642 }
643 "###);
644 }
645
646 #[test]
647 fn test_macos_os_version() {
648 let mut os = OsContext {
650 raw_description: "Unix 17.5.0.0".to_owned().into(),
651 ..OsContext::default()
652 };
653
654 normalize_os_context(&mut os);
655 assert_json_context!(os, @r###"
656 {
657 "os": "Unix",
658 "name": "Unix",
659 "kernel_version": "17.5.0",
660 "raw_description": "Unix 17.5.0.0"
661 }
662 "###);
663 }
664
665 #[test]
666 fn test_macos_runtime() {
667 let mut os = OsContext {
669 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(),
670 ..OsContext::default()
671 };
672
673 normalize_os_context(&mut os);
674 assert_json_context!(os, @r###"
675 {
676 "os": "Darwin",
677 "name": "Darwin",
678 "kernel_version": "17.5.0",
679 "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"
680 }
681 "###);
682 }
683
684 #[test]
685 fn test_centos_os_version() {
686 let mut os = OsContext {
688 raw_description: "Unix 3.10.0.693".to_owned().into(),
689 ..OsContext::default()
690 };
691
692 normalize_os_context(&mut os);
693 assert_json_context!(os, @r###"
694 {
695 "os": "Unix",
696 "name": "Unix",
697 "kernel_version": "3.10.0.693",
698 "raw_description": "Unix 3.10.0.693"
699 }
700 "###);
701 }
702
703 #[test]
704 fn test_centos_runtime_info() {
705 let mut os = OsContext {
707 raw_description: "Linux 3.10.0-693.21.1.el7.x86_64 #1 SMP Wed Mar 7 19:03:37 UTC 2018"
708 .to_owned()
709 .into(),
710 ..OsContext::default()
711 };
712
713 normalize_os_context(&mut os);
714 assert_json_context!(os, @r###"
715 {
716 "os": "Linux",
717 "name": "Linux",
718 "kernel_version": "3.10.0",
719 "raw_description": "Linux 3.10.0-693.21.1.el7.x86_64 #1 SMP Wed Mar 7 19:03:37 UTC 2018"
720 }
721 "###);
722 }
723
724 #[test]
725 fn test_wsl_ubuntu() {
726 let mut os = OsContext {
729 raw_description: "Linux 4.4.0-43-Microsoft #1-Microsoft Wed Dec 31 14:42:53 PST 2014"
730 .to_owned()
731 .into(),
732 ..OsContext::default()
733 };
734
735 normalize_os_context(&mut os);
736 assert_json_context!(os, @r###"
737 {
738 "os": "Linux",
739 "name": "Linux",
740 "kernel_version": "4.4.0",
741 "raw_description": "Linux 4.4.0-43-Microsoft #1-Microsoft Wed Dec 31 14:42:53 PST 2014"
742 }
743 "###);
744 }
745
746 #[test]
747 fn test_macos_with_build() {
748 let mut os = OsContext {
749 raw_description: "Mac OS X 10.14.2 (18C54)".to_owned().into(),
750 ..OsContext::default()
751 };
752
753 normalize_os_context(&mut os);
754 assert_json_context!(os, @r###"
755 {
756 "os": "macOS 10.14.2",
757 "name": "macOS",
758 "version": "10.14.2",
759 "build": "18C54",
760 "raw_description": "Mac OS X 10.14.2 (18C54)"
761 }
762 "###);
763 }
764
765 #[test]
766 fn test_macos_without_build() {
767 let mut os = OsContext {
768 raw_description: "Mac OS X 10.14.2".to_owned().into(),
769 ..OsContext::default()
770 };
771
772 normalize_os_context(&mut os);
773 assert_json_context!(os, @r###"
774 {
775 "os": "macOS 10.14.2",
776 "name": "macOS",
777 "version": "10.14.2",
778 "raw_description": "Mac OS X 10.14.2"
779 }
780 "###);
781 }
782
783 #[test]
784 fn test_name_not_overwritten() {
785 let mut os = OsContext {
786 name: "Properly defined name".to_owned().into(),
787 raw_description: "Linux 4.4.0".to_owned().into(),
788 ..OsContext::default()
789 };
790
791 normalize_os_context(&mut os);
792 assert_json_context!(os, @r###"
793 {
794 "os": "Properly defined name",
795 "name": "Properly defined name",
796 "raw_description": "Linux 4.4.0"
797 }
798 "###);
799 }
800
801 #[test]
802 fn test_version_not_overwritten() {
803 let mut os = OsContext {
804 version: "Properly defined version".to_owned().into(),
805 raw_description: "Linux 4.4.0".to_owned().into(),
806 ..OsContext::default()
807 };
808
809 normalize_os_context(&mut os);
810 assert_json_context!(os, @r###"
811 {
812 "version": "Properly defined version",
813 "raw_description": "Linux 4.4.0"
814 }
815 "###);
816 }
817
818 #[test]
819 fn test_no_name() {
820 let mut os = OsContext::default();
821
822 normalize_os_context(&mut os);
823 assert_json_context!(os, @"{}");
824 }
825
826 #[test]
827 fn test_unity_mac_os() {
828 let mut os = OsContext {
829 raw_description: "Mac OS X 10.16.0".to_owned().into(),
830 ..OsContext::default()
831 };
832 normalize_os_context(&mut os);
833 assert_json_context!(os, @r###"
834 {
835 "os": "macOS 10.16.0",
836 "name": "macOS",
837 "version": "10.16.0",
838 "raw_description": "Mac OS X 10.16.0"
839 }
840 "###);
841 }
842
843 #[test]
844 fn test_unity_ios() {
845 let mut os = OsContext {
846 raw_description: "iOS 17.5.1".to_owned().into(),
847 ..OsContext::default()
848 };
849
850 normalize_os_context(&mut os);
851 assert_json_context!(os, @r###"
852 {
853 "os": "iOS 17.5.1",
854 "name": "iOS",
855 "version": "17.5.1",
856 "raw_description": "iOS 17.5.1"
857 }
858 "###);
859 }
860
861 #[test]
862 fn test_unity_ipados() {
863 let mut os = OsContext {
864 raw_description: "iPadOS 17.5.1".to_owned().into(),
865 ..OsContext::default()
866 };
867
868 normalize_os_context(&mut os);
869 assert_json_context!(os, @r###"
870 {
871 "os": "iPadOS 17.5.1",
872 "name": "iPadOS",
873 "version": "17.5.1",
874 "raw_description": "iPadOS 17.5.1"
875 }
876 "###);
877 }
878
879 #[test]
881 fn test_unity_windows_os() {
882 let mut os = OsContext {
883 raw_description: "Windows 10 (10.0.19042) 64bit".to_owned().into(),
884 ..OsContext::default()
885 };
886
887 normalize_os_context(&mut os);
888 assert_json_context!(os, @r###"
889 {
890 "os": "Windows 10",
891 "name": "Windows",
892 "version": "10",
893 "build": "19042",
894 "raw_description": "Windows 10 (10.0.19042) 64bit"
895 }
896 "###);
897 }
898
899 #[test]
900 fn test_unity_android_os() {
901 let mut os = OsContext {
902 raw_description: "Android OS 11 / API-30 (RP1A.201005.001/2107031736)"
903 .to_owned()
904 .into(),
905 ..OsContext::default()
906 };
907
908 normalize_os_context(&mut os);
909 assert_json_context!(os, @r###"
910 {
911 "os": "Android 11",
912 "name": "Android",
913 "version": "11",
914 "raw_description": "Android OS 11 / API-30 (RP1A.201005.001/2107031736)"
915 }
916 "###);
917 }
918
919 #[test]
920 fn test_unity_android_api_version() {
921 let description = "Android OS 11 / API-30 (RP1A.201005.001/2107031736)";
922 assert_eq!(Some("30"), get_android_api_version(description));
923 }
924
925 #[test]
926 fn test_unreal_windows_os() {
927 let mut os = OsContext {
928 raw_description: "Windows 10".to_owned().into(),
929 ..OsContext::default()
930 };
931
932 normalize_os_context(&mut os);
933 assert_json_context!(os, @r###"
934 {
935 "os": "Windows 10",
936 "name": "Windows",
937 "version": "10",
938 "raw_description": "Windows 10"
939 }
940 "###);
941 }
942
943 #[test]
944 fn test_linux_5_11() {
945 let mut os = OsContext {
946 raw_description: "Linux 5.11 Ubuntu 20.04 64bit".to_owned().into(),
947 ..OsContext::default()
948 };
949
950 normalize_os_context(&mut os);
951 assert_json_context!(os, @r###"
952 {
953 "os": "Ubuntu 20.04",
954 "name": "Ubuntu",
955 "version": "20.04",
956 "kernel_version": "5.11",
957 "raw_description": "Linux 5.11 Ubuntu 20.04 64bit"
958 }
959 "###);
960 }
961
962 #[test]
963 fn test_unity_nintendo_switch() {
964 let mut os = OsContext {
966 raw_description: "Nintendo Switch".to_owned().into(),
967 ..OsContext::default()
968 };
969
970 normalize_os_context(&mut os);
971 assert_json_context!(os, @r###"
972 {
973 "os": "Nintendo OS",
974 "name": "Nintendo OS",
975 "raw_description": "Nintendo Switch"
976 }
977 "###);
978 }
979
980 #[test]
981 fn test_android_4_4_2() {
982 let mut os = OsContext {
983 raw_description: "Android OS 4.4.2 / API-19 (KOT49H/A536_S186_150813_ROW)"
984 .to_owned()
985 .into(),
986 ..OsContext::default()
987 };
988
989 normalize_os_context(&mut os);
990 assert_json_context!(os, @r###"
991 {
992 "os": "Android 4.4.2",
993 "name": "Android",
994 "version": "4.4.2",
995 "raw_description": "Android OS 4.4.2 / API-19 (KOT49H/A536_S186_150813_ROW)"
996 }
997 "###);
998 }
999
1000 #[test]
1001 fn test_infer_json() {
1002 let mut response = ResponseContext {
1003 data: Annotated::from(Value::String(r#"{"foo":"bar"}"#.to_owned())),
1004 ..ResponseContext::default()
1005 };
1006
1007 normalize_response(&mut response);
1008 assert_json_context!(response, @r###"
1009 {
1010 "data": {
1011 "foo": "bar"
1012 },
1013 "inferred_content_type": "application/json"
1014 }
1015 "###);
1016 }
1017
1018 #[test]
1019 fn test_broken_json_with_fallback() {
1020 let mut response = ResponseContext {
1021 data: Annotated::from(Value::String(r#"{"foo":"b"#.to_owned())),
1022 headers: Annotated::from(Headers(PairList(vec![Annotated::new((
1023 Annotated::new("Content-Type".to_owned().into()),
1024 Annotated::new("text/plain; encoding=utf-8".to_owned().into()),
1025 ))]))),
1026 ..ResponseContext::default()
1027 };
1028
1029 normalize_response(&mut response);
1030 assert_json_context!(response, @r###"
1031 {
1032 "headers": [
1033 [
1034 "Content-Type",
1035 "text/plain; encoding=utf-8"
1036 ]
1037 ],
1038 "data": "{\"foo\":\"b",
1039 "inferred_content_type": "text/plain"
1040 }
1041 "###);
1042 }
1043
1044 #[test]
1045 fn test_broken_json_without_fallback() {
1046 let mut response = ResponseContext {
1047 data: Annotated::from(Value::String(r#"{"foo":"b"#.to_owned())),
1048 ..ResponseContext::default()
1049 };
1050
1051 normalize_response(&mut response);
1052 assert_json_context!(response, @r###"
1053 {
1054 "data": "{\"foo\":\"b"
1055 }
1056 "###);
1057 }
1058
1059 #[test]
1060 fn test_os_computed_context() {
1061 let mut os = OsContext {
1062 name: "Windows".to_owned().into(),
1063 version: "10".to_owned().into(),
1064 ..OsContext::default()
1065 };
1066
1067 normalize_os_context(&mut os);
1068 assert_json_context!(os, @r###"
1069 {
1070 "os": "Windows 10",
1071 "name": "Windows",
1072 "version": "10"
1073 }
1074 "###);
1075 }
1076
1077 #[test]
1078 fn test_os_computed_context_missing_version() {
1079 let mut os = OsContext {
1080 name: "Windows".to_owned().into(),
1081 ..OsContext::default()
1082 };
1083
1084 normalize_os_context(&mut os);
1085 assert_json_context!(os, @r###"
1086 {
1087 "os": "Windows",
1088 "name": "Windows"
1089 }
1090 "###);
1091 }
1092
1093 #[test]
1094 fn test_runtime_computed_context() {
1095 let mut runtime = RuntimeContext {
1096 name: "Python".to_owned().into(),
1097 version: "3.9.0".to_owned().into(),
1098 ..RuntimeContext::default()
1099 };
1100
1101 normalize_runtime_context(&mut runtime);
1102 assert_json_context!(runtime, @r###"
1103 {
1104 "runtime": "Python 3.9.0",
1105 "name": "Python",
1106 "version": "3.9.0"
1107 }
1108 "###);
1109 }
1110
1111 #[test]
1112 fn test_runtime_computed_context_missing_version() {
1113 let mut runtime = RuntimeContext {
1114 name: "Python".to_owned().into(),
1115 ..RuntimeContext::default()
1116 };
1117
1118 normalize_runtime_context(&mut runtime);
1119 assert_json_context!(runtime, @r###"
1120 {
1121 "name": "Python"
1122 }
1123 "###);
1124 }
1125
1126 #[test]
1127 fn test_browser_computed_context() {
1128 let mut browser = BrowserContext {
1129 name: "Firefox".to_owned().into(),
1130 version: "89.0".to_owned().into(),
1131 ..BrowserContext::default()
1132 };
1133
1134 normalize_browser_context(&mut browser);
1135 assert_json_context!(browser, @r###"
1136 {
1137 "browser": "Firefox 89.0",
1138 "name": "Firefox",
1139 "version": "89.0"
1140 }
1141 "###);
1142 }
1143
1144 #[test]
1145 fn test_browser_computed_context_missing_version() {
1146 let mut browser = BrowserContext {
1147 name: "Firefox".to_owned().into(),
1148 ..BrowserContext::default()
1149 };
1150
1151 normalize_browser_context(&mut browser);
1152 assert_json_context!(browser, @r###"
1153 {
1154 "browser": "Firefox",
1155 "name": "Firefox"
1156 }
1157 "###);
1158 }
1159}