scirs2_core/platform_compat.rs
1//! Cross-platform compatibility utilities for consistent behavior across
2//! Windows, macOS, Linux, and WebAssembly targets.
3//!
4//! This module provides helper functions that abstract over platform differences
5//! so that the rest of the SciRS2 codebase can remain platform-agnostic.
6//!
7//! # Examples
8//!
9//! ```
10//! use scirs2_core::platform_compat;
11//!
12//! // Portable temporary directory
13//! let tmp = platform_compat::temp_dir();
14//! assert!(tmp.is_absolute());
15//!
16//! // Portable temporary file path
17//! let f = platform_compat::temp_file("my_data.bin");
18//! assert!(f.ends_with("my_data.bin"));
19//!
20//! // CPU count
21//! let n = platform_compat::num_cpus();
22//! assert!(n >= 1);
23//! ```
24
25use std::path::PathBuf;
26
27/// Return the platform's temporary directory.
28///
29/// On Unix this is typically `/tmp`; on Windows it is `%TEMP%` or similar.
30/// Always prefer this over hard-coding `/tmp/`.
31#[inline]
32pub fn temp_dir() -> PathBuf {
33 std::env::temp_dir()
34}
35
36/// Build a [`PathBuf`] pointing to `<temp_dir>/<name>`.
37///
38/// Useful for constructing throwaway file paths in tests and transient
39/// storage configurations.
40#[inline]
41pub fn temp_file(name: &str) -> PathBuf {
42 let mut p = std::env::temp_dir();
43 p.push(name);
44 p
45}
46
47/// Build a [`PathBuf`] pointing to `<temp_dir>/<subdir>/<name>`.
48///
49/// Creates a namespaced temporary path without actually creating the directory.
50#[inline]
51pub fn temp_path(subdir: &str, name: &str) -> PathBuf {
52 let mut p = std::env::temp_dir();
53 p.push(subdir);
54 p.push(name);
55 p
56}
57
58/// Return the number of logical CPUs available on the current machine.
59///
60/// Falls back to `1` if the value cannot be determined (e.g. under some
61/// sandboxed or embedded environments).
62#[inline]
63pub fn num_cpus() -> usize {
64 std::thread::available_parallelism()
65 .map(|n| n.get())
66 .unwrap_or(1)
67}
68
69/// The native path separator character for the current platform.
70///
71/// `'/'` on Unix, `'\\'` on Windows.
72#[inline]
73pub fn path_separator() -> char {
74 std::path::MAIN_SEPARATOR
75}
76
77/// `true` when compiled for a Windows target.
78#[inline]
79pub const fn is_windows() -> bool {
80 cfg!(target_os = "windows")
81}
82
83/// `true` when compiled for a macOS target.
84#[inline]
85pub const fn is_macos() -> bool {
86 cfg!(target_os = "macos")
87}
88
89/// `true` when compiled for a Linux target.
90#[inline]
91pub const fn is_linux() -> bool {
92 cfg!(target_os = "linux")
93}
94
95/// `true` when compiled for a WebAssembly target.
96#[inline]
97pub const fn is_wasm() -> bool {
98 cfg!(target_family = "wasm")
99}
100
101/// `true` when compiled for any Unix-family target (Linux, macOS, BSDs, etc.).
102#[inline]
103pub const fn is_unix() -> bool {
104 cfg!(target_family = "unix")
105}
106
107/// Return the default temporary directory as a [`String`].
108///
109/// Convenience wrapper around [`temp_dir`] for code that stores paths as
110/// `String` fields (e.g. configuration structs).
111pub fn temp_dir_string() -> String {
112 temp_dir()
113 .to_str()
114 .unwrap_or(if cfg!(target_os = "windows") {
115 "C:\\Temp"
116 } else {
117 "/tmp"
118 })
119 .to_string()
120}
121
122/// Join a subdirectory name onto the platform temporary directory and return
123/// the result as a [`String`].
124pub fn temp_subdir_string(subdir: &str) -> String {
125 let mut p = temp_dir();
126 p.push(subdir);
127 p.to_str()
128 .unwrap_or(if cfg!(target_os = "windows") {
129 "C:\\Temp"
130 } else {
131 "/tmp"
132 })
133 .to_string()
134}
135
136// ---------------------------------------------------------------------------
137// Tests
138// ---------------------------------------------------------------------------
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn temp_dir_is_absolute() {
145 assert!(temp_dir().is_absolute());
146 }
147
148 #[test]
149 fn temp_file_ends_with_name() {
150 let p = temp_file("hello.txt");
151 assert!(p.ends_with("hello.txt"));
152 }
153
154 #[test]
155 fn temp_path_contains_subdir() {
156 let p = temp_path("scirs2", "data.bin");
157 assert!(p.ends_with("data.bin"));
158 // The parent should contain "scirs2"
159 let parent = p.parent().expect("should have parent");
160 assert!(parent.ends_with("scirs2"));
161 }
162
163 #[test]
164 fn num_cpus_at_least_one() {
165 assert!(num_cpus() >= 1);
166 }
167
168 #[test]
169 fn path_separator_is_correct() {
170 let sep = path_separator();
171 if cfg!(target_os = "windows") {
172 assert_eq!(sep, '\\');
173 } else {
174 assert_eq!(sep, '/');
175 }
176 }
177
178 #[test]
179 fn platform_detection_consistent() {
180 // At least one platform family should be true
181 let any = is_windows() || is_unix() || is_wasm();
182 assert!(any, "should detect at least one platform family");
183 }
184
185 #[test]
186 fn temp_dir_string_is_nonempty() {
187 assert!(!temp_dir_string().is_empty());
188 }
189
190 #[test]
191 fn temp_subdir_string_contains_subdir() {
192 let s = temp_subdir_string("scirs2_test");
193 assert!(s.contains("scirs2_test"));
194 }
195}