1use std::{
7 fmt::{self, Display, Formatter},
8 path::{Component, Path, PathBuf},
9};
10
11type NativePath = Path;
13
14#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum WinePathError {
46 PrefixNotFound,
48 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
81fn stringify_path(drive_prefix: &str, path: &NativePath) -> String {
83 let parts = path.components().map(|c| match c {
84 Component::RootDir => "",
85 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
100fn 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#[derive(Debug)]
123pub struct WineConfig {
124 prefix: PathBuf,
125 drive_cache: DriveCache,
126}
127
128impl WineConfig {
129 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 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 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 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 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 #[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 #[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}