microsandbox_utils/lib.rs
1//! Shared constants and utilities for the microsandbox project.
2
3pub mod copy;
4pub mod format;
5pub mod log_text;
6pub mod process;
7pub mod size;
8pub mod ttl_reverse_index;
9pub mod wake_pipe;
10
11//--------------------------------------------------------------------------------------------------
12// Constants: Directory Layout
13//--------------------------------------------------------------------------------------------------
14
15/// Name of the microsandbox home directory (relative to user's home).
16pub const BASE_DIR_NAME: &str = ".microsandbox";
17
18/// Subdirectory for shared libraries (libkrunfw).
19pub const LIB_SUBDIR: &str = "lib";
20
21/// Subdirectory for helper binaries.
22pub const BIN_SUBDIR: &str = "bin";
23
24/// Subdirectory for the database.
25pub const DB_SUBDIR: &str = "db";
26
27/// Subdirectory for OCI layer cache.
28pub const CACHE_SUBDIR: &str = "cache";
29
30/// Subdirectory for per-sandbox state.
31pub const SANDBOXES_SUBDIR: &str = "sandboxes";
32
33/// Subdirectory for named volumes.
34pub const VOLUMES_SUBDIR: &str = "volumes";
35
36/// Subdirectory for snapshot artifacts.
37pub const SNAPSHOTS_SUBDIR: &str = "snapshots";
38
39/// Subdirectory for logs.
40pub const LOGS_SUBDIR: &str = "logs";
41
42/// Subdirectory for secrets.
43pub const SECRETS_SUBDIR: &str = "secrets";
44
45/// Subdirectory for TLS certificates.
46pub const TLS_SUBDIR: &str = "tls";
47
48/// Subdirectory for SSH keys.
49pub const SSH_SUBDIR: &str = "ssh";
50
51/// Subdirectory for ephemeral runtime artifacts that should not be backed up.
52pub const RUN_SUBDIR: &str = "run";
53
54/// Subdirectory under `run` for metrics-related diagnostic artifacts.
55pub const METRICS_RUN_SUBDIR: &str = "metrics";
56
57/// Prefix used when constructing the POSIX shared-memory object name for the
58/// live metrics registry. Combined with a stable hash of `GlobalConfig::home()`
59/// so concurrent `MSB_HOME`-isolated environments do not collide.
60///
61/// Kept short because macOS limits `shm_open` names to ~31 bytes including the
62/// leading slash; the final form is `<prefix>-<hex16>-vN` (28 bytes for
63/// single-digit ABI versions).
64pub const METRICS_SHM_PREFIX: &str = "/msb-met";
65
66//--------------------------------------------------------------------------------------------------
67// Constants: Binary Names
68//--------------------------------------------------------------------------------------------------
69
70/// Guest agent binary name.
71pub const AGENTD_BINARY: &str = "agentd";
72
73/// CLI binary name.
74pub const MSB_BINARY: &str = "msb";
75
76//--------------------------------------------------------------------------------------------------
77// Constants: Versions
78//--------------------------------------------------------------------------------------------------
79
80/// Version for downloading prebuilt release artifacts.
81///
82/// This tracks the published crate/package version so the SDK and the
83/// downloaded runtime bundle stay aligned.
84pub const PREBUILT_VERSION: &str = env!("CARGO_PKG_VERSION");
85
86/// libkrunfw release version. Keep in sync with justfile.
87pub const LIBKRUNFW_VERSION: &str = "5.2.1";
88
89/// libkrunfw ABI version (soname major). Keep in sync with justfile.
90pub const LIBKRUNFW_ABI: &str = "5";
91
92//--------------------------------------------------------------------------------------------------
93// Constants: Filenames
94//--------------------------------------------------------------------------------------------------
95
96/// Database filename.
97pub const DB_FILENAME: &str = "msb.db";
98
99/// Global configuration filename.
100pub const CONFIG_FILENAME: &str = "config.json";
101
102/// Project-local sandbox configuration filename.
103pub const SANDBOXFILE_NAME: &str = "Sandboxfile";
104
105//--------------------------------------------------------------------------------------------------
106// Constants: GitHub
107//--------------------------------------------------------------------------------------------------
108
109/// GitHub organization.
110pub const GITHUB_ORG: &str = "superradcompany";
111
112/// Main repository name.
113pub const MICROSANDBOX_REPO: &str = "microsandbox";
114
115//--------------------------------------------------------------------------------------------------
116// Functions
117//--------------------------------------------------------------------------------------------------
118
119/// Derive a short, stable identifier from a path.
120///
121/// Used to build a POSIX shared-memory object name that depends only on the
122/// resolved home directory, so two processes pointed at the same `MSB_HOME`
123/// agree on a single registry without leaking the absolute path through a
124/// public name.
125pub fn stable_hash_path(path: &std::path::Path) -> String {
126 // Avoid pulling sha2 into the utils crate for one filename; a stable
127 // 64-bit FNV-1a over the OS-bytes is plenty for collision-resistance at
128 // this scale (one entry per concurrent MSB_HOME on a host).
129 let bytes = path.as_os_str().as_encoded_bytes();
130 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
131 for byte in bytes {
132 hash ^= u64::from(*byte);
133 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
134 }
135 format!("{hash:016x}")
136}
137
138/// Filename of the optional registry-name diagnostic file under `run/metrics`.
139pub fn metrics_registry_name_filename(registry_abi_version: u32) -> String {
140 format!("registry-v{registry_abi_version}.name")
141}
142
143/// Derive the POSIX shared-memory object name for a metrics registry.
144pub fn metrics_registry_shm_name(home: &std::path::Path, registry_abi_version: u32) -> String {
145 format!(
146 "{}-{}-v{}",
147 METRICS_SHM_PREFIX,
148 stable_hash_path(home),
149 registry_abi_version
150 )
151}
152
153/// Resolve the microsandbox home directory.
154///
155/// Order of resolution:
156/// 1. `MSB_HOME` env var (used as-is, no `.microsandbox` suffix appended)
157/// 2. `~/.microsandbox/` (i.e. `dirs::home_dir().join(BASE_DIR_NAME)`)
158/// 3. `./.microsandbox/` if no home is available
159///
160/// `MSB_HOME` lets CI and integration tests isolate microsandbox state
161/// (db, sandboxes, cache, logs) per process without disturbing other
162/// `$HOME`-rooted tooling.
163pub fn resolve_home() -> std::path::PathBuf {
164 if let Some(path) = std::env::var_os("MSB_HOME") {
165 return std::path::PathBuf::from(path);
166 }
167 dirs::home_dir()
168 .unwrap_or_else(|| std::path::PathBuf::from("."))
169 .join(BASE_DIR_NAME)
170}
171
172/// Returns the platform-specific libkrunfw filename.
173pub fn libkrunfw_filename(os: &str) -> String {
174 if os == "macos" {
175 format!("libkrunfw.{LIBKRUNFW_ABI}.dylib")
176 } else if os == "windows" {
177 "libkrunfw.dll".to_string()
178 } else {
179 format!("libkrunfw.so.{LIBKRUNFW_VERSION}")
180 }
181}
182
183/// Returns the platform-specific msb executable filename.
184pub fn msb_binary_filename(os: &str) -> String {
185 if os == "windows" {
186 format!("{MSB_BINARY}.exe")
187 } else {
188 MSB_BINARY.to_string()
189 }
190}
191
192/// Returns the GitHub release download URL for libkrunfw.
193pub fn libkrunfw_download_url(version: &str, arch: &str, os: &str) -> String {
194 let (target_os, ext) = if os == "macos" {
195 ("darwin", "dylib")
196 } else if os == "windows" {
197 ("windows", "dll")
198 } else {
199 ("linux", "so")
200 };
201
202 format!(
203 "https://github.com/{GITHUB_ORG}/{MICROSANDBOX_REPO}/releases/download/v{version}/libkrunfw-{target_os}-{arch}.{ext}"
204 )
205}
206
207/// Returns the GitHub release download URL for the agentd binary.
208pub fn agentd_download_url(version: &str, arch: &str) -> String {
209 format!(
210 "https://github.com/{GITHUB_ORG}/{MICROSANDBOX_REPO}/releases/download/v{version}/{AGENTD_BINARY}-{arch}"
211 )
212}
213
214/// Returns the GitHub release download URL for the microsandbox bundle tarball.
215pub fn bundle_download_url(version: &str, arch: &str, os: &str) -> String {
216 let target_os = if os == "macos" {
217 "darwin"
218 } else if os == "windows" {
219 "windows"
220 } else {
221 "linux"
222 };
223 format!(
224 "https://github.com/{GITHUB_ORG}/{MICROSANDBOX_REPO}/releases/download/v{version}/{MICROSANDBOX_REPO}-{target_os}-{arch}.tar.gz"
225 )
226}
227
228/// Returns an HTTP client configured for release asset downloads.
229#[cfg(feature = "http-client")]
230pub fn http_client() -> ureq::Agent {
231 ureq::Agent::config_builder()
232 .tls_config(
233 ureq::tls::TlsConfig::builder()
234 .root_certs(ureq::tls::RootCerts::PlatformVerifier)
235 .build(),
236 )
237 .build()
238 .new_agent()
239}
240
241/// Returns true when a user-provided text value should be interpreted as a
242/// local filesystem path rather than a named resource or OCI reference.
243pub fn looks_like_local_path_text(s: &str) -> bool {
244 if s == "." || s == ".." || s.starts_with('/') || s.starts_with("./") || s.starts_with("../") {
245 return true;
246 }
247
248 #[cfg(windows)]
249 {
250 s.starts_with(".\\")
251 || s.starts_with("..\\")
252 || s.starts_with('\\')
253 || is_windows_drive_path_text(s)
254 }
255 #[cfg(not(windows))]
256 {
257 false
258 }
259}
260
261/// Returns true when `s` starts with a Windows drive-rooted path prefix.
262pub fn is_windows_drive_path_text(s: &str) -> bool {
263 let bytes = s.as_bytes();
264 bytes.len() >= 3
265 && bytes[0].is_ascii_alphabetic()
266 && bytes[1] == b':'
267 && matches!(bytes[2], b'\\' | b'/')
268}
269
270/// Returns true when the colon at `index` is the drive separator in a Windows path.
271pub fn is_windows_drive_separator_at(s: &str, index: usize) -> bool {
272 let bytes = s.as_bytes();
273 index == 1
274 && bytes.len() >= 3
275 && bytes[0].is_ascii_alphabetic()
276 && bytes[1] == b':'
277 && matches!(bytes[2], b'\\' | b'/')
278}
279
280//--------------------------------------------------------------------------------------------------
281// Tests
282//--------------------------------------------------------------------------------------------------
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 /// `MSB_HOME` is honoured verbatim (no `.microsandbox` suffix appended)
289 /// so callers can isolate state per process without disturbing tooling
290 /// that reads `$HOME` (npm cache, ssh keys, etc.).
291 ///
292 /// Uses a unique env var per test process to avoid clashing with other
293 /// parallel tests that read `MSB_HOME`.
294 #[test]
295 fn test_resolve_home_respects_env_override() {
296 // SAFETY: This test sets a process-global env var. Vitest-style
297 // single-test isolation isn't available; rely on the test being
298 // the sole reader of `MSB_HOME` in this binary.
299 let custom = std::path::PathBuf::from("/tmp/msb-home-resolve-test-12345");
300 unsafe { std::env::set_var("MSB_HOME", &custom) };
301 let resolved = resolve_home();
302 unsafe { std::env::remove_var("MSB_HOME") };
303 assert_eq!(resolved, custom);
304 }
305
306 #[test]
307 fn test_metrics_registry_names_include_abi_version() {
308 let home = std::path::Path::new("/tmp/msb-home");
309
310 assert_eq!(metrics_registry_name_filename(2), "registry-v2.name");
311 assert_eq!(
312 metrics_registry_shm_name(home, 2),
313 format!("{}-{}-v2", METRICS_SHM_PREFIX, stable_hash_path(home))
314 );
315 }
316
317 #[test]
318 #[cfg(windows)]
319 fn test_looks_like_local_path_text_accepts_windows_paths() {
320 assert!(looks_like_local_path_text(r"C:\Users\Stephen\file.txt"));
321 assert!(looks_like_local_path_text(r".\relative"));
322 assert!(looks_like_local_path_text(r"\\server\share\file.txt"));
323 assert!(!looks_like_local_path_text("alpine"));
324 }
325}