use std::{
fmt::{self, Display, Formatter},
path::{Component, Path, PathBuf},
};
type NativePath = Path;
#[derive(Debug, Clone)]
pub struct WinePath(pub String);
impl AsRef<str> for WinePath {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<String> for WinePath {
fn from(string: String) -> Self {
Self(string)
}
}
impl From<&str> for WinePath {
fn from(string: &str) -> Self {
Self(string.to_string())
}
}
impl Display for WinePath {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WinePathError {
PrefixNotFound,
NoDrive,
}
impl Display for WinePathError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
WinePathError::PrefixNotFound => write!(f, "could not determine wine prefix"),
WinePathError::NoDrive => write!(f, "native path is not mapped to a wine drive"),
}
}
}
impl std::error::Error for WinePathError {}
fn default_wineprefix() -> Option<PathBuf> {
std::env::var_os("HOME").map(PathBuf::from).map(|mut home| {
home.push(".wine");
home
})
}
const ASCII_A: u8 = 0x61;
fn drive_to_index(drive: char) -> usize {
assert!(drive.is_ascii_alphabetic());
(drive.to_ascii_lowercase() as u8 - ASCII_A) as usize
}
fn index_to_drive(index: usize) -> char {
assert!(index < 26);
char::from(ASCII_A + index as u8)
}
fn stringify_path(drive_prefix: &str, path: &NativePath) -> String {
let parts = path.components().map(|c| match c {
Component::RootDir => "",
Component::Prefix(_) => unreachable!(),
Component::CurDir => ".",
Component::ParentDir => "..",
Component::Normal(part) => part.to_str().expect("path is not utf-8"),
});
std::iter::once(drive_prefix)
.chain(parts)
.collect::<Vec<&str>>()
.join(r"\")
}
type DriveCache = [Option<PathBuf>; 26];
fn create_drive_cache(prefix: &NativePath) -> DriveCache {
let drives_dir = prefix.join("dosdevices");
let mut drive_cache = DriveCache::default();
for letter in b'a'..=b'z' {
let drive_name = [letter, b':'];
let drive_name = std::str::from_utf8(&drive_name).unwrap();
let drive_dir = drives_dir.join(drive_name);
if let Ok(target) = drive_dir.read_link() {
if let Ok(resolved_path) = drives_dir.join(target).canonicalize() {
drive_cache[drive_to_index(char::from(letter))] = Some(resolved_path);
}
}
}
drive_cache
}
#[derive(Debug)]
pub struct WineConfig {
prefix: PathBuf,
drive_cache: DriveCache,
}
impl WineConfig {
pub fn from_env() -> Result<Self, WinePathError> {
let prefix = std::env::var_os("WINEPREFIX")
.map(PathBuf::from)
.or_else(default_wineprefix)
.ok_or(WinePathError::PrefixNotFound)?;
let drive_cache = create_drive_cache(&prefix);
Ok(Self {
prefix,
drive_cache,
})
}
pub fn from_prefix(path: impl Into<PathBuf>) -> Self {
let prefix: PathBuf = path.into();
let drive_cache = create_drive_cache(&prefix);
Self {
prefix,
drive_cache,
}
}
pub fn prefix(&self) -> &NativePath {
&self.prefix
}
fn find_drive_root<'p>(
&self,
path: &'p NativePath,
) -> Result<(String, &'p NativePath), WinePathError> {
for (index, root) in self.drive_cache.iter().enumerate() {
if root.is_none() {
continue;
}
let root = root.as_ref().unwrap();
if let Ok(remaining) = path.strip_prefix(root) {
let mut drive = String::new();
drive.push(index_to_drive(index));
drive.push(':');
return Ok((drive, remaining));
}
}
Err(WinePathError::NoDrive)
}
fn to_wine_path_inner(&self, path: &NativePath) -> Result<String, WinePathError> {
let (root, remaining) = self.find_drive_root(path)?;
Ok(stringify_path(&root, remaining))
}
fn to_native_path_inner(&self, path: &str) -> Result<PathBuf, WinePathError> {
assert!(path.len() >= 2);
assert!(
char::from(path.as_bytes()[0]).is_ascii_alphabetic()
&& char::from(path.as_bytes()[1]) == ':'
);
let full_path = path;
let drive_letter = full_path.chars().next().unwrap();
let index = drive_to_index(drive_letter);
if let Some(native_root) = self.drive_cache[index].as_ref() {
let mut path = native_root.to_path_buf();
for part in full_path[2..].split('\\') {
path.push(part);
}
Ok(path)
} else {
Err(WinePathError::NoDrive)
}
}
#[inline]
pub fn to_wine_path(&self, path: impl AsRef<NativePath>) -> Result<WinePath, WinePathError> {
let native = path.as_ref();
self.to_wine_path_inner(native).map(WinePath)
}
#[inline]
pub fn to_native_path(&self, path: impl Into<WinePath>) -> Result<PathBuf, WinePathError> {
let wine_path = path.into();
self.to_native_path_inner(wine_path.0.as_ref())
}
}