ge_man_lib/
config.rs

1//! Get a copy of a Steam or Lutris config file to get or modify the used compatibility tool version.
2//!
3//! This module provides structs that allow a crate to modify the global Proton version for Steam or the
4//! global Wine version in Lutris.
5//!
6use 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
15/// Represents a copy of a Steam configuration file.
16///
17/// This struct only allows modification to the global Proton version to be used by Steam.
18///
19/// # Examples
20///
21/// The struct can be written to a file by using its `Into` trait.
22///
23/// ```ignore
24/// let path = Path::from("/some/path");
25/// let steam_config = SteamConfig::create_copy(path).unwrap();
26///
27/// steam_config.set_proton_version("Proton-6.20-GE-1");
28/// let steam_config: Vec<u8> = steam_config.into();
29/// std::fs::write(path, steam_config).unwrap();
30/// ```
31pub struct SteamConfig {
32    lines: Vec<String>,
33    // This is the line data for the default compatibility tool to use.
34    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    /// Create a copy of a Steam config provided by path.
42    ///
43    /// This method reads a global Steam configuration file and creates a copy of it by reading it line by line. While
44    /// reading each line the following information is determined:
45    /// * The line that contains the default compatibility tool directory name (dir_name_line)
46    /// * The index of the dir_name_line
47    /// * The index where the value of dir_name_line begins
48    /// * The index where the value of dir_name_line ends
49    ///
50    /// This above information is stored to make future modifications easier.
51    ///
52    /// # Errors
53    ///
54    /// This method will return an error in the following cases:
55    /// * When the default compatibility tool attribute could not be found
56    /// * When any filesystem operations return an IO error
57    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        // Create in memory copy and find default value for CompatTool attribute.
66        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    /// Get the global Proton version stored in the Steam config file.
103    ///
104    /// The "version" is actually the name of the directory that contains all the version data.
105    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    /// Set the global Proton version for this file copy.
110    ///
111    /// The "version" is actually the name of the directory that contains all the version data.
112    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
130/// Represents a copy of the global Lutris config file
131///
132/// This struct only provides functionality for reading and setting the global Wine version to use in Lutris.
133///
134/// # Examples
135///
136/// The struct can be written to a file by using its `Into` trait.
137///
138/// ```ignore
139/// let path = Path::from("/some/path");
140/// let lutris_config = LutrisConfig::create_copy(path).unwrap();
141///
142/// lutris_config.set_proton_version("Proton-6.20-GE-1");
143/// let lutris_config: Vec<u8> = lutris_config.into();
144/// std::fs::write(path, lutris_config).unwrap();
145/// ```
146pub 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    /// Create a copy of a Lutris config provided by path.
156    ///
157    /// This method reads the global Wine configuration file of Lutris and creates a copy of it by reading it line by
158    /// line. While reading each line the following information is determined:
159    /// * The line that contains the default compatibility tool directory name (dir_name_line)
160    /// * The index of the dir_name_line
161    /// * The index where the value of dir_name_line begins
162    /// * The index where the value of dir_name_line ends
163    ///
164    /// This above information is stored make future modifications easier.
165    ///
166    /// # Errors
167    /// * When the default compatibility tool attribute could not be found
168    /// * When any filesystem operations return an IO error
169    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    /// Set the global Wine version for this file copy.
207    ///
208    /// The "version" is actually the name of the directory that contains all the version data.
209    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    /// Get the global Wine version stored in global Wine config for Lutris.
221    ///
222    /// The "version" is actually the name of the directory that contains all the version data.
223    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}