proton_finder/
linux.rs

1use std::{collections::HashMap, env, ffi::OsString, fs::File, io::{BufRead, BufReader}, path::PathBuf, str::FromStr};
2
3pub const ENV_STEAM_DIR: &str = "STEAM_DIR";
4
5/// Rust is at times writen by bone headed idiots who,
6/// just because they are way smarter then all of us still do stuff like this:
7/// Refuse a tilde to be resolved on unix systems, but allow a pathbuf with it to be created with
8/// no error...
9fn expand_tilde(str: &str) -> Option<PathBuf> {
10    if let Some(stripped) = str.strip_prefix("~/") {
11        if let Some(mut home) = dirs::home_dir() {
12            home.push(stripped);
13            Some(home)
14        } else {
15            None
16        }
17    } else {
18        // This was not a home base relative path
19        PathBuf::from_str(str).ok()
20    }
21}
22
23/// Returns the value in env `$STEAM_DIR` (if set)
24///
25/// This will still return the value, even if `no_tricks` is set
26pub fn get_steam_dir_env_value() -> Option<OsString> {
27    env::var_os(ENV_STEAM_DIR)
28}
29
30/// Returns the path set in env `$STEAM_DIR`
31///
32/// This will still return the value, even if `no_tricks` is set
33/// If no path was set returns Err(false)
34/// If the value set was not a valid path (or did not exist) returns Err(true)
35pub fn get_steam_dir_env_path() -> Result<PathBuf, bool> {
36    if let Some(val) = get_steam_dir_env_value() {
37        if let Ok(path) = PathBuf::try_from(val) {
38            if path.is_dir() {
39                return Ok(path);
40            }
41        }
42
43        Err(true)
44    } else {
45        Err(false)
46    }
47}
48
49/// An existing steamroot folder with steamapps and steamruntime
50#[derive(Debug, Clone)]
51pub struct SteamRoot {
52    path: PathBuf,
53    steamapps: PathBuf
54}
55
56impl SteamRoot {
57    pub fn get_root(&self) -> PathBuf {
58        self.path.clone()
59    }
60
61    /// The steamapps folder in the root directory of steam
62    pub fn get_steamapps_folder(&self) -> PathBuf {
63        self.steamapps.clone()
64    }
65
66    /// Returns the library in which the game is installed.
67    /// The game prefix data might be elsewhere (like with the Steamdeck SD-card libraries store
68    /// compatdata still in the steamroot).
69    ///
70    /// This is based on the libraryfolder.vdf file, which steam updates infrequently, meaning if
71    /// you just moved the game it will still be noted with it's old location (so this value is
72    /// unfortunatly not that reliable)
73    pub fn get_install_library(&self, game_id: u32) -> Option<SteamLibrary> {
74        let vdf = self.read_library_folders_vdf_file()?;
75        let game_id = game_id.to_string();
76
77        for (_,lib) in vdf.pairs.iter() {
78            if let VdfValue::Complex(lib) = lib {
79                
80                // Extracting necessary values
81                if let (Some(VdfValue::Simple(path)),Some(VdfValue::Complex(apps))) = (lib.pairs.get("path"), lib.pairs.get("apps")) {
82                    
83                    if apps.pairs.contains_key(&game_id) {
84                        // Found the game
85                        
86                        let buf = PathBuf::from_str(path).ok()?;
87                        return SteamLibrary::from_path(&buf);
88                    }
89                }
90            }
91        }
92
93        None
94    }
95
96    /// Attempts to find the prefix for a given game via it's game id
97    pub fn get_prefix(&self, game_id: u32) -> Option<ProtonPrefix> {
98        if let Some(lib) = self.get_install_library(game_id) {
99            if let Some(pre) = lib.get_prefix(game_id) {
100                // Found prefix already
101                return Some(pre);
102            }
103        }
104
105        // As a fallback we iterate through all libraries
106        // Especially duye to the vdf being potentially out of date
107        for lib in self.get_libraries() {
108            if let Some(pre) = lib.get_prefix(game_id) {
109                // Found prefix already
110                return Some(pre);
111            }
112        }
113
114        // Root is "garanteed" to be included, so no need to recheck
115        None
116    }
117
118    /// Returns you all libraries part of this steamroot
119    /// This function always returns at least 1 result, that being the root library
120    pub fn get_libraries(&self) -> Vec<SteamLibrary> {
121        let mut res = Vec::<SteamLibrary>::new();
122
123        if let Some(vdf) = self.read_library_folders_vdf_file() {
124            // Iterating over all entires
125            for (_,lib) in vdf.pairs.iter() {
126                if let VdfValue::Complex(lib) = lib {
127
128                    // retrieving the path for this library
129                    if let Some(VdfValue::Simple(p)) = lib.pairs.get("path") {
130                        
131                        // Parsing into wrapper
132                        if let Ok(path) = PathBuf::from_str(p) {
133                            if let Some(item) = SteamLibrary::from_path(&path) {
134                                res.push(item);
135                            }
136                        }
137                    }
138                }
139            }
140        }
141
142
143        if res.is_empty() {
144            // Fallback to garantee at least the root exists
145            res.push(SteamLibrary { steamapps: self.get_steamapps_folder(), is_root: true });
146        }
147
148        res
149    }
150
151    /// Reads the libraryfolders file for this streamroot,
152    /// returning on success the contained "libraryfolders" struct (so you can directly access the
153    /// libraries).  
154    ///
155    /// This is in contrast to calling `parse_vdf_file` manually, which would give you the root
156    /// object that contains this struct under said key (so this function here saves you one step).
157    pub fn read_library_folders_vdf_file(&self) -> Option<VdfStruct> {
158        let mut path = self.get_steamapps_folder();
159        path.push("libraryfolders.vdf");
160
161        // We use remove here to avoid a clone call
162        let mut vdf = parse_vdf_file(&path)?;
163        if let Some(VdfValue::Complex(res)) = vdf.pairs.remove("libraryfolders") {
164            Some(res)
165        } else {
166            None
167        }
168    }
169}
170
171/// Parses a vdf file at the given location
172///
173/// This was made to parse the libraryfolders.vdf,
174/// so other vdf files might not get properly parsed
175pub fn parse_vdf_file(file_path: &PathBuf) -> Option<VdfStruct> {
176
177    // Parses structs recusrively
178    fn parse_struct(reader: &mut BufReader<File>, root: bool) -> Option<VdfStruct> {
179        let mut obj = VdfStruct { pairs: HashMap::new() };
180        
181        let mut line = String::new();
182        while let Ok(length) = reader.read_line(&mut line) {
183            // EOF detection
184            if length == 0 {
185                if root {
186                    return Some(obj);
187                } else {
188                    return None;
189                }
190            }
191
192            let trimed = line.trim();
193            
194            if trimed == "}" {
195                // Object ended
196                return Some(obj);
197            }
198
199            // Key parsing
200            if let Some(part) = trimed.strip_prefix('"') {
201                let (key, part) = part.split_once('"')?;
202
203                let part_trimed = part.trim();
204
205                let (key, value) = if let Some((_,val_untrimmed)) = part_trimed.split_once('"') {
206                    // This handles simpletype
207                    let (val,_) = val_untrimmed.split_once('"')?;
208                    (key.to_string(), VdfValue::Simple(val.to_string()))
209                } else {
210                    // This handles complextype by reading another line to find the bracket
211                    // open, and then do recursion
212                    let key = key.to_string();
213
214                    line.clear();
215                    if reader.read_line(&mut line).ok()? == 0 {
216                        // EOF between the key and the struct
217                        return None;
218                    }
219
220                    let trimed = line.trim();
221                    if trimed != "{" {
222                        // Unexpected symbol
223                        return None;
224                    }
225
226                    let s = parse_struct(reader, false)?;
227                    (key, VdfValue::Complex(s))
228                };
229
230                obj.pairs.insert(key, value);
231            } else if !trimed.is_empty() {
232                return None;
233            };
234
235            line.clear();
236        }
237
238        None
239    }
240
241
242    let file = File::open(file_path).ok()?;
243    let mut reader = BufReader::new(file);
244
245    parse_struct(&mut reader, true)
246}
247
248/// Represents a Vdf Complextype with multiple key value pairs, where the value can be further nested structs
249#[derive(Debug, Clone)]
250pub struct VdfStruct {
251    pub pairs: HashMap<String, VdfValue>
252}
253
254/// Represents the two value types for vdf:
255/// - Simpletype, which is a String value on the same line as it's key
256/// - Complextype, which is a struct started with { and ended with } on seperate lines
257#[derive(Debug, Clone)]
258pub enum VdfValue {
259    Complex(VdfStruct),
260    Simple(String)
261}
262
263
264/// Wrapper around a SteamLibrary with a compatdata folder
265#[derive(Debug, Clone)]
266pub struct SteamLibrary {
267    steamapps: PathBuf,
268    is_root: bool
269}
270
271
272impl SteamLibrary {
273    /// Produces a new wrapper for the given location, as long as a compatdata folder is present
274    ///
275    /// Important: You are passing in the library folder, as set in steam, not the contained
276    /// steamapps folder!
277    pub fn from_path(lib: &PathBuf) -> Option<Self> {
278        let mut apps = has_steamapps(lib)?;
279        apps.push("compatdata");
280        if !apps.exists() {
281            return None;
282        }
283        apps.pop();
284        
285        Some(Self { steamapps: apps, is_root: has_runtime(lib) })
286    }
287
288    /// Attempts to find the prefix for a given game via it's game id.  
289    ///
290    /// This only checks if there is a prefix for the game in THIS library, so:  
291    /// - The game might be installed here, but the prefix is left in the root (Steamdeck SD-Card behavior)
292    /// - There is leftover data from the game being here that has not been cleaned up (you get a
293    /// prefix then, but you shouldn't use it, as it is irrelevant to the current install of the game)
294    /// - The game is in another library (then you need to check the other Libaries).
295    /// 
296    /// In general, it is better to just SteamRoot, as this compensates for these anomalies
297    pub fn get_prefix(&self, game_id: u32) -> Option<ProtonPrefix> {
298        let mut path = self.steamapps.clone();
299        path.push("compatdata");
300        path.push(game_id.to_string());
301        path.push("pfx");
302
303        if let Some(mut pfx) = ProtonPrefix::from_path(path) {
304            pfx.game = game_id;
305            return Some(pfx);
306        }
307
308        None
309    }
310
311    /// The steamapps folder from this steam library
312    pub fn get_steamapps_folder(&self) -> PathBuf {
313        self.steamapps.clone()
314    }
315
316    /// If this is the root library (and only if),
317    /// then you will be able to retrieve the Steamroot from it again
318    pub fn convert_to_steamroot(&self) -> Option<SteamRoot> {
319        if self.is_root {
320            let mut folder = self.steamapps.clone();
321            folder.pop();
322            steam_root_from(folder)
323        } else {
324            None
325        }
326    }
327
328    /// Retruns if this Library is the one and only root library
329    pub fn is_root(&self) -> bool {
330        self.is_root
331    }
332}
333
334
335fn has_runtime(steam_root: &PathBuf) -> bool {
336    let mut steam_runtime = steam_root.clone();
337    steam_runtime.push("ubuntu12_32");
338
339    if steam_runtime.is_dir() {
340        return true;
341    }
342
343    // Future proofing for 64bit only Steam
344    steam_runtime.pop();
345    steam_runtime.push("ubuntu12_64");
346    
347    steam_runtime.is_dir()
348}
349
350fn has_steamapps(steam_root: &PathBuf) -> Option<PathBuf> {
351    // any spelling of steamapps is apparently valid, so we have to check all folders
352    if let Ok(mut iter) = steam_root.read_dir() {
353        while let Some(Ok(item)) = iter.next() {
354            if item.file_name().to_ascii_lowercase() == OsString::from_str("steamapps").expect("valid os string") && item.path().is_dir() {
355                return Some(item.path());
356            }
357        }
358    }
359
360    None
361}
362
363/// This verifies that at a given path exists a steam root folder
364pub fn steam_root_from(path: PathBuf) -> Option<SteamRoot> {
365    if !path.is_dir() {
366        return None;
367    }
368    
369    if has_runtime(&path) {
370        if let Some(apps) = has_steamapps(&path) {
371            return Some(SteamRoot { path, steamapps: apps });
372        }
373    }
374
375    None
376}
377
378/// Returns the steam root from the path set in env `$STEAM_DIR`
379///
380/// This will still return the value, even if `no_tricks` is set
381/// If no path was set returns Err(false)
382/// If the value set was not a valid path (or did not exist) returns Err(true)
383pub fn steam_root_env() -> Result<SteamRoot, bool> {
384    steam_root_from(get_steam_dir_env_path()?).ok_or(true)
385}
386
387// Common Steam Paths
388const STEAM_DOT_STEAM: &str = "~/.steam/steam";
389const STEAM_LOCAL_SHARE: &str = "~/.local/share/Steam";
390const STEAM_FLATPAK: &str = "~/.var/app/com.valvesoftware.Steam/data/Steam/";
391
392/// Returns the first steam root found.
393///
394/// The Result indicates if an invalid `STEAM_DIR` was set (if you set `no_tricks` you can disgard
395/// all Err, it will always return Ok). It return Ok on unset `STEAM_DIR`
396/// Err(Some(other)) indicates another standard steam root was found, Err(None) that none.
397/// Similarly, Ok(Some(root)) indicates the a root was found (this is the first one), Ok(None) indicates no root was found
398///
399/// The order in which steam roots are found is:
400/// - $STEAM_DIR (skipped if `no_tricks`)
401/// - ~/.steam/steam/
402/// - ~/.local/share/steam/
403/// - ~/.var/app/com.valvesoftware.Steam/data/Steam/
404pub fn find_steam_root() -> Result<Option<SteamRoot>, Option<SteamRoot>> {
405    let err = if cfg!(not(no_tricks)) {
406        match steam_root_env() {
407            Ok(root) => return Ok(Some(root)),
408            Err(err) => err
409        }
410    } else {
411        false
412    };
413
414    let path = expand_tilde(STEAM_DOT_STEAM).expect("A Path from home should always resolve");
415    if let Some(root) = steam_root_from(path) {
416        return match err {
417            true => Err(Some(root)),
418            false => Ok(Some(root))
419        };
420    }
421
422    // protontricks checks for both paths, so we will too
423    let path = expand_tilde(STEAM_LOCAL_SHARE).expect("A Path from home should always resolve");
424    if let Some(root) = steam_root_from(path) {
425        return match err {
426            true => Err(Some(root)),
427            false => Ok(Some(root))
428        };
429    }
430
431    // Flatpak
432    let path = expand_tilde(STEAM_FLATPAK).expect("A Path from home should always resolve");
433    if let Some(root) = steam_root_from(path) {
434        return match err {
435            true => Err(Some(root)),
436            false => Ok(Some(root))
437        };
438    }
439
440    match err {
441        true => Err(None),
442        false => Ok(None)
443    }
444}
445
446/// Returns all the steam roots found.
447///
448/// The Result indicates if an invalid `STEAM_DIR` was set (if you set `no_tricks` you can disgard
449/// all Err, it will always return Ok). It return Ok on unset `STEAM_DIR`
450/// If ~/.steam/steam symlinks to ~/.local/share/steam/ then the path will be included only once
451///
452/// The order in which steam roots are found is:
453/// - $STEAM_DIR (skipped if `no_tricks`)
454/// - ~/.steam/steam/
455/// - ~/.local/share/steam/
456/// - ~/.var/app/com.valvesoftware.Steam/data/Steam/
457pub fn find_all_steam_roots() -> Result<Vec<SteamRoot>, Vec<SteamRoot>> {
458    // it will be rare we even get above 2, but still...
459    let mut roots = Vec::<SteamRoot>::with_capacity(4);
460    let err = if cfg!(not(no_tricks)) {
461        match steam_root_env() {
462            Ok(root) => {
463                roots.push(root);
464                false
465            },
466            Err(err) => err
467        }
468    } else {
469        false
470    };
471
472    // Technically, if the user passes in any of the three following paths as the $STEAM_DIR we
473    // will have that path twice... not a big deal, but still
474    
475    let path = expand_tilde(STEAM_DOT_STEAM).expect("A Path from home should always resolve");
476    if let Some(root) = steam_root_from(path.clone()) {
477        roots.push(root);
478    }
479
480    // Usually ~/.steam/steam links to ~/.local/share/steam , so if this is the case we will skip
481    // adding what is essentially the same folder twice
482    let local_path = expand_tilde(STEAM_LOCAL_SHARE).expect("A Path from home should always resolve");
483    let already = if path.is_symlink() {
484        if let (Ok(link), Ok(local_path)) = (path.canonicalize(), local_path.canonicalize()) {
485            local_path == link
486        } else {
487            false
488        }
489    } else {
490        false
491    };
492
493    if !already {
494        if let Some(root) = steam_root_from(local_path) {
495            roots.push(root);
496        }
497    }
498
499    // Flatpak
500    let path = expand_tilde(STEAM_FLATPAK).expect("A Path from home should always resolve");
501    if let Some(root) = steam_root_from(path) {
502        roots.push(root);
503    }
504
505    match err {
506        true => Err(roots),
507        false => Ok(roots)
508    }
509}
510
511// Name of the important paths
512const USER_REG: &str = "user.reg";
513const DOS_DEVICES: &str = "dosdevices";
514const REG_SHELL_FOLDERS: &str = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders";
515const REG_VOLATILE: &str = "Volatile Environment";
516
517/// The Proton Prefix for a specfic game, containing the windows like enviroment in which save
518/// files and the like are stored
519#[derive(Debug, Clone)]
520pub struct ProtonPrefix {
521    game: u32,
522    pfx: PathBuf
523}
524
525impl ProtonPrefix {
526    /// This can be used to load wineprefixes
527    /// Game_id will be set to 0
528    pub fn from_path(pfx: PathBuf) -> Option<ProtonPrefix> {
529        if pfx.is_dir() {
530            let mut dos_devices = pfx.clone();
531            dos_devices.push(DOS_DEVICES);
532
533            let mut user_reg = pfx.clone();
534            user_reg.push(USER_REG);
535            if user_reg.is_file() && dos_devices.is_dir() {
536                return Some(ProtonPrefix { game: 0, pfx });
537            }
538        }
539
540        None
541    }
542
543    /// Game ID is 0 for all generic wineprefixes
544    pub fn get_game_id(&self) -> u32 {
545        self.game
546    }
547
548    /// Returns the prefix folder,
549    /// containing the dosdevices folder, and registry files
550    pub fn get_pfx_path(&self) -> PathBuf {
551        self.pfx.clone()
552    }
553
554    /// Returns what is treated as the C drive within the prefix
555    pub fn get_c_drive(&self) -> PathBuf {
556        self.parse_windows_path("C:\\")
557    }
558
559    /// Returns the home folder for the user within the prefix,
560    /// usually `C:\Users\username`
561    pub fn home_dir(&self) -> Option<PathBuf> {
562        self.get_path_from_registry(REG_VOLATILE, "USERPROFILE")
563    }
564
565    /// Returns the AppData\Roaming folder within the Home folder within the prefix
566    pub fn appdata_roaming(&self) -> Option<PathBuf> {
567        self.get_path_from_registry(REG_SHELL_FOLDERS, "AppData")
568    }
569
570    /// Returns the AppData\Local folder within the Home folder within the prefix
571    pub fn appdata_local(&self) -> Option<PathBuf> {
572        self.get_path_from_registry(REG_SHELL_FOLDERS, "Local AppData")
573    }
574
575    /// Returns the AppData\LocalLow folder within the Home folder within the prefix
576    pub fn appdata_local_low(&self) -> Option<PathBuf> {
577        // Yeah, this is the key of hell... but if it works...
578        self.get_path_from_registry(REG_SHELL_FOLDERS, "{A520A1A4-1780-4FF6-BD18-167343C5AF16}")
579    }
580
581    /// Returns the Music folder within the Home folder within the prefix
582    pub fn music_dir(&self) -> Option<PathBuf> {
583        self.get_path_from_registry(REG_SHELL_FOLDERS, "My Music")
584    }
585
586    /// Returns the Videos folder within the Home folder within the prefix
587    pub fn videos_dir(&self) -> Option<PathBuf> {
588        self.get_path_from_registry(REG_SHELL_FOLDERS, "My Videos")
589    }
590
591    /// Returns the Pictures folder within the Home folder within the prefix
592    pub fn picture_dir(&self) -> Option<PathBuf> {
593        self.get_path_from_registry(REG_SHELL_FOLDERS, "My Pictures")
594    }
595
596    /// Returns the Documents folder within the Home folder within the prefix
597    pub fn documents_dir(&self) -> Option<PathBuf> {
598        self.get_path_from_registry(REG_SHELL_FOLDERS, "Personal")
599    }
600
601    /// Returns the Downloads folder within the Home folder within the prefix
602    pub fn downloads_dir(&self) -> Option<PathBuf> {
603        self.get_path_from_registry(REG_SHELL_FOLDERS, "{374DE290-123F-4565-9164-39C4925E467B}")
604    }
605
606    /// Returns the Desktop folder within the Home folder within the prefix
607    pub fn desktop_dir(&self) -> Option<PathBuf> {
608        self.get_path_from_registry(REG_SHELL_FOLDERS, "Desktop")
609    }
610
611    fn get_path_from_registry(&self, key: &str, sub_key: &str) -> Option<PathBuf> {
612        let mut user_reg = self.pfx.clone();
613        user_reg.push(USER_REG);
614        if let Some(user_reg) = RegParser::new(user_reg) {
615            if let Some(val) = user_reg.open_key(key) {
616                if let Some(path) = val.get(sub_key) {
617                    return self.parse_windows_path(path).canonicalize().ok();
618                }
619            }
620        }
621
622        None
623    }
624
625    /// Returns the public user folder within the prefix
626    pub fn public_user_dir(&self) -> Option<PathBuf> {
627        let mut user_reg = self.pfx.clone();
628        user_reg.push("system.reg");
629        if let Some(user_reg) = RegParser::new(user_reg) {
630            if let Some(val) = user_reg.open_key("Software\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList") {
631                if let Some(path) = val.get("Public") {
632                    return self.parse_windows_path(path).canonicalize().ok();
633                }
634            }
635        }
636
637        None
638    }
639
640
641    /// Turns a string with a absolute windows formated path
642    /// into the complete path within this prefix
643    pub fn parse_windows_path(&self, str: &str) -> PathBuf {
644        if str.is_empty() {
645            return self.get_pfx_path();
646        }
647
648        let res = str.replace("\\\\", "/"); // paths in reg are written with two \\
649        let mut res = res.replace("\\", "/"); // but if someone needs a regualr path converted, this deals with it
650        
651        let (letter,_) = res.split_at_mut(1);
652        letter.make_ascii_lowercase(); // the dirve names in the prefix are lowercase
653
654        let mut path = self.get_pfx_path();
655        path.push(DOS_DEVICES);
656        path.push(res);
657
658        path
659    }
660}
661
662/// Returns the first prefix found for this game.
663///
664/// There is a chance there are multiple prefixes through multiple steam installs.
665/// The Result indicates if an invalid `STEAM_DIR` was set (if you set `no_tricks` you can disgard
666/// all Err, it will always return Ok). It returns Ok on unset `STEAM_DIR`
667/// Ok(Some(prefix)) indicates the prefix for the game was found, but not necessarily within the
668/// set `STEAM_DIR`.
669///
670/// The search order is:
671/// - $STEAM_DIR (skipped if `no_tricks`)
672/// - ~/.steam/steam/
673/// - ~/.local/share/steam/
674/// - ~/.var/app/com.valvesoftware.Steam/data/Steam/
675pub fn find_prefix(game_id: u32) -> Result<Option<ProtonPrefix>, Option<ProtonPrefix>> {
676    let (roots, err) = match find_all_steam_roots() {
677        Err(res) => (res, true),
678        Ok(res) => (res, false)
679    };
680
681    for root in roots {
682        if let Some(pref) = root.get_prefix(game_id) {
683            return match err {
684                true => Err(Some(pref)),
685                false => Ok(Some(pref))
686            };
687        }
688    }
689
690    match err {
691        true => Err(None),
692        false => Ok(None)
693    }
694}
695
696/// Returns all prefixes found for this game.
697///
698/// There is a chance there are multiple prefixes through multiple steam installs.
699/// The Result indicates if an invalid `STEAM_DIR` was set (if you set `no_tricks` you can disgard
700/// all Err, it will always return Ok). It returns Ok on unset `STEAM_DIR`
701///
702/// The search order is:
703/// - $STEAM_DIR (skipped if `no_tricks`)
704/// - ~/.steam/steam/
705/// - ~/.local/share/steam/
706/// - ~/.var/app/com.valvesoftware.Steam/data/Steam/
707pub fn find_all_prefixes(game_id: u32) -> Result<Vec<ProtonPrefix>, Vec<ProtonPrefix>> {
708    let mut prefixes = Vec::<ProtonPrefix>::with_capacity(4);
709    let (roots, err) = match find_all_steam_roots() {
710        Err(res) => (res, true),
711        Ok(res) => (res, false)
712    };
713
714    for root in roots {
715        if let Some(pref) = root.get_prefix(game_id) {
716            prefixes.push(pref);
717        }
718    }
719
720    match err {
721        true => Err(prefixes),
722        false => Ok(prefixes)
723    }
724}
725
726/// Acts as a wrapper for reading registry entries
727#[derive(Debug)]
728pub struct RegParser {
729    reg: File
730}
731
732impl RegParser {
733    /// Creates a wrapper around a .reg Registry file
734    pub fn new(reg_file: PathBuf) -> Option<RegParser> {
735        if let Ok(file) = File::open(reg_file) {
736            Some(RegParser { reg: file })
737        } else {
738            None
739        }
740    }
741    
742    /// Tries to open a given key.
743    /// This key has to be formated in a windows path format
744    pub fn open_key(&self, key_path: &str) -> Option<HashMap<String, String>> {
745
746        // Serves to read the section header, or determine if there is one at all
747        fn read_line_section<'a>(trimed: &'a str) -> Option<&'a str> {
748            if let Some(text) = trimed.strip_prefix('[') {
749                // Likely found a section header
750                if let Some((path,_)) = text.split_once(']') {
751                    // split_once insures the character exists, and it doesn't matter if there is a
752                    // postfix or not, we are anyway not interested in it
753
754                    return Some(path);
755                }
756                
757            }
758
759            None
760        }
761
762        // Serves to read the sub key and write it into the map
763        fn read_sub_key(trimed: &str, map: &mut HashMap<String, String>) -> bool {
764            if let Some(part) = trimed.strip_prefix('"') {
765                if let Some((key, part)) = part.split_once('"') {
766
767                    // This will still not capture dwords, but whatever
768                    if let Some(val_part) = part.strip_prefix('=') {
769                        if let Some((_,val_untrimmed)) = val_part.split_once('"') {
770                            if let Some((val,_)) = val_untrimmed.split_once('"') {
771                                map.insert(key.to_string(), val.to_string());
772                                return true;
773                            }
774                        }
775                    }
776
777                    // In case we didn't find any other value we insert the valid key
778                    map.insert(key.to_string(), String::new());
779                    return true;
780                }
781            }
782
783            false
784        }
785
786        // The reg files format paths with \\, in programming \\ becomes one backslash, so \\ is \\\\
787        // But since someone could pass a corrected or a standard windows style path, we normalize
788        // We do two replace to not accidentally produce  backslashes
789        let key_path = key_path.replace("\\\\", "\\").replace("\\", "\\\\");
790
791        let mut reader = BufReader::new(self.reg.try_clone().ok()?);
792        let mut output = None;
793
794        let mut line = String::new();
795        while let Ok(length) = reader.read_line(&mut line) {
796            // read_line does not Err on EOF, it will instead return Ok(0), which we catch
797            if length == 0 {
798                break;
799            }
800
801            let trimed = line.trim();
802            
803            if let Some(path) = read_line_section(trimed) {
804                if output.is_none() && path == key_path {
805                    // We found our key
806                    output = Some(HashMap::new());
807                } else if output.is_some() {
808                    break;
809                }
810                
811            }
812
813            if let Some(map) = output.as_mut() {
814                read_sub_key(trimed, map);
815            }
816            
817            line.clear();
818        }
819
820        output
821    }
822}