Skip to main content

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