diff --git a/Cargo.lock b/Cargo.lock index 2cbb83215..98cd1166c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -784,21 +784,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.0" @@ -1198,16 +1183,17 @@ dependencies = [ ] [[package]] -name = "hyper-tls" -version = "0.5.0" +name = "hyper-rustls" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ - "bytes", + "futures-util", + "http", "hyper", - "native-tls", + "rustls", "tokio", - "tokio-native-tls", + "tokio-rustls", ] [[package]] @@ -1594,24 +1580,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "native-tls" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1668,50 +1636,6 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" -[[package]] -name = "openssl" -version = "0.10.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" -dependencies = [ - "bitflags 2.4.1", - "cfg-if 1.0.0", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.38", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "option-ext" version = "0.2.0" @@ -1851,12 +1775,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkg-config" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" - [[package]] name = "plain" version = "0.2.3" @@ -1868,7 +1786,6 @@ name = "platform-host" version = "0.0.1" dependencies = [ "fs-err", - "glibc_version", "goblin", "platform-info", "plist", @@ -2562,22 +2479,23 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-tls", + "hyper-rustls", "ipnet", "js-sys", "log", "mime", "mime_guess", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "system-configuration", "tokio", - "tokio-native-tls", + "tokio-rustls", "tokio-util", "tower-service", "url", @@ -2585,6 +2503,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", + "webpki-roots", "winreg", ] @@ -2651,6 +2570,20 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2676,6 +2609,37 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustls" +version = "0.21.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.4", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.15" @@ -2697,15 +2661,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "schannel" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" -dependencies = [ - "windows-sys 0.48.0", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -2733,26 +2688,13 @@ dependencies = [ ] [[package]] -name = "security-framework" -version = "2.9.2" +name = "sct" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" -dependencies = [ - "core-foundation-sys", - "libc", + "ring", + "untrusted", ] [[package]] @@ -2896,6 +2838,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "ssri" version = "9.2.0" @@ -3225,12 +3173,12 @@ dependencies = [ ] [[package]] -name = "tokio-native-tls" -version = "0.3.1" +name = "tokio-rustls" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "native-tls", + "rustls", "tokio", ] @@ -3477,6 +3425,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.4.1" @@ -3501,12 +3455,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.4" @@ -3694,6 +3642,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" + [[package]] name = "which" version = "4.4.2" diff --git a/Cargo.toml b/Cargo.toml index c5c010211..5a3ba3557 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ pyproject-toml = { version = "0.7.0" } rayon = { version = "1.8.0" } reflink-copy = { version = "0.1.10" } regex = { version = "1.9.6" } -reqwest = { version = "0.11.22", features = ["json", "gzip", "brotli", "stream"] } +reqwest = { version = "0.11.22", default-features = false, features = ["json", "gzip", "brotli", "stream", "rustls-tls"] } reqwest-middleware = { version = "0.2.3" } reqwest-retry = { version = "0.3.0" } rfc2047-decoder = { version = "1.0.1" } diff --git a/crates/gourgeist/Cargo.toml b/crates/gourgeist/Cargo.toml index d2b5e1eb5..280b3c47e 100644 --- a/crates/gourgeist/Cargo.toml +++ b/crates/gourgeist/Cargo.toml @@ -24,7 +24,7 @@ clap = { workspace = true } configparser = { workspace = true } dirs = { workspace = true } fs-err = { workspace = true } -reqwest = { workspace = true, optional = true, features = ["blocking"] } +reqwest = { workspace = true, optional = true, default-features = false, features = ["blocking"] } rayon = { workspace = true, optional = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/platform-host/Cargo.toml b/crates/platform-host/Cargo.toml index affa6dc42..45a16aa53 100644 --- a/crates/platform-host/Cargo.toml +++ b/crates/platform-host/Cargo.toml @@ -11,12 +11,11 @@ license = { workspace = true } [dependencies] fs-err = { workspace = true } -glibc_version = { workspace = true } goblin = { workspace = true } platform-info = { workspace = true } plist = { workspace = true } regex = { workspace = true } -serde = { workspace = true } +serde = { workspace = true, features = ["derive"] } target-lexicon = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } diff --git a/crates/platform-host/src/lib.rs b/crates/platform-host/src/lib.rs index 57bc080f4..ebbc61741 100644 --- a/crates/platform-host/src/lib.rs +++ b/crates/platform-host/src/lib.rs @@ -1,16 +1,15 @@ //! Abstractions for understanding the current platform (operating system and architecture). -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; use std::{fmt, io}; -use fs_err as fs; -use goblin::elf::Elf; use platform_info::{PlatformInfo, PlatformInfoAPI, UNameAPI}; -use regex::Regex; -use serde::Deserialize; use thiserror::Error; -use tracing::trace; + +use crate::linux::detect_linux_libc; +use crate::mac_os::get_mac_os_version; + +mod linux; +mod mac_os; #[derive(Error, Debug)] pub enum PlatformError { @@ -70,7 +69,7 @@ impl Os { let target_triple = target_lexicon::HOST; let os = match target_triple.operating_system { - target_lexicon::OperatingSystem::Linux => Self::detect_linux_libc()?, + target_lexicon::OperatingSystem::Linux => detect_linux_libc()?, target_lexicon::OperatingSystem::Windows => Os::Windows, target_lexicon::OperatingSystem::MacOSX { major, minor, .. } => { Os::Macos { major, minor } @@ -113,55 +112,6 @@ impl Os { fn platform_info() -> Result { PlatformInfo::new().map_err(|err| PlatformError::OsVersionDetectionError(err.to_string())) } - - fn detect_linux_libc() -> Result { - let libc = find_libc()?; - let linux = if let Ok(Some((major, minor))) = get_musl_version(&libc) { - Os::Musllinux { major, minor } - } else if let Ok(glibc_ld) = fs::read_link(&libc) { - // Try reading the link first as it's faster - let filename = glibc_ld - .file_name() - .ok_or_else(|| { - PlatformError::OsVersionDetectionError( - "Expected the glibc ld to be a file".to_string(), - ) - })? - .to_string_lossy(); - let expr = Regex::new(r"ld-(\d{1,3})\.(\d{1,3})\.so").unwrap(); - - if let Some(capture) = expr.captures(&filename) { - let major = capture.get(1).unwrap().as_str().parse::().unwrap(); - let minor = capture.get(2).unwrap().as_str().parse::().unwrap(); - Os::Manylinux { major, minor } - } else { - trace!("Couldn't use ld filename, using `ldd --version`"); - // runs `ldd --version` - let version = glibc_version::get_version().map_err(|err| { - PlatformError::OsVersionDetectionError(format!( - "Failed to determine glibc version with `ldd --version`: {err}" - )) - })?; - Os::Manylinux { - major: u16::try_from(version.major).map_err(|_| { - PlatformError::OsVersionDetectionError(format!( - "Invalid glibc major version {}", - version.major - )) - })?, - minor: u16::try_from(version.minor).map_err(|_| { - PlatformError::OsVersionDetectionError(format!( - "Invalid glibc minor version {}", - version.minor - )) - })?, - } - } - } else { - return Err(PlatformError::OsVersionDetectionError("Couldn't detect neither glibc version nor musl libc version, at least one of which is required".to_string())); - }; - Ok(linux) - } } impl fmt::Display for Os { @@ -237,76 +187,3 @@ impl Arch { } } } - -/// Get the macOS version from the SystemVersion.plist file. -fn get_mac_os_version() -> Result<(u16, u16), PlatformError> { - // This is actually what python does - // https://github.com/python/cpython/blob/cb2b3c8d3566ae46b3b8d0718019e1c98484589e/Lib/platform.py#L409-L428 - #[derive(Deserialize)] - #[serde(rename_all = "PascalCase")] - struct SystemVersion { - product_version: String, - } - let system_version: SystemVersion = - plist::from_file("/System/Library/CoreServices/SystemVersion.plist") - .map_err(|err| PlatformError::OsVersionDetectionError(err.to_string()))?; - - let invalid_mac_os_version = || { - PlatformError::OsVersionDetectionError(format!( - "Invalid macOS version {}", - system_version.product_version - )) - }; - match system_version - .product_version - .split('.') - .collect::>() - .as_slice() - { - [major, minor] | [major, minor, _] => { - let major = major.parse::().map_err(|_| invalid_mac_os_version())?; - let minor = minor.parse::().map_err(|_| invalid_mac_os_version())?; - Ok((major, minor)) - } - _ => Err(invalid_mac_os_version()), - } -} - -/// Find musl libc path from executable's ELF header. -fn find_libc() -> Result { - let buffer = fs::read("/bin/ls")?; - let error_str = "Couldn't parse /bin/ls for detecting the ld version"; - let elf = Elf::parse(&buffer) - .map_err(|err| PlatformError::OsVersionDetectionError(format!("{error_str}: {err}")))?; - if let Some(elf_interpreter) = elf.interpreter { - Ok(PathBuf::from(elf_interpreter)) - } else { - Err(PlatformError::OsVersionDetectionError( - error_str.to_string(), - )) - } -} - -/// Read the musl version from libc library's output. Taken from maturin. -/// -/// The libc library should output something like this to `stderr`: -/// -/// ```text -/// musl libc (`x86_64`) -/// Version 1.2.2 -/// Dynamic Program Loader -/// ``` -fn get_musl_version(ld_path: impl AsRef) -> std::io::Result> { - let output = Command::new(ld_path.as_ref()) - .stdout(Stdio::null()) - .stderr(Stdio::piped()) - .output()?; - let stderr = String::from_utf8_lossy(&output.stderr); - let expr = Regex::new(r"Version (\d{2,4})\.(\d{2,4})").unwrap(); - if let Some(capture) = expr.captures(&stderr) { - let major = capture.get(1).unwrap().as_str().parse::().unwrap(); - let minor = capture.get(2).unwrap().as_str().parse::().unwrap(); - return Ok(Some((major, minor))); - } - Ok(None) -} diff --git a/crates/platform-host/src/linux.rs b/crates/platform-host/src/linux.rs new file mode 100644 index 000000000..397d556d2 --- /dev/null +++ b/crates/platform-host/src/linux.rs @@ -0,0 +1,149 @@ +//! Taken from `glibc_version` (), +//! which used the Apache 2.0 license (but not the MIT license) + +use crate::{Os, PlatformError}; +use fs_err as fs; +use goblin::elf::Elf; +use regex::Regex; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use tracing::trace; + +// glibc version is taken from std/sys/unix/os.rs +fn get_version() -> Result { + let output = Command::new("ldd") + .args(["--version"]) + .output() + .expect("failed to execute ldd"); + let output_str = std::str::from_utf8(&output.stdout).unwrap(); + let version_str = ldd_output_to_version_str(output_str)?; + + parse_glibc_version(version_str).ok_or_else(|| { + PlatformError::OsVersionDetectionError(format!( + "Invalid version string from ldd output: {version_str}" + )) + }) +} + +fn ldd_output_to_version_str(output_str: &str) -> Result<&str, PlatformError> { + let version_reg = Regex::new(r"ldd \(.+\) ([0-9]+\.[0-9]+)").unwrap(); + if let Some(captures) = version_reg.captures(output_str) { + Ok(captures.get(1).unwrap().as_str()) + } else { + Err(PlatformError::OsVersionDetectionError(format!( + "ERROR: failed to detect glibc version. ldd output: {output_str}", + ))) + } +} + +// Returns Some((major, minor)) if the string is a valid "x.y" version, +// ignoring any extra dot-separated parts. Otherwise return None. +fn parse_glibc_version(version: &str) -> Option { + let mut parsed_ints = version.split('.').map(str::parse).fuse(); + match (parsed_ints.next(), parsed_ints.next()) { + (Some(Ok(major)), Some(Ok(minor))) => Some(Os::Manylinux { major, minor }), + _ => None, + } +} + +pub(crate) fn detect_linux_libc() -> Result { + let libc = find_libc()?; + let linux = if let Ok(Some((major, minor))) = get_musl_version(&libc) { + Os::Musllinux { major, minor } + } else if let Ok(glibc_ld) = fs::read_link(&libc) { + // Try reading the link first as it's faster + let filename = glibc_ld + .file_name() + .ok_or_else(|| { + PlatformError::OsVersionDetectionError( + "Expected the glibc ld to be a file".to_string(), + ) + })? + .to_string_lossy(); + let expr = Regex::new(r"ld-(\d{1,3})\.(\d{1,3})\.so").unwrap(); + + if let Some(capture) = expr.captures(&filename) { + let major = capture.get(1).unwrap().as_str().parse::().unwrap(); + let minor = capture.get(2).unwrap().as_str().parse::().unwrap(); + Os::Manylinux { major, minor } + } else { + trace!("Couldn't use ld filename, using `ldd --version`"); + // runs `ldd --version` + get_version().map_err(|err| { + PlatformError::OsVersionDetectionError(format!( + "Failed to determine glibc version with `ldd --version`: {err}" + )) + })? + } + } else { + return Err(PlatformError::OsVersionDetectionError("Couldn't detect neither glibc version nor musl libc version, at least one of which is required".to_string())); + }; + Ok(linux) +} + +/// Read the musl version from libc library's output. Taken from maturin. +/// +/// The libc library should output something like this to `stderr`: +/// +/// ```text +/// musl libc (`x86_64`) +/// Version 1.2.2 +/// Dynamic Program Loader +/// ``` +fn get_musl_version(ld_path: impl AsRef) -> std::io::Result> { + let output = Command::new(ld_path.as_ref()) + .stdout(Stdio::null()) + .stderr(Stdio::piped()) + .output()?; + let stderr = String::from_utf8_lossy(&output.stderr); + let expr = Regex::new(r"Version (\d{2,4})\.(\d{2,4})").unwrap(); + if let Some(capture) = expr.captures(&stderr) { + let major = capture.get(1).unwrap().as_str().parse::().unwrap(); + let minor = capture.get(2).unwrap().as_str().parse::().unwrap(); + return Ok(Some((major, minor))); + } + Ok(None) +} + +/// Find musl libc path from executable's ELF header. +fn find_libc() -> Result { + let buffer = fs::read("/bin/ls")?; + let error_str = "Couldn't parse /bin/ls for detecting the ld version"; + let elf = Elf::parse(&buffer) + .map_err(|err| PlatformError::OsVersionDetectionError(format!("{error_str}: {err}")))?; + if let Some(elf_interpreter) = elf.interpreter { + Ok(PathBuf::from(elf_interpreter)) + } else { + Err(PlatformError::OsVersionDetectionError( + error_str.to_string(), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_ldd_output() { + let ver_str = ldd_output_to_version_str( + r#"ldd (GNU libc) 2.12 +Copyright (C) 2010 Free Software Foundation, Inc. +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +Written by Roland McGrath and Ulrich Drepper."#, + ) + .unwrap(); + assert_eq!(ver_str, "2.12"); + + let ver_str = ldd_output_to_version_str( + r#"ldd (Ubuntu GLIBC 2.31-0ubuntu9.2) 2.31 + Copyright (C) 2020 Free Software Foundation, Inc. + This is free software; see the source for copying conditions. There is NO + warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + Written by Roland McGrath and Ulrich Drepper."#, + ) + .unwrap(); + assert_eq!(ver_str, "2.31"); + } +} diff --git a/crates/platform-host/src/mac_os.rs b/crates/platform-host/src/mac_os.rs new file mode 100644 index 000000000..a8f5a21ca --- /dev/null +++ b/crates/platform-host/src/mac_os.rs @@ -0,0 +1,36 @@ +use crate::PlatformError; +use serde::Deserialize; + +/// Get the macOS version from the SystemVersion.plist file. +pub(crate) fn get_mac_os_version() -> Result<(u16, u16), PlatformError> { + // This is actually what python does + // https://github.com/python/cpython/blob/cb2b3c8d3566ae46b3b8d0718019e1c98484589e/Lib/platform.py#L409-L428 + #[derive(Deserialize)] + #[serde(rename_all = "PascalCase")] + struct SystemVersion { + product_version: String, + } + let system_version: SystemVersion = + plist::from_file("/System/Library/CoreServices/SystemVersion.plist") + .map_err(|err| PlatformError::OsVersionDetectionError(err.to_string()))?; + + let invalid_mac_os_version = || { + PlatformError::OsVersionDetectionError(format!( + "Invalid macOS version {}", + system_version.product_version + )) + }; + match system_version + .product_version + .split('.') + .collect::>() + .as_slice() + { + [major, minor] | [major, minor, _] => { + let major = major.parse::().map_err(|_| invalid_mac_os_version())?; + let minor = minor.parse::().map_err(|_| invalid_mac_os_version())?; + Ok((major, minor)) + } + _ => Err(invalid_mac_os_version()), + } +}