Skip to main content

electron_hook/
paths.rs

1//! Path utilities
2
3fn cache_dir() -> std::path::PathBuf {
4    dirs::cache_dir()
5        .expect("Failed to get cache directory")
6        .join("electron-hook")
7}
8
9fn asar_cache_dir() -> std::path::PathBuf {
10    ensure_dir(cache_dir().join("asar"))
11}
12
13/// The path to a specific .asar file
14pub fn asar_cache_path(asar_id: &str) -> std::path::PathBuf {
15    asar_cache_dir().join(format!("{asar_id}.asar"))
16}
17
18fn mod_artifacts_dir() -> std::path::PathBuf {
19    ensure_dir(cache_dir().join("mods"))
20}
21
22/// The path to a specific mod artifact folder
23pub fn mod_artifact_dir(mod_name: &str) -> std::path::PathBuf {
24    mod_artifacts_dir().join(mod_name)
25}
26
27fn data_dir() -> std::path::PathBuf {
28    dirs::data_dir()
29        .expect("Failed to get data directory")
30        .join("electron-hook")
31}
32
33fn data_profiles_dir() -> std::path::PathBuf {
34    ensure_dir(data_dir().join("profiles"))
35}
36
37/// The path to a specific profile directory
38pub fn data_profile_dir(profile_id: &str) -> std::path::PathBuf {
39    data_profiles_dir().join(profile_id)
40}
41
42/// Ensure a directory exists, recursively creating it if it doesn't
43pub fn ensure_dir(path: std::path::PathBuf) -> std::path::PathBuf {
44    if !path.exists() {
45        std::fs::create_dir_all(&path)
46            .map_err(|e| format!("Failed to create directory: {e}"))
47            .unwrap();
48    }
49    path
50}
51
52pub(crate) fn redirect_asar_path(
53    path: &str,
54    custom_asar: &str,
55    app_folder: Option<&str>,
56) -> Option<String> {
57    for sep in ['/', '\\'] {
58        let original = format!("resources{sep}_app.asar");
59        if path.contains(&original) {
60            return Some(path.replace(&format!("{sep}_app.asar"), &format!("{sep}app.asar")));
61        }
62    }
63
64    for sep in ['/', '\\'] {
65        let needle = match app_folder {
66            Some(folder) => format!("{folder}{sep}resources{sep}app.asar"),
67            None => format!("resources{sep}app.asar"),
68        };
69        if path.contains(&needle) {
70            return Some(custom_asar.to_string());
71        }
72    }
73
74    None
75}
76
77/// We don't want to proxy when the updater is running.
78pub(crate) fn should_proxy_open(flags: i32) -> bool {
79    flags & libc::O_CREAT == 0
80}
81
82#[cfg(test)]
83mod tests {
84    use super::{redirect_asar_path, should_proxy_open};
85
86    #[test]
87    fn writes_are_never_proxied() {
88        assert!(!should_proxy_open(libc::O_CREAT));
89        assert!(!should_proxy_open(libc::O_CREAT | libc::O_EXCL | libc::O_WRONLY));
90        assert!(!should_proxy_open(libc::O_CREAT | libc::O_TRUNC));
91    }
92
93    #[test]
94    fn reads_are_proxied() {
95        assert!(should_proxy_open(libc::O_RDONLY));
96        assert!(should_proxy_open(libc::O_RDONLY | libc::O_CLOEXEC));
97        assert!(should_proxy_open(libc::O_RDWR));
98    }
99
100    #[test]
101    fn maps_original_asar_back_to_app_asar() {
102        assert_eq!(
103            redirect_asar_path("/opt/Discord/resources/_app.asar", "/tmp/mod.asar", None),
104            Some("/opt/Discord/resources/app.asar".to_string())
105        );
106        assert_eq!(
107            redirect_asar_path(
108                "C:\\Users\\me\\Discord\\resources\\_app.asar",
109                "C:\\tmp\\mod.asar",
110                None
111            ),
112            Some("C:\\Users\\me\\Discord\\resources\\app.asar".to_string())
113        );
114    }
115
116    #[test]
117    fn maps_app_asar_to_custom_asar() {
118        assert_eq!(
119            redirect_asar_path("/opt/Discord/resources/app.asar", "/tmp/mod.asar", None),
120            Some("/tmp/mod.asar".to_string())
121        );
122        assert_eq!(
123            redirect_asar_path(
124                "C:\\Users\\me\\Discord\\app-1.2.3\\resources\\app.asar",
125                "C:\\tmp\\mod.asar",
126                Some("app-1.2.3")
127            ),
128            Some("C:\\tmp\\mod.asar".to_string())
129        );
130    }
131
132    #[test]
133    fn windows_app_folder_restricts_the_redirect() {
134        assert_eq!(
135            redirect_asar_path(
136                "C:\\Users\\me\\Discord\\app-9.9.9\\resources\\app.asar",
137                "C:\\tmp\\mod.asar",
138                Some("app-1.2.3")
139            ),
140            None
141        );
142    }
143
144    #[test]
145    fn app_folder_scopes_redirect_on_linux() {
146        // The running app's own folder is redirected…
147        assert_eq!(
148            redirect_asar_path(
149                "/home/u/.config/discordcanary/app-1.0.1599/resources/app.asar",
150                "/tmp/mod.asar",
151                Some("app-1.0.1599")
152            ),
153            Some("/tmp/mod.asar".to_string())
154        );
155
156        // …but a new folder being written by the auto-updater is not.
157        assert_eq!(
158            redirect_asar_path(
159                "/home/u/.config/discordcanary/app-1.0.1626/resources/app.asar",
160                "/tmp/mod.asar",
161                Some("app-1.0.1599")
162            ),
163            None
164        );
165    }
166
167    #[test]
168    fn unrelated_paths_are_untouched() {
169        assert_eq!(
170            redirect_asar_path("/opt/Discord/resources/asar", "/tmp/mod.asar", None),
171            None
172        );
173        assert_eq!(redirect_asar_path("/etc/hosts", "/tmp/mod.asar", None), None);
174    }
175
176    #[test]
177    fn _app_asar_takes_precedence_over_app_asar() {
178        assert_eq!(
179            redirect_asar_path(
180                "/opt/Discord/resources/_app.asar",
181                "/tmp/mod.asar",
182                None
183            ),
184            Some("/opt/Discord/resources/app.asar".to_string())
185        );
186    }
187}