use crate::{PathConfig, PathError, PathResult, PathStyle};
use regex::Regex;
#[derive(Debug, Clone)]
pub struct PathConverter {
config: PathConfig,
windows_path_regex: Regex,
unix_path_regex: Regex,
drive_letter_regex: Regex,
}
impl PathConverter {
#[must_use]
pub fn new(config: &PathConfig) -> Self {
Self {
config: config.clone(),
windows_path_regex: Regex::new(r"^([a-zA-Z]:)([/\\].*)?$").unwrap(),
unix_path_regex: Regex::new(r"^/([^/].*)?$").unwrap(),
drive_letter_regex: Regex::new(r"^[a-zA-Z]:$").unwrap(),
}
}
pub fn convert(&self, path: &str, target_style: PathStyle) -> PathResult<String> {
let source_style = self.detect_style(path)?;
if source_style == target_style {
match target_style {
PathStyle::Windows => return Ok(self.normalize_windows_path(path)),
PathStyle::Unix => return Ok(Self::normalize_unix_path(path)),
PathStyle::Auto => return Ok(path.to_string()),
}
}
match (source_style, target_style) {
(PathStyle::Windows, PathStyle::Unix) => self.windows_to_unix(path),
(PathStyle::Unix, PathStyle::Windows) => Ok(self.unix_to_windows(path)),
_ => Err(PathError::UnsupportedFormat(format!(
"Unsupported conversion: {source_style:?} -> {target_style:?}"
))),
}
}
pub fn detect_style(&self, path: &str) -> PathResult<PathStyle> {
if self.windows_path_regex.is_match(path) {
return Ok(PathStyle::Windows);
}
if self.unix_path_regex.is_match(path) {
return Ok(PathStyle::Unix);
}
if path.contains('\\') && !path.contains('/') {
Ok(PathStyle::Windows)
} else if path.contains('/') && !path.contains('\\') {
Ok(PathStyle::Unix)
} else {
if path.starts_with(r"\\") || path.contains(":\\") {
Ok(PathStyle::Windows)
} else if path.starts_with('/') {
Ok(PathStyle::Unix)
} else {
Ok(super::platform::current_style())
}
}
}
fn windows_to_unix(&self, path: &str) -> PathResult<String> {
let normalized = self.normalize_windows_path(path);
if normalized.starts_with(r"\\") {
return Self::convert_unc_path(&normalized);
}
if let Some((drive, rest)) = self.split_drive_path(&normalized) {
let unix_path = self.map_drive_to_unix(&drive, &rest);
return Ok(unix_path);
}
let unix_path = normalized.replace('\\', "/");
Ok(unix_path)
}
fn unix_to_windows(&self, path: &str) -> String {
let normalized = Self::normalize_unix_path(path);
if normalized.starts_with("//") {
return normalized.replace('/', "\\");
}
for (windows_drive, unix_prefix) in &self.config.drive_mappings {
if normalized.starts_with(unix_prefix) {
let rest = &normalized[unix_prefix.len()..];
return format!("{}{}", windows_drive, rest.replace('/', "\\"));
}
}
#[cfg(not(target_os = "windows"))]
if normalized.starts_with("/mnt/")
&& let Some((drive, rest)) = crate::platform::unix::parse_unix_mount_point(&normalized)
{
let drive_str: String = drive.to_ascii_uppercase().clone();
let rest_str: String = rest.replace('/', "\\");
return format!(
"{}:{}{}",
drive_str,
rest_str,
if rest.is_empty() { "\\" } else { "" }
);
}
if normalized.starts_with('/') {
return format!("C:{}", normalized.replace('/', "\\"));
}
normalized.replace('/', "\\")
}
fn normalize_windows_path(&self, path: &str) -> String {
let mut result = path.to_string();
result = result.replace('/', "\\");
while result.contains("\\\\") && !result.starts_with(r"\\") {
result = result.replace("\\\\", "\\");
}
if result.ends_with('\\') && result.len() > 3 && !self.drive_letter_regex.is_match(&result)
{
result.pop();
}
result
}
fn normalize_unix_path(path: &str) -> String {
let mut result = path.to_string();
result = result.replace('\\', "/");
while result.contains("//") && !result.starts_with("//") {
result = result.replace("//", "/");
}
if result.ends_with('/') && result != "/" {
result.pop();
}
result
}
fn split_drive_path(&self, path: &str) -> Option<(String, String)> {
if path.len() >= 2 {
let drive = &path[..2];
if self.drive_letter_regex.is_match(drive) {
let rest = if path.len() > 2 { &path[2..] } else { "" };
return Some((drive.to_string(), rest.to_string()));
}
}
None
}
fn map_drive_to_unix(&self, drive: &str, rest: &str) -> String {
for (windows_drive, unix_mount) in &self.config.drive_mappings {
if windows_drive == drive {
return format!("{}{}", unix_mount, rest.replace('\\', "/"));
}
}
let drive_letter = drive.chars().next().unwrap().to_ascii_lowercase();
format!("/mnt/{}{}", drive_letter, rest.replace('\\', "/"))
}
fn convert_unc_path(path: &str) -> PathResult<String> {
let parts: Vec<&str> = path.split('\\').collect();
if parts.len() >= 4 {
let server = parts[2];
let share = parts[3];
let rest = if parts.len() > 4 {
parts[4..].join("/")
} else {
String::new()
};
let unix_path = format!("//{server}/{share}/{rest}");
return Ok(unix_path.trim_end_matches('/').to_string());
}
Err(PathError::ParseError(format!("Invalid UNC path: {path}")))
}
}