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 )?Windows (NT )?(?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_string()).into();
98 runtime.version = captures
99 .name("version")
100 .map(|m| m.as_str().to_string())
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_string()),
114 "378675" => Some("4.5.1".to_string()),
115 "378758" => Some("4.5.1".to_string()),
116 "379893" => Some("4.5.2".to_string()),
117 "393295" => Some("4.6".to_string()),
118 "393297" => Some("4.6".to_string()),
119 "394254" => Some("4.6.1".to_string()),
120 "394271" => Some("4.6.1".to_string()),
121 "394802" => Some("4.6.2".to_string()),
122 "394806" => Some("4.6.2".to_string()),
123 "460798" => Some("4.7".to_string()),
124 "460805" => Some("4.7".to_string()),
125 "461308" => Some("4.7.1".to_string()),
126 "461310" => Some("4.7.1".to_string()),
127 "461808" => Some("4.7.2".to_string()),
128 "461814" => Some("4.7.2".to_string()),
129 "528040" => Some("4.8".to_string()),
130 "528049" => Some("4.8".to_string()),
131 "528209" => Some("4.8".to_string()),
132 "528372" => Some("4.8".to_string()),
133 "528449" => Some("4.8".to_string()),
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
182#[allow(dead_code)]
183pub fn get_android_api_version(description: &str) -> Option<&str> {
187 if let Some(captures) = OS_ANDROID_REGEX.captures(description) {
188 captures.name("api").map(|m| m.as_str())
189 } else {
190 None
191 }
192}
193
194fn normalize_os_context(os: &mut OsContext) {
195 if os.name.value().is_some() || os.version.value().is_some() {
196 compute_os_context(os);
197 return;
198 }
199
200 if let Some(raw_description) = os.raw_description.as_str() {
201 if let Some((version, build_number)) = get_windows_version(raw_description) {
202 os.name = "Windows".to_string().into();
203 os.version = version.to_string().into();
204 if os.build.is_empty() {
205 os.build.set_value(Some(build_number.to_string().into()));
207 }
208 } else if let Some(captures) = OS_MACOS_REGEX.captures(raw_description) {
209 os.name = "macOS".to_string().into();
210 os.version = captures
211 .name("version")
212 .map(|m| m.as_str().to_string())
213 .into();
214 os.build = captures
215 .name("build")
216 .map(|m| m.as_str().to_string().into())
217 .into();
218 } else if let Some(captures) = OS_IOS_REGEX.captures(raw_description) {
219 os.name = "iOS".to_string().into();
220 os.version = captures
221 .name("version")
222 .map(|m| m.as_str().to_string())
223 .into();
224 os.build = captures
225 .name("build")
226 .map(|m| m.as_str().to_string().into())
227 .into();
228 } else if let Some(captures) = OS_IPADOS_REGEX.captures(raw_description) {
229 os.name = "iPadOS".to_string().into();
230 os.version = captures
231 .name("version")
232 .map(|m| m.as_str().to_string())
233 .into();
234 os.build = captures
235 .name("build")
236 .map(|m| m.as_str().to_string().into())
237 .into();
238 } else if let Some(captures) = OS_LINUX_DISTRO_UNAME_REGEX.captures(raw_description) {
239 os.name = captures.name("name").map(|m| m.as_str().to_string()).into();
240 os.version = captures
241 .name("version")
242 .map(|m| m.as_str().to_string())
243 .into();
244 os.kernel_version = captures
245 .name("kernel_version")
246 .map(|m| m.as_str().to_string())
247 .into();
248 } else if let Some(captures) = OS_UNAME_REGEX.captures(raw_description) {
249 os.name = captures.name("name").map(|m| m.as_str().to_string()).into();
250 os.kernel_version = captures
251 .name("kernel_version")
252 .map(|m| m.as_str().to_string())
253 .into();
254 } else if let Some(captures) = OS_ANDROID_REGEX.captures(raw_description) {
255 os.name = "Android".to_string().into();
256 os.version = captures
257 .name("version")
258 .map(|m| m.as_str().to_string())
259 .into();
260 }
261 }
262
263 compute_os_context(os);
264}
265
266fn compute_os_context(os: &mut OsContext) {
267 if os.os.value().is_none() {
270 if let (Some(name), Some(version)) = (os.name.value(), os.version.value()) {
271 os.os = Annotated::from(format!("{} {}", name, version));
272 }
273 }
274}
275
276fn normalize_browser_context(browser: &mut BrowserContext) {
277 if browser.browser.value().is_none() {
280 if let (Some(name), Some(version)) = (browser.name.value(), browser.version.value()) {
281 browser.browser = Annotated::from(format!("{} {}", name, version));
282 }
283 }
284}
285
286fn parse_raw_response_data(response: &ResponseContext) -> Option<(&'static str, Value)> {
287 let raw = response.data.as_str()?;
288
289 serde_json::from_str(raw)
290 .ok()
291 .map(|value| ("application/json", value))
292}
293
294fn normalize_response_data(response: &mut ResponseContext) {
295 if let Some((content_type, parsed_data)) = parse_raw_response_data(response) {
300 response.data.set_value(Some(parsed_data));
303 response.inferred_content_type = Annotated::from(content_type.to_string());
304 } else {
305 response.inferred_content_type = response
306 .headers
307 .value()
308 .and_then(|headers| headers.get_header("Content-Type"))
309 .map(|value| value.split(';').next().unwrap_or(value).to_string())
310 .into();
311 }
312}
313
314fn normalize_response(response: &mut ResponseContext) {
315 normalize_response_data(response);
316
317 let headers = match response.headers.value_mut() {
318 Some(headers) => headers,
319 None => return,
320 };
321
322 if response.cookies.value().is_some() {
323 headers.remove("Set-Cookie");
324 return;
325 }
326
327 let cookie_header = match headers.get_header("Set-Cookie") {
328 Some(header) => header,
329 None => return,
330 };
331
332 if let Ok(new_cookies) = Cookies::parse(cookie_header) {
333 response.cookies = Annotated::from(new_cookies);
334 headers.remove("Set-Cookie");
335 }
336}
337
338pub fn normalize_context(context: &mut Context) {
340 match context {
341 Context::Runtime(runtime) => normalize_runtime_context(runtime),
342 Context::Os(os) => normalize_os_context(os),
343 Context::Browser(browser) => normalize_browser_context(browser),
344 Context::Response(response) => normalize_response(response),
345 Context::Device(device) => {
346 if let Some(product_name) = device
347 .as_ref()
348 .model
349 .value()
350 .and_then(|model| ANDROID_MODEL_NAMES.get(model.as_str()))
351 {
352 device.name.set_value(Some(product_name.to_string()))
353 }
354 }
355 _ => {}
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use relay_event_schema::protocol::{Headers, LenientString, PairList};
362 use relay_protocol::Object;
363 use similar_asserts::assert_eq;
364
365 use super::*;
366
367 #[test]
368 fn test_get_product_name() {
369 assert_eq!(
370 ANDROID_MODEL_NAMES.get("NE2211").unwrap(),
371 &"OnePlus 10 Pro 5G"
372 );
373
374 assert_eq!(
375 ANDROID_MODEL_NAMES.get("MP04").unwrap(),
376 &"A13 Pro Max 5G EEA"
377 );
378
379 assert_eq!(ANDROID_MODEL_NAMES.get("ZT216_7").unwrap(), &"zyrex");
380
381 assert!(ANDROID_MODEL_NAMES.get("foobar").is_none());
382 }
383
384 #[test]
385 fn test_dotnet_framework_48_without_build_id() {
386 let mut runtime = RuntimeContext {
387 raw_description: ".NET Framework 4.8.4250.0".to_string().into(),
388 ..RuntimeContext::default()
389 };
390
391 normalize_runtime_context(&mut runtime);
392 assert_eq!(Some(".NET Framework"), runtime.name.as_str());
393 assert_eq!(Some("4.8.4250.0"), runtime.version.as_str());
394 }
395
396 #[test]
397 fn test_dotnet_framework_472() {
398 let mut runtime = RuntimeContext {
399 raw_description: ".NET Framework 4.7.3056.0".to_string().into(),
400 build: LenientString("461814".to_string()).into(),
401 ..RuntimeContext::default()
402 };
403
404 normalize_runtime_context(&mut runtime);
405 assert_eq!(Some(".NET Framework"), runtime.name.as_str());
406 assert_eq!(Some("4.7.2"), runtime.version.as_str());
407 }
408
409 #[test]
410 fn test_dotnet_framework_future_version() {
411 let mut runtime = RuntimeContext {
412 raw_description: ".NET Framework 200.0".to_string().into(),
413 build: LenientString("999999".to_string()).into(),
414 ..RuntimeContext::default()
415 };
416
417 normalize_runtime_context(&mut runtime);
419 assert_eq!(Some(".NET Framework"), runtime.name.as_str());
420 assert_eq!(Some("200.0"), runtime.version.as_str());
421 }
422
423 #[test]
424 fn test_dotnet_native() {
425 let mut runtime = RuntimeContext {
426 raw_description: ".NET Native 2.0".to_string().into(),
427 ..RuntimeContext::default()
428 };
429
430 normalize_runtime_context(&mut runtime);
431 assert_eq!(Some(".NET Native"), runtime.name.as_str());
432 assert_eq!(Some("2.0"), runtime.version.as_str());
433 }
434
435 #[test]
436 fn test_dotnet_core() {
437 let mut runtime = RuntimeContext {
438 raw_description: ".NET Core 2.0".to_string().into(),
439 ..RuntimeContext::default()
440 };
441
442 normalize_runtime_context(&mut runtime);
443 assert_eq!(Some(".NET Core"), runtime.name.as_str());
444 assert_eq!(Some("2.0"), runtime.version.as_str());
445 }
446
447 #[test]
448 fn test_windows_7_or_server_2008() {
449 let mut os = OsContext {
451 raw_description: "Microsoft Windows NT 6.1.7601 Service Pack 1"
452 .to_string()
453 .into(),
454 ..OsContext::default()
455 };
456
457 normalize_os_context(&mut os);
458 assert_eq!(Some("Windows"), os.name.as_str());
459 assert_eq!(Some("7"), os.version.as_str());
460 }
461
462 #[test]
463 fn test_windows_8_or_server_2012_or_later() {
464 let mut os = OsContext {
469 raw_description: "Microsoft Windows NT 6.2.9200.0".to_string().into(),
470 ..OsContext::default()
471 };
472
473 normalize_os_context(&mut os);
474 assert_eq!(Some("Windows"), os.name.as_str());
475 assert_eq!(Some("8"), os.version.as_str());
476 }
477
478 #[test]
479 fn test_windows_10() {
480 let mut os = OsContext {
483 raw_description: "Microsoft Windows 10.0.16299".to_string().into(),
484 ..OsContext::default()
485 };
486
487 normalize_os_context(&mut os);
488 assert_eq!(Some("Windows"), os.name.as_str());
489 assert_eq!(Some("10"), os.version.as_str());
490 }
491
492 #[test]
493 fn test_windows_11() {
494 let mut os = OsContext {
496 raw_description: "Microsoft Windows 10.0.22000".to_string().into(),
497 ..OsContext::default()
498 };
499
500 normalize_os_context(&mut os);
501 assert_eq!(Some("Windows"), os.name.as_str());
502 assert_eq!(Some("11"), os.version.as_str());
503 }
504
505 #[test]
506 fn test_windows_11_future1() {
507 let mut os = OsContext {
509 raw_description: "Microsoft Windows 10.0.22001".to_string().into(),
510 ..OsContext::default()
511 };
512
513 normalize_os_context(&mut os);
514 assert_eq!(Some("Windows"), os.name.as_str());
515 assert_eq!(Some("11"), os.version.as_str());
516 }
517
518 #[test]
519 fn test_windows_11_future2() {
520 let mut os = OsContext {
522 raw_description: "Microsoft Windows 10.1.23456".to_string().into(),
523 ..OsContext::default()
524 };
525
526 normalize_os_context(&mut os);
527 assert_eq!(Some("Windows"), os.name.as_str());
528 assert_eq!(Some("10.1.23456"), os.version.as_str());
529 }
530
531 #[test]
532 fn test_macos_os_version() {
533 let mut os = OsContext {
535 raw_description: "Unix 17.5.0.0".to_string().into(),
536 ..OsContext::default()
537 };
538
539 normalize_os_context(&mut os);
540 assert_eq!(Some("Unix"), os.name.as_str());
541 assert_eq!(Some("17.5.0"), os.kernel_version.as_str());
542 }
543
544 #[test]
545 fn test_macos_runtime() {
546 let mut os = OsContext {
548 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_string().into(),
549 ..OsContext::default()
550 };
551
552 normalize_os_context(&mut os);
553 assert_eq!(Some("Darwin"), os.name.as_str());
554 assert_eq!(Some("17.5.0"), os.kernel_version.as_str());
555 }
556
557 #[test]
558 fn test_centos_os_version() {
559 let mut os = OsContext {
561 raw_description: "Unix 3.10.0.693".to_string().into(),
562 ..OsContext::default()
563 };
564
565 normalize_os_context(&mut os);
566 assert_eq!(Some("Unix"), os.name.as_str());
567 assert_eq!(Some("3.10.0.693"), os.kernel_version.as_str());
568 }
569
570 #[test]
571 fn test_centos_runtime_info() {
572 let mut os = OsContext {
574 raw_description: "Linux 3.10.0-693.21.1.el7.x86_64 #1 SMP Wed Mar 7 19:03:37 UTC 2018"
575 .to_string()
576 .into(),
577 ..OsContext::default()
578 };
579
580 normalize_os_context(&mut os);
581 assert_eq!(Some("Linux"), os.name.as_str());
582 assert_eq!(Some("3.10.0"), os.kernel_version.as_str());
583 }
584
585 #[test]
586 fn test_wsl_ubuntu() {
587 let mut os = OsContext {
590 raw_description: "Linux 4.4.0-43-Microsoft #1-Microsoft Wed Dec 31 14:42:53 PST 2014"
591 .to_string()
592 .into(),
593 ..OsContext::default()
594 };
595
596 normalize_os_context(&mut os);
597 assert_eq!(Some("Linux"), os.name.as_str());
598 assert_eq!(Some("4.4.0"), os.kernel_version.as_str());
599 }
600
601 #[test]
602 fn test_macos_with_build() {
603 let mut os = OsContext {
604 raw_description: "Mac OS X 10.14.2 (18C54)".to_string().into(),
605 ..OsContext::default()
606 };
607
608 normalize_os_context(&mut os);
609 assert_eq!(Some("macOS"), os.name.as_str());
610 assert_eq!(Some("10.14.2"), os.version.as_str());
611 assert_eq!(Some("18C54"), os.build.as_str());
612 }
613
614 #[test]
615 fn test_macos_without_build() {
616 let mut os = OsContext {
617 raw_description: "Mac OS X 10.14.2".to_string().into(),
618 ..OsContext::default()
619 };
620
621 normalize_os_context(&mut os);
622 assert_eq!(Some("macOS"), os.name.as_str());
623 assert_eq!(Some("10.14.2"), os.version.as_str());
624 assert_eq!(None, os.build.value());
625 }
626
627 #[test]
628 fn test_name_not_overwritten() {
629 let mut os = OsContext {
630 name: "Properly defined name".to_string().into(),
631 raw_description: "Linux 4.4.0".to_string().into(),
632 ..OsContext::default()
633 };
634
635 normalize_os_context(&mut os);
636 assert_eq!(Some("Properly defined name"), os.name.as_str());
637 }
638
639 #[test]
640 fn test_version_not_overwritten() {
641 let mut os = OsContext {
642 version: "Properly defined version".to_string().into(),
643 raw_description: "Linux 4.4.0".to_string().into(),
644 ..OsContext::default()
645 };
646
647 normalize_os_context(&mut os);
648 assert_eq!(Some("Properly defined version"), os.version.as_str());
649 }
650
651 #[test]
652 fn test_no_name() {
653 let mut os = OsContext::default();
654
655 normalize_os_context(&mut os);
656 assert_eq!(None, os.name.value());
657 assert_eq!(None, os.version.value());
658 assert_eq!(None, os.kernel_version.value());
659 assert_eq!(None, os.raw_description.value());
660 }
661
662 #[test]
663 fn test_unity_mac_os() {
664 let mut os = OsContext {
665 raw_description: "Mac OS X 10.16.0".to_string().into(),
666 ..OsContext::default()
667 };
668 normalize_os_context(&mut os);
669 assert_eq!(Some("macOS"), os.name.as_str());
670 assert_eq!(Some("10.16.0"), os.version.as_str());
671 assert_eq!(None, os.build.value());
672 }
673
674 #[test]
675 fn test_unity_ios() {
676 let mut os = OsContext {
677 raw_description: "iOS 17.5.1".to_string().into(),
678 ..OsContext::default()
679 };
680
681 normalize_os_context(&mut os);
682 assert_eq!(Some("iOS"), os.name.as_str());
683 assert_eq!(Some("17.5.1"), os.version.as_str());
684 assert_eq!(None, os.build.value());
685 }
686
687 #[test]
688 fn test_unity_ipados() {
689 let mut os = OsContext {
690 raw_description: "iPadOS 17.5.1".to_string().into(),
691 ..OsContext::default()
692 };
693
694 normalize_os_context(&mut os);
695 assert_eq!(Some("iPadOS"), os.name.as_str());
696 assert_eq!(Some("17.5.1"), os.version.as_str());
697 assert_eq!(None, os.build.value());
698 }
699
700 #[test]
702 fn test_unity_windows_os() {
703 let mut os = OsContext {
704 raw_description: "Windows 10 (10.0.19042) 64bit".to_string().into(),
705 ..OsContext::default()
706 };
707 normalize_os_context(&mut os);
708 assert_eq!(Some("Windows"), os.name.as_str());
709 assert_eq!(Some("10"), os.version.as_str());
710 assert_eq!(Some(&LenientString("19042".to_string())), os.build.value());
711 }
712
713 #[test]
714 fn test_unity_android_os() {
715 let mut os = OsContext {
716 raw_description: "Android OS 11 / API-30 (RP1A.201005.001/2107031736)"
717 .to_string()
718 .into(),
719 ..OsContext::default()
720 };
721 normalize_os_context(&mut os);
722 assert_eq!(Some("Android"), os.name.as_str());
723 assert_eq!(Some("11"), os.version.as_str());
724 assert_eq!(None, os.build.value());
725 }
726
727 #[test]
728 fn test_unity_android_api_version() {
729 let description = "Android OS 11 / API-30 (RP1A.201005.001/2107031736)";
730 assert_eq!(Some("30"), get_android_api_version(description));
731 }
732
733 #[test]
734 fn test_linux_5_11() {
735 let mut os = OsContext {
736 raw_description: "Linux 5.11 Ubuntu 20.04 64bit".to_string().into(),
737 ..OsContext::default()
738 };
739 normalize_os_context(&mut os);
740 assert_eq!(Some("Ubuntu"), os.name.as_str());
741 assert_eq!(Some("20.04"), os.version.as_str());
742 assert_eq!(Some("5.11"), os.kernel_version.as_str());
743 assert_eq!(None, os.build.value());
744 }
745
746 #[test]
747 fn test_android_4_4_2() {
748 let mut os = OsContext {
749 raw_description: "Android OS 4.4.2 / API-19 (KOT49H/A536_S186_150813_ROW)"
750 .to_string()
751 .into(),
752 ..OsContext::default()
753 };
754 normalize_os_context(&mut os);
755 assert_eq!(Some("Android"), os.name.as_str());
756 assert_eq!(Some("4.4.2"), os.version.as_str());
757 assert_eq!(None, os.build.value());
758 }
759
760 #[test]
761 fn test_infer_json() {
762 let mut response = ResponseContext {
763 data: Annotated::from(Value::String(r#"{"foo":"bar"}"#.to_string())),
764 ..ResponseContext::default()
765 };
766
767 let mut expected_value = Object::new();
768 expected_value.insert(
769 "foo".to_string(),
770 Annotated::from(Value::String("bar".into())),
771 );
772
773 normalize_response(&mut response);
774 assert_eq!(
775 response.inferred_content_type.as_str(),
776 Some("application/json")
777 );
778 assert_eq!(response.data.value(), Some(&Value::Object(expected_value)));
779 }
780
781 #[test]
782 fn test_broken_json_with_fallback() {
783 let mut response = ResponseContext {
784 data: Annotated::from(Value::String(r#"{"foo":"b"#.to_string())),
785 headers: Annotated::from(Headers(PairList(vec![Annotated::new((
786 Annotated::new("Content-Type".to_string().into()),
787 Annotated::new("text/plain; encoding=utf-8".to_string().into()),
788 ))]))),
789 ..ResponseContext::default()
790 };
791
792 normalize_response(&mut response);
793 assert_eq!(response.inferred_content_type.as_str(), Some("text/plain"));
794 assert_eq!(response.data.as_str(), Some(r#"{"foo":"b"#));
795 }
796
797 #[test]
798 fn test_broken_json_without_fallback() {
799 let mut response = ResponseContext {
800 data: Annotated::from(Value::String(r#"{"foo":"b"#.to_string())),
801 ..ResponseContext::default()
802 };
803
804 normalize_response(&mut response);
805 assert_eq!(response.inferred_content_type.value(), None);
806 assert_eq!(response.data.as_str(), Some(r#"{"foo":"b"#));
807 }
808
809 #[test]
810 fn test_os_computed_context() {
811 let mut os = OsContext {
812 name: "Windows".to_string().into(),
813 version: "10".to_string().into(),
814 ..OsContext::default()
815 };
816
817 normalize_os_context(&mut os);
818 assert_eq!(Some("Windows 10"), os.os.as_str());
819 }
820
821 #[test]
822 fn test_os_computed_context_missing_version() {
823 let mut os = OsContext {
824 name: "Windows".to_string().into(),
825 ..OsContext::default()
826 };
827
828 normalize_os_context(&mut os);
829 assert_eq!(None, os.os.value());
830 }
831
832 #[test]
833 fn test_runtime_computed_context() {
834 let mut runtime = RuntimeContext {
835 name: "Python".to_string().into(),
836 version: "3.9.0".to_string().into(),
837 ..RuntimeContext::default()
838 };
839
840 normalize_runtime_context(&mut runtime);
841 assert_eq!(Some("Python 3.9.0"), runtime.runtime.as_str());
842 }
843
844 #[test]
845 fn test_runtime_computed_context_missing_version() {
846 let mut runtime = RuntimeContext {
847 name: "Python".to_string().into(),
848 ..RuntimeContext::default()
849 };
850
851 normalize_runtime_context(&mut runtime);
852 assert_eq!(None, runtime.runtime.value());
853 }
854
855 #[test]
856 fn test_browser_computed_context() {
857 let mut browser = BrowserContext {
858 name: "Firefox".to_string().into(),
859 version: "89.0".to_string().into(),
860 ..BrowserContext::default()
861 };
862
863 normalize_browser_context(&mut browser);
864 assert_eq!(Some("Firefox 89.0"), browser.browser.as_str());
865 }
866
867 #[test]
868 fn test_browser_computed_context_missing_version() {
869 let mut browser = BrowserContext {
870 name: "Firefox".to_string().into(),
871 ..BrowserContext::default()
872 };
873
874 normalize_browser_context(&mut browser);
875 assert_eq!(None, browser.browser.value());
876 }
877}