1use std::fs::File;
7use std::io::{BufRead, BufReader};
8use std::path::Path;
9
10use crate::error::{LutrisConfigError, SteamConfigError};
11
12const DEFAULT_PROTON_VERSION_INDEX: &str = r###""0""###;
13const WINE_VERSION_ATTRIBUTE: &str = "version";
14
15pub struct SteamConfig {
32 lines: Vec<String>,
33 compat_tool_line: String,
35 compat_tool_line_idx: usize,
36 compat_tool_value_start_idx: usize,
37 compat_tool_value_end_idx: usize,
38}
39
40impl SteamConfig {
41 pub fn create_copy(config_file_path: &Path) -> Result<Self, SteamConfigError> {
58 let steam_config = File::open(config_file_path)?;
59 let steam_config = BufReader::new(steam_config);
60
61 let mut passed_compat_tool_attribute = false;
62 let mut default_compat_tool_dir_name_line_idx = None;
63 let mut lines: Vec<String> = Vec::with_capacity(steam_config.capacity());
64
65 for (idx, line) in steam_config.lines().enumerate() {
67 if let Ok(line) = line {
68 lines.push(line.clone());
69
70 if line.contains("CompatToolMapping") {
71 passed_compat_tool_attribute = true;
72 }
73
74 if default_compat_tool_dir_name_line_idx.is_none()
75 && passed_compat_tool_attribute
76 && line.contains(DEFAULT_PROTON_VERSION_INDEX)
77 {
78 default_compat_tool_dir_name_line_idx = Some(idx + 2);
79 }
80 }
81 }
82
83 if let Some(dir_name_line_idx) = default_compat_tool_dir_name_line_idx {
84 let dir_name_line = lines.get(dir_name_line_idx).cloned().unwrap();
85 let mut value_indices = dir_name_line.rmatch_indices('\"');
86
87 let value_end_idx = value_indices.next().unwrap().0;
88 let value_start_idx = value_indices.next().unwrap().0 + 1;
89
90 Ok(SteamConfig {
91 lines,
92 compat_tool_line: dir_name_line,
93 compat_tool_line_idx: dir_name_line_idx,
94 compat_tool_value_start_idx: value_start_idx,
95 compat_tool_value_end_idx: value_end_idx,
96 })
97 } else {
98 Err(SteamConfigError::NoDefaultCompatToolAttribute)
99 }
100 }
101
102 pub fn proton_version(&self) -> String {
106 self.compat_tool_line[self.compat_tool_value_start_idx..self.compat_tool_value_end_idx].to_owned()
107 }
108
109 pub fn set_proton_version(&mut self, proton_dir_name: &str) {
113 self.compat_tool_line.replace_range(
114 self.compat_tool_value_start_idx..self.compat_tool_value_end_idx,
115 proton_dir_name,
116 );
117 self.lines.splice(
118 self.compat_tool_line_idx..=self.compat_tool_line_idx,
119 [self.compat_tool_line.clone()],
120 );
121 }
122}
123
124impl Into<Vec<u8>> for SteamConfig {
125 fn into(self) -> Vec<u8> {
126 self.lines.join("\n").into_bytes()
127 }
128}
129
130pub struct LutrisConfig {
147 lines: Vec<String>,
148 wine_dir_line: String,
149 wine_dir_line_idx: usize,
150 wine_dir_line_value_start_idx: usize,
151 wine_dir_line_value_end_idx: usize,
152}
153
154impl LutrisConfig {
155 pub fn create_copy(config_file_path: &Path) -> Result<Self, LutrisConfigError> {
170 let runner_config = File::open(config_file_path)?;
171 let runner_config = BufReader::new(runner_config);
172
173 let mut lines: Vec<String> = Vec::with_capacity(runner_config.capacity());
174
175 let mut dir_name_line_idx = None;
176 let mut wine_dir_line = String::new();
177 let mut wine_dir_name_line_value_start_idx = usize::default();
178 let mut wine_dir_name_line_value_end_idx = usize::default();
179
180 for (idx, line) in runner_config.lines().enumerate() {
181 let line = line?;
182
183 if line.contains(WINE_VERSION_ATTRIBUTE) {
184 dir_name_line_idx = Some(idx);
185 wine_dir_name_line_value_start_idx = line.find(": ").unwrap() + 2;
186 wine_dir_name_line_value_end_idx = line.len();
187
188 wine_dir_line = line.clone();
189 }
190 lines.push(line.clone());
191 }
192
193 if let Some(dir_name_line_idx) = dir_name_line_idx {
194 Ok(LutrisConfig {
195 lines,
196 wine_dir_line,
197 wine_dir_line_idx: dir_name_line_idx,
198 wine_dir_line_value_start_idx: wine_dir_name_line_value_start_idx,
199 wine_dir_line_value_end_idx: wine_dir_name_line_value_end_idx,
200 })
201 } else {
202 Err(LutrisConfigError::NoVersionAttribute)
203 }
204 }
205
206 pub fn set_wine_version(&mut self, wine_directory_name: &str) {
210 self.wine_dir_line.replace_range(
211 self.wine_dir_line_value_start_idx..self.wine_dir_line_value_end_idx,
212 wine_directory_name,
213 );
214 self.lines.splice(
215 self.wine_dir_line_idx..=self.wine_dir_line_idx,
216 [self.wine_dir_line.clone()],
217 );
218 }
219
220 pub fn wine_version(&self) -> String {
224 self.wine_dir_line[self.wine_dir_line_value_start_idx..self.wine_dir_line_value_end_idx].to_owned()
225 }
226}
227
228impl Into<Vec<u8>> for LutrisConfig {
229 fn into(self) -> Vec<u8> {
230 self.lines.join("\n").into_bytes()
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use std::io::BufRead;
237 use std::path::PathBuf;
238
239 use super::*;
240
241 #[test]
242 fn create_lutris_config_from_non_existent_file() {
243 let config_path = PathBuf::from("/tmp/none");
244 let result = LutrisConfig::create_copy(&config_path);
245 assert!(result.is_err());
246
247 let err = result.err().unwrap();
248 assert!(matches!(err, LutrisConfigError::IoError { .. }));
249 }
250
251 #[test]
252 fn create_lutris_config_from_file_with_no_version_property() {
253 let config_path = Path::new("test_resources/assets/wine-no-version.yml");
254 let result = LutrisConfig::create_copy(&config_path);
255 assert!(result.is_err());
256
257 let err = result.err().unwrap();
258 assert!(matches!(err, LutrisConfigError::NoVersionAttribute));
259 }
260
261 #[test]
262 fn create_lutris_config_copy_with_modified_default_wine_runner_version() {
263 let lutris_runner_dir = "lutris-ge-6.20-1-x86_64";
264 let config_file_path = Path::new("test_resources/assets/wine.yml");
265 let mut lutris_config = LutrisConfig::create_copy(config_file_path).unwrap();
266 lutris_config.set_wine_version(lutris_runner_dir);
267
268 let bytes_copy: Vec<u8> = lutris_config.into();
269
270 let mut lines_copy = BufReader::new(std::io::Cursor::new(bytes_copy)).lines();
271 let config_file = BufReader::new(File::open(config_file_path).unwrap());
272
273 for (idx, line) in config_file.lines().enumerate() {
274 let line = line.unwrap();
275 let line_copy = lines_copy.next().unwrap().unwrap();
276
277 if idx == 8 {
278 assert_eq!(line_copy, format!(r###" version: {}"###, lutris_runner_dir));
279 assert_eq!(line, r###" version: lutris-ge-6.21-1-x86_64"###);
280 } else {
281 assert_eq!(line, line_copy);
282 }
283 }
284 }
285
286 #[test]
287 fn read_wine_version_from_lutris_config() {
288 let config_file = Path::new("test_resources/assets/wine.yml");
289 let lutris_config = LutrisConfig::create_copy(config_file).unwrap();
290
291 let version = lutris_config.wine_version();
292 assert_eq!(version, "lutris-ge-6.21-1-x86_64");
293 }
294
295 #[test]
296 fn create_steam_config_copy_with_modified_default_proton_version() {
297 let proton_dir_name = "Proton-6.20-GE-1";
298 let config_file_path = Path::new("test_resources/assets/config.vdf");
299
300 let mut steam_config = SteamConfig::create_copy(config_file_path).unwrap();
301 steam_config.set_proton_version(proton_dir_name);
302
303 let bytes_copy: Vec<u8> = steam_config.into();
304
305 let mut lines_copy = bytes_copy.lines();
306 let conf_file = BufReader::new(File::open(config_file_path).unwrap());
307
308 for (idx, line) in conf_file.lines().enumerate() {
309 let line = line.unwrap();
310 let line_copy = lines_copy.next().unwrap().unwrap();
311
312 if idx == 12 {
313 assert_eq!(line_copy, format!(r###" "name" "{}""###, proton_dir_name));
314 assert_eq!(line, r###" "name" "Proton-6.21-GE-2""###)
315 } else {
316 assert_eq!(line, line_copy);
317 }
318 }
319 }
320
321 #[test]
322 fn read_proton_version_from_steam_config() {
323 let config_file = Path::new("test_resources/assets/config.vdf");
324
325 let steam_config = SteamConfig::create_copy(config_file).unwrap();
326 let version = steam_config.proton_version();
327
328 assert_eq!(version, "Proton-6.21-GE-2");
329 }
330
331 #[test]
332 fn create_steam_config_copy_from_file_with_no_compat_tool_attribute() {
333 let config_file = Path::new("test_resources/assets/config-no-compat-tool-attr.vdf");
334 let result = SteamConfig::create_copy(&config_file);
335 assert!(result.is_err());
336
337 let err = result.err().unwrap();
338 assert!(matches!(err, SteamConfigError::NoDefaultCompatToolAttribute));
339 }
340
341 #[test]
342 fn create_steam_config_copy_from_file_with_no_default_proton_version() {
343 let config_file = Path::new("test_resources/assets/config-no-default-version.vdf");
344 let result = SteamConfig::create_copy(&config_file);
345 assert!(result.is_err());
346
347 let err = result.err().unwrap();
348 assert!(matches!(err, SteamConfigError::NoDefaultCompatToolAttribute));
349 }
350
351 #[test]
352 fn create_steam_config_copy_from_invalid_path() {
353 let config_file = PathBuf::from("/tmp/none");
354 let result = SteamConfig::create_copy(&config_file);
355 assert!(result.is_err());
356
357 let err = result.err().unwrap();
358 assert!(matches!(err, SteamConfigError::IoError { .. }));
359 }
360}