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