use std::{
path::{Path, PathBuf},
sync::LazyLock,
};
use typed_path::{Utf8UnixComponent, Utf8UnixPathBuf};
use crate::{MicrosandboxUtilsError, MicrosandboxUtilsResult};
pub const MICROSANDBOX_ENV_DIR: &str = ".menv";
pub const MICROSANDBOX_HOME_DIR: &str = ".microsandbox";
pub const RW_SUBDIR: &str = "rw";
pub const PATCH_SUBDIR: &str = "patch";
pub const LOG_SUBDIR: &str = "log";
pub const LAYERS_SUBDIR: &str = "layers";
pub const INSTALLS_SUBDIR: &str = "installs";
pub const SANDBOX_DB_FILENAME: &str = "sandbox.db";
pub const OCI_DB_FILENAME: &str = "oci.db";
pub const SANDBOX_DIR: &str = ".sandbox";
pub const SCRIPTS_DIR: &str = "scripts";
pub const EXTRACTED_LAYER_SUFFIX: &str = "extracted";
pub const MICROSANDBOX_CONFIG_FILENAME: &str = "Sandboxfile";
pub const SHELL_SCRIPT_NAME: &str = "shell";
pub const NAMESPACES_SUBDIR: &str = "namespaces";
pub const SERVER_PID_FILE: &str = "server.pid";
pub const SERVER_KEY_FILE: &str = "server.key";
pub const PORTAL_PORTS_FILE: &str = "portal.ports";
pub static XDG_HOME_DIR: LazyLock<PathBuf> =
LazyLock::new(|| dirs::home_dir().unwrap().join(".local"));
pub const XDG_BIN_DIR: &str = "bin";
pub const XDG_LIB_DIR: &str = "lib";
pub const LOG_SUFFIX: &str = "log";
pub const SUPERVISOR_LOG_FILENAME: &str = "supervisor.log";
pub enum SupportedPathType {
Any,
Absolute,
Relative,
}
pub fn normalize_path(path: &str, path_type: SupportedPathType) -> MicrosandboxUtilsResult<String> {
if path.is_empty() {
return Err(MicrosandboxUtilsError::PathValidation(
"Path cannot be empty".to_string(),
));
}
let path = Utf8UnixPathBuf::from(path);
let mut normalized = Vec::new();
let mut is_absolute = false;
let mut depth = 0;
for component in path.components() {
match component {
Utf8UnixComponent::RootDir => {
if normalized.is_empty() {
is_absolute = true;
normalized.push("/".to_string());
} else {
return Err(MicrosandboxUtilsError::PathValidation(
"Invalid path: root component '/' found in middle of path".to_string(),
));
}
}
Utf8UnixComponent::ParentDir => {
if depth > 0 {
normalized.pop();
depth -= 1;
} else {
return Err(MicrosandboxUtilsError::PathValidation(
"Invalid path: cannot traverse above root directory".to_string(),
));
}
}
Utf8UnixComponent::CurDir => continue,
Utf8UnixComponent::Normal(c) => {
if !c.is_empty() {
normalized.push(c.to_string());
depth += 1;
}
}
}
}
match path_type {
SupportedPathType::Absolute if !is_absolute => {
return Err(MicrosandboxUtilsError::PathValidation(
"Path must be absolute (start with '/')".to_string(),
));
}
SupportedPathType::Relative if is_absolute => {
return Err(MicrosandboxUtilsError::PathValidation(
"Path must be relative (must not start with '/')".to_string(),
));
}
_ => {}
}
if is_absolute {
if normalized.len() == 1 {
Ok("/".to_string())
} else {
Ok(format!("/{}", normalized[1..].join("/")))
}
} else {
Ok(normalized.join("/"))
}
}
pub fn resolve_env_path(
env_var: &str,
default_path: impl AsRef<Path>,
) -> MicrosandboxUtilsResult<PathBuf> {
let (path, source) = std::env::var(env_var)
.map(|p| (PathBuf::from(p), "environment variable"))
.unwrap_or_else(|_| (default_path.as_ref().to_path_buf(), "default path"));
if !path.exists() {
return Err(MicrosandboxUtilsError::FileNotFound(
path.to_string_lossy().to_string(),
source.to_string(),
));
}
Ok(path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_path() {
assert_eq!(
normalize_path("/data/app/", SupportedPathType::Absolute).unwrap(),
"/data/app"
);
assert_eq!(
normalize_path("/data//app", SupportedPathType::Absolute).unwrap(),
"/data/app"
);
assert_eq!(
normalize_path("/data/./app", SupportedPathType::Absolute).unwrap(),
"/data/app"
);
assert_eq!(
normalize_path("data/app/", SupportedPathType::Relative).unwrap(),
"data/app"
);
assert_eq!(
normalize_path("./data/app", SupportedPathType::Relative).unwrap(),
"data/app"
);
assert_eq!(
normalize_path("data//app", SupportedPathType::Relative).unwrap(),
"data/app"
);
assert_eq!(
normalize_path("/data/app", SupportedPathType::Any).unwrap(),
"/data/app"
);
assert_eq!(
normalize_path("data/app", SupportedPathType::Any).unwrap(),
"data/app"
);
assert_eq!(
normalize_path("/data/temp/../app", SupportedPathType::Absolute).unwrap(),
"/data/app"
);
assert_eq!(
normalize_path("data/temp/../app", SupportedPathType::Relative).unwrap(),
"data/app"
);
assert!(matches!(
normalize_path("data/app", SupportedPathType::Absolute),
Err(MicrosandboxUtilsError::PathValidation(e)) if e.contains("must be absolute")
));
assert!(matches!(
normalize_path("/data/app", SupportedPathType::Relative),
Err(MicrosandboxUtilsError::PathValidation(e)) if e.contains("must be relative")
));
assert!(matches!(
normalize_path("/data/../..", SupportedPathType::Any),
Err(MicrosandboxUtilsError::PathValidation(e)) if e.contains("cannot traverse above root")
));
}
#[test]
fn test_normalize_path_complex() {
assert_eq!(
normalize_path(
"/data/./temp/../logs/app/./config/../",
SupportedPathType::Absolute
)
.unwrap(),
"/data/logs/app"
);
assert_eq!(
normalize_path(
"/data///temp/././../app//./test/..",
SupportedPathType::Absolute
)
.unwrap(),
"/data/app"
);
assert_eq!(
normalize_path("/data/./././.", SupportedPathType::Absolute).unwrap(),
"/data"
);
assert_eq!(
normalize_path("/data/test/../../data/app", SupportedPathType::Absolute).unwrap(),
"/data/app"
);
assert!(matches!(
normalize_path("/data/test/../../../root", SupportedPathType::Any),
Err(MicrosandboxUtilsError::PathValidation(e)) if e.contains("cannot traverse above root")
));
assert!(matches!(
normalize_path("/./data/../..", SupportedPathType::Any),
Err(MicrosandboxUtilsError::PathValidation(e)) if e.contains("cannot traverse above root")
));
}
}