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
5fn 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 PathBuf::from_str(str).ok()
20 }
21}
22
23pub fn get_steam_dir_env_value() -> Option<OsString> {
27 env::var_os(ENV_STEAM_DIR)
28}
29
30pub 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#[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 pub fn get_steamapps_folder(&self) -> PathBuf {
63 self.steamapps.clone()
64 }
65
66 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 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 let buf = PathBuf::from_str(path).ok()?;
87 return SteamLibrary::from_path(&buf);
88 }
89 }
90 }
91 }
92
93 None
94 }
95
96 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 return Some(pre);
102 }
103 }
104
105 for lib in self.get_libraries() {
108 if let Some(pre) = lib.get_prefix(game_id) {
109 return Some(pre);
111 }
112 }
113
114 None
116 }
117
118 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 for (_,lib) in vdf.pairs.iter() {
126 if let VdfValue::Complex(lib) = lib {
127
128 if let Some(VdfValue::Simple(p)) = lib.pairs.get("path") {
130
131 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 res.push(SteamLibrary { steamapps: self.get_steamapps_folder(), is_root: true });
146 }
147
148 res
149 }
150
151 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 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
171pub fn parse_vdf_file(file_path: &PathBuf) -> Option<VdfStruct> {
176
177 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 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 return Some(obj);
197 }
198
199 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 let (val,_) = val_untrimmed.split_once('"')?;
208 (key.to_string(), VdfValue::Simple(val.to_string()))
209 } else {
210 let key = key.to_string();
213
214 line.clear();
215 if reader.read_line(&mut line).ok()? == 0 {
216 return None;
218 }
219
220 let trimed = line.trim();
221 if trimed != "{" {
222 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#[derive(Debug, Clone)]
250pub struct VdfStruct {
251 pub pairs: HashMap<String, VdfValue>
252}
253
254#[derive(Debug, Clone)]
258pub enum VdfValue {
259 Complex(VdfStruct),
260 Simple(String)
261}
262
263
264#[derive(Debug, Clone)]
266pub struct SteamLibrary {
267 steamapps: PathBuf,
268 is_root: bool
269}
270
271
272impl SteamLibrary {
273 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 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 pub fn get_steamapps_folder(&self) -> PathBuf {
313 self.steamapps.clone()
314 }
315
316 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 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 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 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
363pub 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
378pub fn steam_root_env() -> Result<SteamRoot, bool> {
384 steam_root_from(get_steam_dir_env_path()?).ok_or(true)
385}
386
387const 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
392pub 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 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 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
446pub fn find_all_steam_roots() -> Result<Vec<SteamRoot>, Vec<SteamRoot>> {
458 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 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 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 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
511const 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#[derive(Debug, Clone)]
520pub struct ProtonPrefix {
521 game: u32,
522 pfx: PathBuf
523}
524
525impl ProtonPrefix {
526 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 pub fn get_game_id(&self) -> u32 {
545 self.game
546 }
547
548 pub fn get_pfx_path(&self) -> PathBuf {
551 self.pfx.clone()
552 }
553
554 pub fn get_c_drive(&self) -> PathBuf {
556 self.parse_windows_path("C:\\")
557 }
558
559 pub fn home_dir(&self) -> Option<PathBuf> {
562 self.get_path_from_registry(REG_VOLATILE, "USERPROFILE")
563 }
564
565 pub fn appdata_roaming(&self) -> Option<PathBuf> {
567 self.get_path_from_registry(REG_SHELL_FOLDERS, "AppData")
568 }
569
570 pub fn appdata_local(&self) -> Option<PathBuf> {
572 self.get_path_from_registry(REG_SHELL_FOLDERS, "Local AppData")
573 }
574
575 pub fn appdata_local_low(&self) -> Option<PathBuf> {
577 self.get_path_from_registry(REG_SHELL_FOLDERS, "{A520A1A4-1780-4FF6-BD18-167343C5AF16}")
579 }
580
581 pub fn music_dir(&self) -> Option<PathBuf> {
583 self.get_path_from_registry(REG_SHELL_FOLDERS, "My Music")
584 }
585
586 pub fn videos_dir(&self) -> Option<PathBuf> {
588 self.get_path_from_registry(REG_SHELL_FOLDERS, "My Videos")
589 }
590
591 pub fn picture_dir(&self) -> Option<PathBuf> {
593 self.get_path_from_registry(REG_SHELL_FOLDERS, "My Pictures")
594 }
595
596 pub fn documents_dir(&self) -> Option<PathBuf> {
598 self.get_path_from_registry(REG_SHELL_FOLDERS, "Personal")
599 }
600
601 pub fn downloads_dir(&self) -> Option<PathBuf> {
603 self.get_path_from_registry(REG_SHELL_FOLDERS, "{374DE290-123F-4565-9164-39C4925E467B}")
604 }
605
606 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 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 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("\\\\", "/"); let mut res = res.replace("\\", "/"); let (letter,_) = res.split_at_mut(1);
652 letter.make_ascii_lowercase(); let mut path = self.get_pfx_path();
655 path.push(DOS_DEVICES);
656 path.push(res);
657
658 path
659 }
660}
661
662pub 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
696pub 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#[derive(Debug)]
728pub struct RegParser {
729 reg: File
730}
731
732impl RegParser {
733 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 pub fn open_key(&self, key_path: &str) -> Option<HashMap<String, String>> {
745
746 fn read_line_section<'a>(trimed: &'a str) -> Option<&'a str> {
748 if let Some(text) = trimed.strip_prefix('[') {
749 if let Some((path,_)) = text.split_once(']') {
751 return Some(path);
755 }
756
757 }
758
759 None
760 }
761
762 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 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 map.insert(key.to_string(), String::new());
779 return true;
780 }
781 }
782
783 false
784 }
785
786 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 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 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}