1fn 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
13pub 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
22pub 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
37pub fn data_profile_dir(profile_id: &str) -> std::path::PathBuf {
39 data_profiles_dir().join(profile_id)
40}
41
42pub 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
77pub(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 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 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}