Skip to main content

steamkit/util/
utils.rs

1//! Misc helpers: hex strings and OS detection.
2
3use std::sync::OnceLock;
4
5use crate::base::generated::steam_language::EOSType;
6use crate::error::SteamError;
7
8/// Misc helpers used throughout the library.
9#[derive(Debug)]
10pub(crate) struct Utils;
11
12impl Utils {
13    /// Encodes bytes as a lowercase hex string.
14    pub(crate) fn encode_hex_string(input: &[u8]) -> String {
15        hex::encode(input)
16    }
17
18    /// Decodes a hex string. `None` in, `None` out; invalid hex is an error.
19    pub(crate) fn decode_hex_string(hex: Option<&str>) -> Result<Option<Vec<u8>>, SteamError> {
20        let Some(hex) = hex else {
21            return Ok(None);
22        };
23
24        hex::decode(hex)
25            .map(Some)
26            .map_err(|error| SteamError::InvalidArgument(format!("Invalid hex string: {error}")))
27    }
28
29    /// The operating system we're running on. Cached, since it can't change.
30    pub(crate) fn get_os_type() -> EOSType {
31        static OS_TYPE: OnceLock<EOSType> = OnceLock::new();
32
33        *OS_TYPE.get_or_init(detect_os_type)
34    }
35}
36
37/// Encodes bytes as a lowercase hex string.
38pub(crate) fn encode_hex_string(input: &[u8]) -> String {
39    Utils::encode_hex_string(input)
40}
41
42/// Decodes a hex string. Same as [`Utils::decode_hex_string`].
43pub(crate) fn decode_hex_string(hex: Option<&str>) -> Result<Option<Vec<u8>>, SteamError> {
44    Utils::decode_hex_string(hex)
45}
46
47/// The operating system we're running on. Same as [`Utils::get_os_type`].
48pub(crate) fn get_os_type() -> EOSType {
49    Utils::get_os_type()
50}
51
52/// Detects the operating system and maps it to Valve's `EOSType`. The standard
53/// library doesn't expose OS or kernel versions, so each `detect_*` function
54/// asks the platform's own tools.
55fn detect_os_type() -> EOSType {
56    #[cfg(target_os = "windows")]
57    {
58        detect_windows_os_type()
59    }
60
61    #[cfg(target_os = "macos")]
62    {
63        detect_macos_os_type()
64    }
65
66    #[cfg(target_os = "linux")]
67    {
68        detect_linux_os_type()
69    }
70
71    #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
72    {
73        EOSType::Unknown
74    }
75}
76
77/// Maps a Windows version to Valve's `EOSType`: by major, then minor version,
78/// and for Windows 10+ by build number (which tells Windows 11 apart).
79#[cfg(target_os = "windows")]
80fn detect_windows_os_type() -> EOSType {
81    // `cmd /c ver` prints e.g. "Microsoft Windows [Version 10.0.19045.4046]".
82    // The bracketed part doesn't depend on the locale.
83    let version = std::process::Command::new("cmd")
84        .args(["/c", "ver"])
85        .output()
86        .ok()
87        .and_then(|output| String::from_utf8(output.stdout).ok())
88        .and_then(|text| {
89            let start = text.find('[')? + 1;
90            let end = text[start..].find(']')? + start;
91            Some(text[start..end].trim().to_string())
92        })
93        // The bracketed text starts with "Version ", e.g.
94        // "Version 10.0.26300.9711"; strip it before parsing.
95        .and_then(|text| parse_version(strip_version_prefix(&text)));
96
97    let Some((major, minor, build)) = version else {
98        // Couldn't get the version; better to report a modern Windows than an
99        // unknown platform.
100        return EOSType::Windows10;
101    };
102
103    match major {
104        4 => EOSType::WinNT,
105        5 => match minor {
106            0 => EOSType::Win2000,
107            1 => EOSType::WinXP,
108            // Assume nobody runs Windows XP Professional x64 Edition;
109            // it is an edition of Windows Server 2003 anyway.
110            2 => EOSType::Win2003,
111            _ => EOSType::WinUnknown,
112        },
113        6 => match minor {
114            0 => EOSType::WinVista, // Also Server 2008
115            1 => EOSType::Windows7, // Also Server 2008 R2
116            2 => EOSType::Windows8, // Also Server 2012
117            3 => EOSType::Windows81, // Also Server 2012 R2
118            _ => EOSType::WinUnknown,
119        },
120        10 if build >= 22000 => EOSType::Win11,
121        10 => EOSType::Windows10, // Also Server 2016, Server 2019, Server 2022
122        _ => EOSType::WinUnknown,
123    }
124}
125
126/// Maps a Linux kernel version (from `/proc/sys/kernel/osrelease`) to Valve's
127/// `EOSType`.
128#[cfg(target_os = "linux")]
129fn detect_linux_os_type() -> EOSType {
130    let Some((major, minor, _)) = std::fs::read_to_string("/proc/sys/kernel/osrelease")
131        .ok()
132        .and_then(|text| parse_version(text.trim()))
133    else {
134        return EOSType::LinuxUnknown;
135    };
136
137    // The specific minor versions only exist in Valve's enum for LTS versions.
138    match major {
139        2 => match minor {
140            2 => EOSType::Linux22,
141            4 => EOSType::Linux24,
142            6 => EOSType::Linux26,
143            _ => EOSType::LinuxUnknown,
144        },
145        3 => match minor {
146            2 => EOSType::Linux32,
147            5 => EOSType::Linux35,
148            6 => EOSType::Linux36,
149            10 => EOSType::Linux310,
150            16 => EOSType::Linux316,
151            18 => EOSType::Linux318,
152            _ => EOSType::Linux3x,
153        },
154        4 => match minor {
155            1 => EOSType::Linux41,
156            4 => EOSType::Linux44,
157            9 => EOSType::Linux49,
158            14 => EOSType::Linux414,
159            19 => EOSType::Linux419,
160            _ => EOSType::Linux4x,
161        },
162        5 => match minor {
163            4 => EOSType::Linux54,
164            10 => EOSType::Linux510,
165            _ => EOSType::Linux5x,
166        },
167        6 => EOSType::Linux6x,
168        7 => EOSType::Linux7x,
169        _ => EOSType::LinuxUnknown,
170    }
171}
172
173/// Maps a macOS version (from `sw_vers`) to Valve's `EOSType`, by way of the
174/// matching Darwin kernel version.
175#[cfg(target_os = "macos")]
176fn detect_macos_os_type() -> EOSType {
177    let Some((product_major, product_minor, _)) = std::process::Command::new("sw_vers")
178        .arg("-productVersion")
179        .output()
180        .ok()
181        .and_then(|output| String::from_utf8(output.stdout).ok())
182        .and_then(|text| parse_version(text.trim()))
183    else {
184        return EOSType::MacOSUnknown;
185    };
186
187    // macOS 10.x ships with the Darwin kernel version `minor + 4`
188    // (10.7 -> 11, 10.15 -> 19); macOS 11 and later use `major + 9`
189    // (11 -> 20, 15 -> 24).
190    let darwin_major = if product_major == 10 {
191        product_minor + 4
192    } else {
193        product_major + 9
194    };
195
196    match darwin_major {
197        11 => EOSType::MacOS107, // "Lion"
198        12 => EOSType::MacOS108, // "Mountain Lion"
199        13 => EOSType::MacOS109, // "Mavericks"
200        14 => EOSType::MacOS1010, // "Yosemite"
201        15 => EOSType::MacOS1011, // El Capitan
202        16 => EOSType::MacOS1012, // Sierra
203        17 => EOSType::Macos1013, // High Sierra
204        18 => EOSType::Macos1014, // Mojave
205        19 => EOSType::Macos1015, // Catalina
206        20 => EOSType::MacOS11, // Big Sur
207        21 => EOSType::MacOS12, // Monterey
208        22 => EOSType::MacOS13, // Ventura
209        23 => EOSType::MacOS14, // Sonoma
210        24 => EOSType::MacOS15, // Sequoia
211        _ => EOSType::MacOSUnknown,
212    }
213}
214
215/// Skips any text before the first digit, e.g. the "Version " in the output of
216/// `cmd /c ver`.
217fn strip_version_prefix(text: &str) -> &str {
218    match text.find(|character: char| character.is_ascii_digit()) {
219        Some(start) => &text[start..],
220        None => text,
221    }
222}
223
224/// Parses a dotted version into major, minor and build. Non-numeric trailing
225/// parts (like a Linux kernel's `-generic`) are ignored.
226fn parse_version(text: &str) -> Option<(u64, u64, u64)> {
227    let mut parts = text.split('.');
228
229    let major = parts.next()?.trim().parse::<u64>().ok()?;
230    let minor = parts.next().unwrap_or("0").trim().parse::<u64>().ok()?;
231    let build = parts.next().unwrap_or("0").trim().parse::<u64>().ok()?;
232
233    Some((major, minor, build))
234}