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}