microsandbox_utils/
path.rs

1//! `microsandbox_utils::path` is a module containing path utilities for the microsandbox project.
2
3use std::{
4    path::{Path, PathBuf},
5    sync::LazyLock,
6};
7
8use typed_path::{Utf8UnixComponent, Utf8UnixPathBuf};
9
10use crate::{MicrosandboxUtilsError, MicrosandboxUtilsResult};
11
12//--------------------------------------------------------------------------------------------------
13// Constants
14//--------------------------------------------------------------------------------------------------
15
16/// The directory name for microsandbox's project-specific data
17pub const MICROSANDBOX_ENV_DIR: &str = ".menv";
18
19/// The directory name for microsandbox's global data
20pub const MICROSANDBOX_HOME_DIR: &str = ".microsandbox";
21
22/// The directory where project read-write layers are stored
23///
24/// Example: <PROJECT_ROOT>/<MICROSANDBOX_ENV_DIR>/<RW_SUBDIR>
25pub const RW_SUBDIR: &str = "rw";
26
27/// The directory where project patch layers are stored
28///
29/// Example: <PROJECT_ROOT>/<MICROSANDBOX_ENV_DIR>/<PATCH_SUBDIR>
30pub const PATCH_SUBDIR: &str = "patch";
31
32/// The directory where project logs are stored
33///
34/// Example: <PROJECT_ROOT>/<MICROSANDBOX_ENV_DIR>/<LOG_SUBDIR>
35pub const LOG_SUBDIR: &str = "log";
36
37/// The directory where global image layers are stored
38///
39/// Example: <MICROSANDBOX_HOME_DIR>/<LAYERS_SUBDIR>
40pub const LAYERS_SUBDIR: &str = "layers";
41
42/// The directory where installed sandboxes are stored
43///
44/// Example: <MICROSANDBOX_HOME_DIR>/<INSTALLS_SUBDIR>
45pub const INSTALLS_SUBDIR: &str = "installs";
46
47/// The filename for the project active sandbox database
48///
49/// Example: <PROJECT_ROOT>/<MICROSANDBOX_ENV_DIR>/<SANDBOX_DB_FILENAME>
50pub const SANDBOX_DB_FILENAME: &str = "sandbox.db";
51
52/// The filename for the global OCI database
53///
54/// Example: <MICROSANDBOX_HOME_DIR>/<OCI_DB_FILENAME>
55pub const OCI_DB_FILENAME: &str = "oci.db";
56
57/// The directory on the microvm where sandbox scripts are stored
58pub const SANDBOX_DIR: &str = ".sandbox";
59
60/// The directory on the microvm where sandbox scripts are stored
61///
62/// Example: <SANDBOX_DIR>/<SCRIPTS_DIR>
63pub const SCRIPTS_DIR: &str = "scripts";
64
65/// The suffix added to extracted layer directories
66///
67/// Example: <MICROSANDBOX_HOME_DIR>/<LAYERS_SUBDIR>/<LAYER_ID>.<EXTRACTED_LAYER_SUFFIX>
68pub const EXTRACTED_LAYER_SUFFIX: &str = "extracted";
69
70/// The microsandbox config file name.
71///
72/// Example: <PROJECT_ROOT>/<MICROSANDBOX_ENV_DIR>/<SANDBOX_DB_FILENAME>
73pub const MICROSANDBOX_CONFIG_FILENAME: &str = "Sandboxfile";
74
75/// The shell script name.
76///
77/// Example: <PROJECT_ROOT>/<MICROSANDBOX_ENV_DIR>/<PATCH_SUBDIR>/<CONFIG_NAME>/<SHELL_SCRIPT_NAME>
78pub const SHELL_SCRIPT_NAME: &str = "shell";
79
80/// The directory where namespaces are stored
81///
82/// Example: <MICROSANDBOX_HOME_DIR>/<NAMESPACES_SUBDIR>
83pub const NAMESPACES_SUBDIR: &str = "namespaces";
84
85/// The PID file for the server
86///
87/// Example: <MICROSANDBOX_HOME_DIR>/<SERVER_PID_FILE>
88pub const SERVER_PID_FILE: &str = "server.pid";
89
90/// The server secret key file
91///
92/// Example: <MICROSANDBOX_HOME_DIR>/<SERVER_KEY_FILE>
93pub const SERVER_KEY_FILE: &str = "server.key";
94
95/// The file where sandbox portal ports are stored
96///
97/// Example: <MICROSANDBOX_HOME_DIR>/<NAMESPACE_SUBDIR>/<PORTAL_PORTS_FILE>
98pub const PORTAL_PORTS_FILE: &str = "portal.ports";
99
100/// The XDG home directory
101///
102/// Example: <HOME>/.local
103pub static XDG_HOME_DIR: LazyLock<PathBuf> =
104    LazyLock::new(|| dirs::home_dir().unwrap().join(".local"));
105
106/// The bin subdirectory for microsandbox
107///
108/// Example: <XDG_HOME_DIR>/bin
109pub const XDG_BIN_DIR: &str = "bin";
110
111/// The lib subdirectory for microsandbox
112///
113/// Example: <XDG_HOME_DIR>/lib
114pub const XDG_LIB_DIR: &str = "lib";
115
116/// The suffix for log files
117pub const LOG_SUFFIX: &str = "log";
118
119/// The filename for the supervisor's log file
120pub const SUPERVISOR_LOG_FILENAME: &str = "supervisor.log";
121
122//--------------------------------------------------------------------------------------------------
123// Types
124//--------------------------------------------------------------------------------------------------
125
126/// The type of a supported path.
127pub enum SupportedPathType {
128    /// Any path type.
129    Any,
130
131    /// An absolute path.
132    Absolute,
133
134    /// A relative path.
135    Relative,
136}
137
138//--------------------------------------------------------------------------------------------------
139// Functions
140//--------------------------------------------------------------------------------------------------
141
142/// Normalizes a path string for volume mount comparison.
143///
144/// Rules:
145/// - Resolves . and .. components where possible
146/// - Prevents path traversal that would escape the root
147/// - Removes redundant separators and trailing slashes
148/// - Case-sensitive comparison (Unix standard)
149/// - Can enforce path type requirements (absolute, relative, or any)
150///
151/// # Arguments
152/// * `path` - The path to normalize
153/// * `path_type` - The required path type (absolute, relative, or any)
154///
155/// # Returns
156/// An error if the path is invalid, would escape root, or doesn't meet path type requirement
157pub fn normalize_path(path: &str, path_type: SupportedPathType) -> MicrosandboxUtilsResult<String> {
158    if path.is_empty() {
159        return Err(MicrosandboxUtilsError::PathValidation(
160            "Path cannot be empty".to_string(),
161        ));
162    }
163
164    let path = Utf8UnixPathBuf::from(path);
165    let mut normalized = Vec::new();
166    let mut is_absolute = false;
167    let mut depth = 0;
168
169    for component in path.components() {
170        match component {
171            // Root component must come first if present
172            Utf8UnixComponent::RootDir => {
173                if normalized.is_empty() {
174                    is_absolute = true;
175                    normalized.push("/".to_string());
176                } else {
177                    return Err(MicrosandboxUtilsError::PathValidation(
178                        "Invalid path: root component '/' found in middle of path".to_string(),
179                    ));
180                }
181            }
182            // Handle parent directory references
183            Utf8UnixComponent::ParentDir => {
184                if depth > 0 {
185                    // Can go up if we have depth
186                    normalized.pop();
187                    depth -= 1;
188                } else {
189                    // Trying to go above root
190                    return Err(MicrosandboxUtilsError::PathValidation(
191                        "Invalid path: cannot traverse above root directory".to_string(),
192                    ));
193                }
194            }
195            // Skip current dir components
196            Utf8UnixComponent::CurDir => continue,
197            // Normal components are fine
198            Utf8UnixComponent::Normal(c) => {
199                if !c.is_empty() {
200                    normalized.push(c.to_string());
201                    depth += 1;
202                }
203            }
204        }
205    }
206
207    // Check path type requirements
208    match path_type {
209        SupportedPathType::Absolute if !is_absolute => {
210            return Err(MicrosandboxUtilsError::PathValidation(
211                "Path must be absolute (start with '/')".to_string(),
212            ));
213        }
214        SupportedPathType::Relative if is_absolute => {
215            return Err(MicrosandboxUtilsError::PathValidation(
216                "Path must be relative (must not start with '/')".to_string(),
217            ));
218        }
219        _ => {}
220    }
221
222    if is_absolute {
223        if normalized.len() == 1 {
224            // Just root
225            Ok("/".to_string())
226        } else {
227            // Join all components with "/" and add root at start
228            Ok(format!("/{}", normalized[1..].join("/")))
229        }
230    } else {
231        // For relative paths, just join all components
232        Ok(normalized.join("/"))
233    }
234}
235
236/// Resolves the path to a file, checking both environment variable and default locations.
237///
238/// First checks the environment variable specified by `env_var`.
239/// If that's not set, falls back to `default_path`.
240/// Returns an error if the file is not found at the resolved location.
241pub fn resolve_env_path(
242    env_var: &str,
243    default_path: impl AsRef<Path>,
244) -> MicrosandboxUtilsResult<PathBuf> {
245    let (path, source) = std::env::var(env_var)
246        .map(|p| (PathBuf::from(p), "environment variable"))
247        .unwrap_or_else(|_| (default_path.as_ref().to_path_buf(), "default path"));
248
249    if !path.exists() {
250        return Err(MicrosandboxUtilsError::FileNotFound(
251            path.to_string_lossy().to_string(),
252            source.to_string(),
253        ));
254    }
255
256    Ok(path)
257}
258
259//--------------------------------------------------------------------------------------------------
260// Tests
261//--------------------------------------------------------------------------------------------------
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_normalize_path() {
269        // Test with SupportedPathType::Absolute
270        assert_eq!(
271            normalize_path("/data/app/", SupportedPathType::Absolute).unwrap(),
272            "/data/app"
273        );
274        assert_eq!(
275            normalize_path("/data//app", SupportedPathType::Absolute).unwrap(),
276            "/data/app"
277        );
278        assert_eq!(
279            normalize_path("/data/./app", SupportedPathType::Absolute).unwrap(),
280            "/data/app"
281        );
282
283        // Test with SupportedPathType::Relative
284        assert_eq!(
285            normalize_path("data/app/", SupportedPathType::Relative).unwrap(),
286            "data/app"
287        );
288        assert_eq!(
289            normalize_path("./data/app", SupportedPathType::Relative).unwrap(),
290            "data/app"
291        );
292        assert_eq!(
293            normalize_path("data//app", SupportedPathType::Relative).unwrap(),
294            "data/app"
295        );
296
297        // Test with SupportedPathType::Any
298        assert_eq!(
299            normalize_path("/data/app", SupportedPathType::Any).unwrap(),
300            "/data/app"
301        );
302        assert_eq!(
303            normalize_path("data/app", SupportedPathType::Any).unwrap(),
304            "data/app"
305        );
306
307        // Path traversal within bounds
308        assert_eq!(
309            normalize_path("/data/temp/../app", SupportedPathType::Absolute).unwrap(),
310            "/data/app"
311        );
312        assert_eq!(
313            normalize_path("data/temp/../app", SupportedPathType::Relative).unwrap(),
314            "data/app"
315        );
316
317        // Invalid paths
318        assert!(matches!(
319            normalize_path("data/app", SupportedPathType::Absolute),
320            Err(MicrosandboxUtilsError::PathValidation(e)) if e.contains("must be absolute")
321        ));
322        assert!(matches!(
323            normalize_path("/data/app", SupportedPathType::Relative),
324            Err(MicrosandboxUtilsError::PathValidation(e)) if e.contains("must be relative")
325        ));
326        assert!(matches!(
327            normalize_path("/data/../..", SupportedPathType::Any),
328            Err(MicrosandboxUtilsError::PathValidation(e)) if e.contains("cannot traverse above root")
329        ));
330    }
331
332    #[test]
333    fn test_normalize_path_complex() {
334        // Complex but valid paths
335        assert_eq!(
336            normalize_path(
337                "/data/./temp/../logs/app/./config/../",
338                SupportedPathType::Absolute
339            )
340            .unwrap(),
341            "/data/logs/app"
342        );
343        assert_eq!(
344            normalize_path(
345                "/data///temp/././../app//./test/..",
346                SupportedPathType::Absolute
347            )
348            .unwrap(),
349            "/data/app"
350        );
351
352        // Edge cases
353        assert_eq!(
354            normalize_path("/data/./././.", SupportedPathType::Absolute).unwrap(),
355            "/data"
356        );
357        assert_eq!(
358            normalize_path("/data/test/../../data/app", SupportedPathType::Absolute).unwrap(),
359            "/data/app"
360        );
361
362        // Invalid complex paths
363        assert!(matches!(
364            normalize_path("/data/test/../../../root", SupportedPathType::Any),
365            Err(MicrosandboxUtilsError::PathValidation(e)) if e.contains("cannot traverse above root")
366        ));
367        assert!(matches!(
368            normalize_path("/./data/../..", SupportedPathType::Any),
369            Err(MicrosandboxUtilsError::PathValidation(e)) if e.contains("cannot traverse above root")
370        ));
371    }
372}