use crate::container::environment::{HostOs, PathFormat};
use std::path::{Path, PathBuf};
use tracing::debug;
#[derive(Debug, thiserror::Error)]
pub enum PathUtilsError {
#[error("Path processing error: {0}")]
InvalidPath(String),
}
#[derive(Debug, Clone)]
pub struct PathProcessor {
pub host_os: HostOs,
pub path_format: PathFormat,
}
impl PathProcessor {
pub fn new(host_os: HostOs, path_format: PathFormat) -> Self {
Self {
host_os,
path_format,
}
}
pub fn normalize_path(&self, input_path: &str) -> Result<String, PathUtilsError> {
let path = input_path.trim();
if path.is_empty() {
return Err(PathUtilsError::InvalidPath("Path cannot be empty".to_string()));
}
debug!(
"🔍 Normalizing path: '{}' (current environment: {:?})",
path, self.path_format
);
let cleaned_path = self.clean_path(path);
let formatted_path = match self.path_format {
PathFormat::Wsl2 => self.to_wsl2_format(&cleaned_path),
PathFormat::Windows => self.to_windows_format(&cleaned_path),
PathFormat::Posix => self.to_posix_format(&cleaned_path),
};
debug!("✅ Path normalization complete: '{}'", formatted_path);
Ok(formatted_path)
}
pub fn is_bind_mount_path(&self, path: &str) -> bool {
let path = path.trim();
if path.is_empty() {
return false;
}
if path.starts_with('/') && !path.starts_with("//") {
return true;
}
if path.len() >= 3
&& path.chars().nth(1).unwrap_or_default() == ':'
&& (path.chars().nth(2).unwrap_or_default() == '\\'
|| path.chars().nth(2).unwrap_or_default() == '/')
{
return true;
}
if path.starts_with("/mnt/") || path.starts_with("/c/") || path.starts_with("/d/") {
return true;
}
if path.contains('/') || path.contains('\\') {
return !Path::new(path).file_name().map_or(false, |f| {
!Path::new(f).extension().map_or(false, |ext| {
ext.len() > 0
})
});
}
false
}
pub fn to_absolute_path(&self, path: &str, work_dir: &Path) -> Result<PathBuf, PathUtilsError> {
let normalized_path = self.normalize_path(path)?;
let path_buf = PathBuf::from(&normalized_path);
if path_buf.is_absolute() {
Ok(path_buf)
} else {
let absolute = work_dir.join(path_buf);
Ok(absolute)
}
}
fn clean_path(&self, path: &str) -> String {
let mut components = Vec::new();
for component in Path::new(path).components() {
match component {
std::path::Component::CurDir => {
continue;
}
std::path::Component::ParentDir => {
if let Some(last) = components.last() {
if last != &std::path::Component::RootDir {
components.pop();
}
}
}
_ => {
components.push(component);
}
}
}
let cleaned = components
.iter()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join(std::path::MAIN_SEPARATOR_STR);
if cleaned.is_empty() {
".".to_string()
} else {
cleaned
}
}
fn to_wsl2_format(&self, path: &str) -> String {
let path = path.replace('\\', "/");
if path.len() >= 3
&& path.chars().nth(1).unwrap_or_default() == ':'
&& (path.chars().nth(2).unwrap_or_default() == '/'
|| path.chars().nth(2).unwrap_or_default() == '\\')
{
let drive_letter = path
.chars()
.nth(0)
.unwrap_or_default()
.to_lowercase()
.next()
.unwrap_or_default();
let rest = &path[3..];
return format!("/mnt/{}{}", drive_letter, rest);
}
if path.starts_with("/mnt/") || path.starts_with("/c/") || path.starts_with("/d/") {
return path;
}
path
}
fn to_windows_format(&self, path: &str) -> String {
let path = path.replace('/', "\\");
if path.starts_with("/mnt/") {
let rest = &path[5..]; if !rest.is_empty() {
let drive_letter = rest
.chars()
.next()
.unwrap_or_default()
.to_uppercase()
.next()
.unwrap_or_default();
let rest = &rest[1..];
return format!("{}:\\{}", drive_letter, rest);
}
}
if path.starts_with("/c/") {
return format!("C:\\{}", &path[3..]);
}
if path.starts_with("/d/") {
return format!("D:\\{}", &path[3..]);
}
path
}
fn to_posix_format(&self, path: &str) -> String {
let path = path.replace('\\', "/");
if path.len() >= 3
&& path.chars().nth(1).unwrap_or_default() == ':'
&& (path.chars().nth(2).unwrap_or_default() == '\\'
|| path.chars().nth(2).unwrap_or_default() == '/')
{
let drive_letter = path
.chars()
.nth(0)
.unwrap_or_default()
.to_lowercase()
.next()
.unwrap_or_default();
let rest = &path[3..];
return format!("/mnt/{}{}", drive_letter, rest);
}
if path.starts_with("/mnt/") || path.starts_with("/c/") || path.starts_with("/d/") {
return path;
}
path
}
pub fn convert_separators(&self, path: &str) -> String {
match self.path_format {
PathFormat::Wsl2 | PathFormat::Posix => path.replace('\\', "/"),
PathFormat::Windows => path.replace('/', "\\"),
}
}
pub fn needs_special_handling(&self, path: &str) -> bool {
self.is_bind_mount_path(path)
&& (self.path_format == PathFormat::Wsl2 || self.path_format == PathFormat::Windows)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normalize_path_wsl2() {
let processor = PathProcessor::new(HostOs::WindowsWsl2, PathFormat::Wsl2);
assert_eq!(
processor.normalize_path(r"C:\Users\test\data").unwrap(),
"/mnt/c/Users/test/data"
);
assert_eq!(processor.normalize_path("./data").unwrap(), "data");
assert_eq!(
processor.normalize_path("/mnt/c/Users/test").unwrap(),
"/mnt/c/Users/test"
);
}
#[test]
fn test_normalize_path_windows() {
let processor = PathProcessor::new(HostOs::WindowsNative, PathFormat::Windows);
assert_eq!(
processor.normalize_path("/mnt/c/Users/test").unwrap(),
r"C:\Users\test"
);
assert_eq!(
processor.normalize_path("C:/Users/test").unwrap(),
r"C:\Users\test"
);
}
#[test]
fn test_is_bind_mount_path_wsl2() {
let processor = PathProcessor::new(HostOs::WindowsWsl2, PathFormat::Wsl2);
assert!(processor.is_bind_mount_path("/mnt/c/Users/test/data"));
assert!(processor.is_bind_mount_path("/c/Users/test/data"));
assert!(processor.is_bind_mount_path("/data/mysql"));
assert!(processor.is_bind_mount_path("./data"));
assert!(processor.is_bind_mount_path("../data"));
assert!(processor.is_bind_mount_path(r"C:\data"));
assert!(!processor.is_bind_mount_path("volume_name"));
assert!(!processor.is_bind_mount_path(""));
}
#[test]
fn test_to_absolute_path() {
let processor = PathProcessor::new(HostOs::LinuxNative, PathFormat::Posix);
let work_dir = PathBuf::from("/workspace");
assert_eq!(
processor.to_absolute_path("/data", &work_dir).unwrap(),
PathBuf::from("/data")
);
assert_eq!(
processor.to_absolute_path("./data", &work_dir).unwrap(),
PathBuf::from("/workspace/data")
);
}
#[test]
fn test_convert_separators() {
let processor_wsl2 = PathProcessor::new(HostOs::WindowsWsl2, PathFormat::Wsl2);
assert_eq!(
processor_wsl2.convert_separators(r"C:\Users\test"),
"/mnt/c/Users/test"
);
let processor_windows = PathProcessor::new(HostOs::WindowsNative, PathFormat::Windows);
assert_eq!(
processor_windows.convert_separators("/mnt/c/Users/test"),
r"\mnt\c\Users\test"
);
}
#[test]
fn test_clean_path() {
let processor = PathProcessor::new(HostOs::LinuxNative, PathFormat::Posix);
assert_eq!(processor.clean_path("./data"), "data");
assert_eq!(processor.clean_path("data/./test"), "data/test");
assert_eq!(processor.clean_path("data/../test"), "test");
assert_eq!(processor.clean_path("./"), "");
}
#[test]
fn test_needs_special_handling() {
let processor_wsl2 = PathProcessor::new(HostOs::WindowsWsl2, PathFormat::Wsl2);
assert!(processor_wsl2.needs_special_handling("/mnt/c/data"));
let processor_posix = PathProcessor::new(HostOs::LinuxNative, PathFormat::Posix);
assert!(!processor_posix.needs_special_handling("/data"));
}
}