use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConvertError {
#[error("无法识别路径格式: {0}")]
UnrecognizedFormat(String),
#[error("无效的盘符: {0}")]
InvalidDrive(char),
#[error("UNC 路径格式不正确: {0}")]
InvalidUnc(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
ToWsl,
ToWindows,
}
#[derive(Debug, Clone)]
pub struct ConvertResult {
pub original: String,
pub converted: String,
pub direction: Direction,
}
pub struct Converter {
mount_prefix: String,
}
impl Converter {
pub fn new() -> Self {
Self {
mount_prefix: detect_mount_prefix(),
}
}
#[cfg(test)]
pub fn with_mount_prefix(prefix: &str) -> Self {
Self {
mount_prefix: prefix.to_string(),
}
}
pub fn to_wsl(&self, input: &str) -> Result<ConvertResult, ConvertError> {
let trimmed = clean_path_input(input);
if trimmed.starts_with('/') {
return Ok(ConvertResult {
original: input.to_string(),
converted: trimmed.to_string(),
direction: Direction::ToWsl,
});
}
if trimmed == "~" || trimmed.starts_with("~/") {
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/home"));
let rest = trimmed.strip_prefix('~').unwrap_or("");
return Ok(ConvertResult {
original: input.to_string(),
converted: format!("{}{}", home.display(), rest),
direction: Direction::ToWsl,
});
}
if trimmed.starts_with("\\\\") || trimmed.starts_with("//") {
return self.convert_unc(&trimmed, input);
}
if let Some(drive) = extract_drive_letter(&trimmed) {
return self.convert_drive_path(drive, &trimmed, input);
}
Err(ConvertError::UnrecognizedFormat(input.to_string()))
}
pub fn to_windows(&self, input: &str, mixed: bool) -> Result<ConvertResult, ConvertError> {
let trimmed = clean_path_input(input);
if !trimmed.starts_with('/') {
return Err(ConvertError::UnrecognizedFormat(input.to_string()));
}
let prefix = format!("{}/", self.mount_prefix);
if trimmed.starts_with(&prefix) || trimmed == self.mount_prefix {
let rest = trimmed
.strip_prefix(&prefix)
.or_else(|| trimmed.strip_prefix(&self.mount_prefix))
.unwrap_or("");
if !rest.is_empty() {
let drive_char = rest
.split('/')
.next()
.unwrap_or("")
.chars()
.next()
.ok_or_else(|| ConvertError::UnrecognizedFormat(input.to_string()))?;
if drive_char.is_ascii_alphabetic() {
let path_part = rest
.strip_prefix(drive_char)
.unwrap_or("")
.trim_start_matches('/');
let sep = if mixed { "/" } else { "\\" };
let win_path = format!(
"{}:{}{}",
drive_char.to_uppercase(),
sep,
path_part.replace('/', sep)
);
return Ok(ConvertResult {
original: input.to_string(),
converted: win_path,
direction: Direction::ToWindows,
});
}
}
}
let distro = detect_distro_name();
let win_path = if mixed {
format!("//wsl$/{}/{}", distro, trimmed.trim_start_matches('/'))
} else {
format!(
"\\\\wsl$\\{}\\{}",
distro,
trimmed.trim_start_matches('/').replace('/', "\\")
)
};
Ok(ConvertResult {
original: input.to_string(),
converted: win_path,
direction: Direction::ToWindows,
})
}
fn convert_unc(&self, cleaned: &str, original: &str) -> Result<ConvertResult, ConvertError> {
let normalized = cleaned.replace('\\', "/");
let parts: Vec<&str> = normalized.trim_start_matches('/').split('/').collect();
if parts.len() < 2 {
return Err(ConvertError::InvalidUnc(original.to_string()));
}
let server = parts[0];
if server == "wsl$" || server == "wsl.localhost" {
if parts.len() < 3 {
return Err(ConvertError::InvalidUnc(original.to_string()));
}
let path = parts[2..].join("/");
return Ok(ConvertResult {
original: original.to_string(),
converted: format!("/{}", path),
direction: Direction::ToWsl,
});
}
let unc_path = parts.join("/");
Ok(ConvertResult {
original: original.to_string(),
converted: format!("{}/unc/{}", self.mount_prefix, unc_path),
direction: Direction::ToWsl,
})
}
fn convert_drive_path(
&self,
drive: char,
cleaned: &str,
original: &str,
) -> Result<ConvertResult, ConvertError> {
let rest = &cleaned[2..];
let rest = rest.replace('\\', "/");
let rest = rest.trim_start_matches('/');
let wsl_path = format!("{}/{}/{}", self.mount_prefix, drive.to_lowercase(), rest);
Ok(ConvertResult {
original: original.to_string(),
converted: wsl_path,
direction: Direction::ToWsl,
})
}
}
impl Default for Converter {
fn default() -> Self {
Self::new()
}
}
pub fn clean_path_input(input: &str) -> String {
let mut s = input.trim();
while (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
s = &s[1..s.len() - 1];
}
s.trim().to_string()
}
fn extract_drive_letter(path: &str) -> Option<char> {
let bytes = path.as_bytes();
if bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' {
Some(bytes[0].to_ascii_lowercase() as char)
} else {
None
}
}
fn detect_mount_prefix() -> String {
if let Ok(content) = std::fs::read_to_string("/etc/wsl.conf") {
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("root") && trimmed.contains('=') {
if let Some(val) = trimmed.split('=').nth(1) {
let val = val.trim().trim_matches('"').trim_matches('/');
if !val.is_empty() {
return format!("/{}", val);
}
}
}
}
}
"/mnt".to_string()
}
fn detect_distro_name() -> String {
if let Ok(content) = std::fs::read_to_string("/etc/os-release") {
for line in content.lines() {
if let Some(val) = line.strip_prefix("PRETTY_NAME=") {
return val.trim_matches('"').to_string();
}
}
}
std::env::var("WSL_DISTRO_NAME").unwrap_or_else(|_| "Ubuntu".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_drive_path() {
let c = Converter::with_mount_prefix("/mnt");
let r = c.to_wsl("C:\\Users\\foo").unwrap();
assert_eq!(r.converted, "/mnt/c/Users/foo");
}
#[test]
fn test_forward_slash() {
let c = Converter::with_mount_prefix("/mnt");
let r = c.to_wsl("C:/Users/foo").unwrap();
assert_eq!(r.converted, "/mnt/c/Users/foo");
}
#[test]
fn test_uppercase_drive() {
let c = Converter::with_mount_prefix("/mnt");
let r = c.to_wsl("D:\\Projects").unwrap();
assert_eq!(r.converted, "/mnt/d/Projects");
}
#[test]
fn test_already_wsl_path() {
let c = Converter::with_mount_prefix("/mnt");
let r = c.to_wsl("/home/user").unwrap();
assert_eq!(r.converted, "/home/user");
}
#[test]
fn test_unc_wsl_path() {
let c = Converter::with_mount_prefix("/mnt");
let r = c.to_wsl("\\\\wsl$\\Ubuntu\\home\\user").unwrap();
assert_eq!(r.converted, "/home/user");
}
#[test]
fn test_unc_wsl_localhost() {
let c = Converter::with_mount_prefix("/mnt");
let r = c
.to_wsl("\\\\wsl.localhost\\Ubuntu-22.04\\home\\user\\projects")
.unwrap();
assert_eq!(r.converted, "/home/user/projects");
}
#[test]
fn test_unc_network_share() {
let c = Converter::with_mount_prefix("/mnt");
let r = c.to_wsl("\\\\server\\share\\file.txt").unwrap();
assert_eq!(r.converted, "/mnt/unc/server/share/file.txt");
}
#[test]
fn test_quoted_path() {
let c = Converter::with_mount_prefix("/mnt");
let r = c.to_wsl("\"C:\\Users\\foo\"").unwrap();
assert_eq!(r.converted, "/mnt/c/Users/foo");
}
#[test]
fn test_single_quoted_path() {
let c = Converter::with_mount_prefix("/mnt");
let r = c.to_wsl("'C:\\Users\\foo'").unwrap();
assert_eq!(r.converted, "/mnt/c/Users/foo");
}
#[test]
fn test_leading_trailing_spaces() {
let c = Converter::with_mount_prefix("/mnt");
let r = c.to_wsl(" C:\\Users\\foo ").unwrap();
assert_eq!(r.converted, "/mnt/c/Users/foo");
}
#[test]
fn test_to_windows_basic() {
let c = Converter::with_mount_prefix("/mnt");
let r = c.to_windows("/mnt/c/Users/foo", false).unwrap();
assert_eq!(r.converted, "C:\\Users\\foo");
}
#[test]
fn test_to_windows_mixed() {
let c = Converter::with_mount_prefix("/mnt");
let r = c.to_windows("/mnt/c/Users/foo", true).unwrap();
assert_eq!(r.converted, "C:/Users/foo");
}
#[test]
fn test_custom_mount_prefix() {
let c = Converter::with_mount_prefix("/drv");
let r = c.to_wsl("C:\\Users\\foo").unwrap();
assert_eq!(r.converted, "/drv/c/Users/foo");
}
#[test]
fn test_drive_only() {
let c = Converter::with_mount_prefix("/mnt");
let r = c.to_wsl("C:\\").unwrap();
assert_eq!(r.converted, "/mnt/c/");
}
#[test]
fn test_clean_path_input() {
assert_eq!(clean_path_input(" \"C:\\foo\" "), "C:\\foo");
assert_eq!(clean_path_input("'C:\\foo'"), "C:\\foo");
assert_eq!(clean_path_input(" C:\\foo "), "C:\\foo");
}
#[test]
fn test_extract_drive_letter() {
assert_eq!(extract_drive_letter("C:\\foo"), Some('c'));
assert_eq!(extract_drive_letter("d:/bar"), Some('d'));
assert_eq!(extract_drive_letter("/home"), None);
assert_eq!(extract_drive_letter("foo"), None);
}
}