winepath/
lib.rs

1//! Convert between Wine and native file paths without spawning a `winepath` process.
2//!
3//! This crate implements the conversion logic in much the same way as Wine itself.
4//!
5//! > Only for use on systems that have Wine!
6use std::{
7    fmt::{self, Display, Formatter},
8    path::{Component, Path, PathBuf},
9};
10
11/// A native path on the host system.
12type NativePath = Path;
13
14/// A file path within Wine. Wrapper around a string.
15///
16/// ```rust
17/// use winepath::WinePath;
18/// let wine_path = WinePath(r"C:\windows\system32\ddraw.dll".to_string());
19/// ```
20#[derive(Debug, Clone)]
21pub struct WinePath(pub String);
22impl AsRef<str> for WinePath {
23    fn as_ref(&self) -> &str {
24        &self.0
25    }
26}
27impl From<String> for WinePath {
28    fn from(string: String) -> Self {
29        Self(string)
30    }
31}
32impl From<&str> for WinePath {
33    fn from(string: &str) -> Self {
34        Self(string.to_string())
35    }
36}
37impl Display for WinePath {
38    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
39        write!(f, "{}", self.0)
40    }
41}
42
43/// Error type.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum WinePathError {
46    /// Could not determine the wine prefix to use.
47    PrefixNotFound,
48    /// No drive letter → file path mapping is available for the given path.
49    NoDrive,
50}
51
52impl Display for WinePathError {
53    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
54        match self {
55            WinePathError::PrefixNotFound => write!(f, "could not determine wine prefix"),
56            WinePathError::NoDrive => write!(f, "native path is not mapped to a wine drive"),
57        }
58    }
59}
60
61impl std::error::Error for WinePathError {}
62
63fn default_wineprefix() -> Option<PathBuf> {
64    std::env::var_os("HOME").map(PathBuf::from).map(|mut home| {
65        home.push(".wine");
66        home
67    })
68}
69
70const ASCII_A: u8 = 0x61;
71fn drive_to_index(drive: char) -> usize {
72    assert!(drive.is_ascii_alphabetic());
73    (drive.to_ascii_lowercase() as u8 - ASCII_A) as usize
74}
75
76fn index_to_drive(index: usize) -> char {
77    assert!(index < 26);
78    char::from(ASCII_A + index as u8)
79}
80
81/// Stringify a native path, Windows-style.
82fn stringify_path(drive_prefix: &str, path: &NativePath) -> String {
83    let parts = path.components().map(|c| match c {
84        Component::RootDir => "",
85        // `path` is not a windows path
86        Component::Prefix(_) => unreachable!(),
87        Component::CurDir => ".",
88        Component::ParentDir => "..",
89        Component::Normal(part) => part.to_str().expect("path is not utf-8"),
90    });
91
92    std::iter::once(drive_prefix)
93        .chain(parts)
94        .collect::<Vec<&str>>()
95        .join(r"\")
96}
97
98type DriveCache = [Option<PathBuf>; 26];
99
100// Maybe this should be done in constructors instead
101fn create_drive_cache(prefix: &NativePath) -> DriveCache {
102    let drives_dir = prefix.join("dosdevices");
103    let mut drive_cache = DriveCache::default();
104
105    for letter in b'a'..=b'z' {
106        let drive_name = [letter, b':'];
107        let drive_name = std::str::from_utf8(&drive_name).unwrap();
108        let drive_dir = drives_dir.join(drive_name);
109        if let Ok(target) = drive_dir.read_link() {
110            if let Ok(resolved_path) = drives_dir.join(target).canonicalize() {
111                drive_cache[drive_to_index(char::from(letter))] = Some(resolved_path);
112            }
113        }
114    }
115    drive_cache
116}
117
118/// The main conversion struct: create one of these to do conversions.
119///
120/// Tracks the WINEPREFIX and the drive letter mappings so they don't have to be recomputed every
121/// time you convert a path.
122#[derive(Debug)]
123pub struct WineConfig {
124    prefix: PathBuf,
125    drive_cache: DriveCache,
126}
127
128impl WineConfig {
129    /// Determine the wine prefix from the environment.
130    pub fn from_env() -> Result<Self, WinePathError> {
131        let prefix = std::env::var_os("WINEPREFIX")
132            .map(PathBuf::from)
133            .or_else(default_wineprefix)
134            .ok_or(WinePathError::PrefixNotFound)?;
135
136        let drive_cache = create_drive_cache(&prefix);
137
138        Ok(Self {
139            prefix,
140            drive_cache,
141        })
142    }
143
144    /// Create a config assuming that the given path is a valid WINEPREFIX.
145    ///
146    /// Note that this is not validated, and you will end up with empty drive mappings if it is not
147    /// actually a wine prefix.
148    ///
149    /// You can manually validate if a directory is Wine-y *enough* by doing:
150    /// ```rust,ignore
151    /// use std::path::Path;
152    /// fn is_wineprefix_like(some_path: &Path) -> bool {
153    ///     some_path.join("dosdevices").is_dir()
154    /// }
155    /// ```
156    pub fn from_prefix(path: impl Into<PathBuf>) -> Self {
157        let prefix: PathBuf = path.into();
158        let drive_cache = create_drive_cache(&prefix);
159
160        Self {
161            prefix,
162            drive_cache,
163        }
164    }
165
166    /// Get the current wine prefix.
167    pub fn prefix(&self) -> &NativePath {
168        &self.prefix
169    }
170
171    fn find_drive_root<'p>(
172        &self,
173        path: &'p NativePath,
174    ) -> Result<(String, &'p NativePath), WinePathError> {
175        for (index, root) in self.drive_cache.iter().enumerate() {
176            if root.is_none() {
177                continue;
178            }
179            let root = root.as_ref().unwrap();
180            // Returns `err` if `root` is not a parent of `path`.
181            if let Ok(remaining) = path.strip_prefix(root) {
182                let mut drive = String::new();
183                drive.push(index_to_drive(index));
184                drive.push(':');
185                return Ok((drive, remaining));
186            }
187        }
188
189        Err(WinePathError::NoDrive)
190    }
191
192    fn to_wine_path_inner(&self, path: &NativePath) -> Result<String, WinePathError> {
193        let (root, remaining) = self.find_drive_root(path)?;
194
195        Ok(stringify_path(&root, remaining))
196    }
197
198    fn to_native_path_inner(&self, path: &str) -> Result<PathBuf, WinePathError> {
199        // TODO resolve the path…maybe?
200        assert!(path.len() >= 2);
201        assert!(
202            char::from(path.as_bytes()[0]).is_ascii_alphabetic()
203                && char::from(path.as_bytes()[1]) == ':'
204        );
205        let full_path = path;
206
207        let drive_letter = full_path.chars().next().unwrap();
208        let index = drive_to_index(drive_letter);
209        if let Some(native_root) = self.drive_cache[index].as_ref() {
210            let mut path = native_root.to_path_buf();
211            for part in full_path[2..].split('\\') {
212                path.push(part);
213            }
214            Ok(path)
215        } else {
216            Err(WinePathError::NoDrive)
217        }
218    }
219
220    /// Convert a native file path to a Wine path.
221    ///
222    /// ```rust,no_run
223    /// use winepath::WineConfig;
224    /// let config = WineConfig::from_env().unwrap();
225    /// let path = config.to_wine_path("/home/username/.wine/drive_c/Program Files/CoolApp/start.exe").unwrap();
226    /// assert_eq!(path.to_string(), r"c:\Program Files\CoolApp\start.exe");
227    /// let path = config.to_wine_path("/home/username/some-path/some-file").unwrap();
228    /// assert_eq!(path.to_string(), r"z:\home\username\some-path\some-file");
229    /// ```
230    #[inline]
231    pub fn to_wine_path(&self, path: impl AsRef<NativePath>) -> Result<WinePath, WinePathError> {
232        let native = path.as_ref();
233        self.to_wine_path_inner(native).map(WinePath)
234    }
235
236    /// Convert a Wine path to a native file path.
237    ///
238    /// ```rust,no_run
239    /// use winepath::WineConfig;
240    /// use std::path::PathBuf;
241    /// let config = WineConfig::from_env().unwrap();
242    /// let path = config.to_native_path(r"c:\Program Files\CoolApp\start.exe").unwrap();
243    /// assert_eq!(path, PathBuf::from("/home/username/.wine/drive_c/Program Files/CoolApp/start.exe"));
244    /// let path = config.to_native_path(r"z:\home\username\some-path\some-file").unwrap();
245    /// assert_eq!(path, PathBuf::from("/home/username/some-path/some-file"));
246    /// ```
247    #[inline]
248    pub fn to_native_path(&self, path: impl Into<WinePath>) -> Result<PathBuf, WinePathError> {
249        let wine_path = path.into();
250        self.to_native_path_inner(wine_path.0.as_ref())
251    }
252}