1use anyhow::Result;
2use std::fmt;
3
4#[cfg(target_os = "macos")]
5const PATH: &str = "/System/Library/CoreServices/SystemVersion.plist";
6
7#[derive(Debug, Clone, PartialEq)]
8pub struct MacOS {
9 pub version: String,
10}
11
12impl MacOS {
13 #[cfg(target_os = "macos")]
14 pub fn detect() -> Result<MacOS> {
15 let file = std::fs::read_to_string(PATH)?;
16 parse(&file)
17 }
18
19 #[cfg(not(target_os = "macos"))]
20 pub fn detect() -> Result<MacOS> {
21 unreachable!()
22 }
23
24 #[cfg(all(feature = "tokio", target_os = "macos"))]
25 pub async fn detect_async() -> Result<MacOS> {
26 let file = tokio::fs::read_to_string(PATH).await?;
27 parse(&file)
28 }
29
30 #[cfg(all(feature = "tokio", not(target_os = "macos")))]
31 pub async fn detect_async() -> Result<MacOS> {
32 unreachable!()
33 }
34}
35
36impl fmt::Display for MacOS {
37 fn fmt(&self, w: &mut fmt::Formatter<'_>) -> fmt::Result {
38 write!(w, "macOS {}", self.version)
39 }
40}
41
42#[cfg(target_os = "macos")]
43fn parse(file: &str) -> Result<MacOS> {
44 use anyhow::Error;
45
46 let cur = std::io::Cursor::new(file.as_bytes());
47 let v = plist::Value::from_reader(cur)?;
48
49 let version = v
50 .as_dictionary()
51 .ok_or_else(|| Error::msg("SystemVersion.plist is not a dictionary"))?
52 .get("ProductVersion")
53 .ok_or_else(|| Error::msg("ProductVersion is missing"))?;
54
55 let version = version
56 .as_string()
57 .ok_or_else(|| Error::msg("Version is not a string"))?
58 .to_string();
59
60 Ok(MacOS { version })
61}
62
63#[cfg(test)]
64mod tests {
65 #[cfg(target_os = "macos")]
66 use super::*;
67
68 #[test]
69 #[cfg(target_os = "macos")]
70 fn detect_macos() {
71 let version = parse(r#"<?xml version="1.0" encoding="UTF-8"?>
72<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
73<plist version="1.0">
74<dict>
75 <key>ProductBuildVersion</key>
76 <string>17G11023</string>
77 <key>ProductCopyright</key>
78 <string>1983-2020 Apple Inc.</string>
79 <key>ProductName</key>
80 <string>Mac OS X</string>
81 <key>ProductUserVisibleVersion</key>
82 <string>10.13.6</string>
83 <key>ProductVersion</key>
84 <string>10.13.6</string>
85</dict>
86</plist>
87"#).unwrap();
88 assert_eq!(
89 MacOS {
90 version: "10.13.6".to_string(),
91 },
92 version
93 );
94 }
95}