fn cache_dir() -> std::path::PathBuf {
dirs::cache_dir()
.expect("Failed to get cache directory")
.join("electron-hook")
}
fn asar_cache_dir() -> std::path::PathBuf {
ensure_dir(cache_dir().join("asar"))
}
pub fn asar_cache_path(asar_id: &str) -> std::path::PathBuf {
asar_cache_dir().join(format!("{asar_id}.asar"))
}
fn mod_artifacts_dir() -> std::path::PathBuf {
ensure_dir(cache_dir().join("mods"))
}
pub fn mod_artifact_dir(mod_name: &str) -> std::path::PathBuf {
mod_artifacts_dir().join(mod_name)
}
fn data_dir() -> std::path::PathBuf {
dirs::data_dir()
.expect("Failed to get data directory")
.join("electron-hook")
}
fn data_profiles_dir() -> std::path::PathBuf {
ensure_dir(data_dir().join("profiles"))
}
pub fn data_profile_dir(profile_id: &str) -> std::path::PathBuf {
data_profiles_dir().join(profile_id)
}
pub fn ensure_dir(path: std::path::PathBuf) -> std::path::PathBuf {
if !path.exists() {
std::fs::create_dir_all(&path)
.map_err(|e| format!("Failed to create directory: {e}"))
.unwrap();
}
path
}
pub(crate) fn redirect_asar_path(
path: &str,
custom_asar: &str,
app_folder: Option<&str>,
) -> Option<String> {
for sep in ['/', '\\'] {
let original = format!("resources{sep}_app.asar");
if path.contains(&original) {
return Some(path.replace(&format!("{sep}_app.asar"), &format!("{sep}app.asar")));
}
}
for sep in ['/', '\\'] {
let needle = match app_folder {
Some(folder) => format!("{folder}{sep}resources{sep}app.asar"),
None => format!("resources{sep}app.asar"),
};
if path.contains(&needle) {
return Some(custom_asar.to_string());
}
}
None
}
pub(crate) fn should_proxy_open(flags: i32) -> bool {
flags & libc::O_CREAT == 0
}
#[cfg(test)]
mod tests {
use super::{redirect_asar_path, should_proxy_open};
#[test]
fn writes_are_never_proxied() {
assert!(!should_proxy_open(libc::O_CREAT));
assert!(!should_proxy_open(libc::O_CREAT | libc::O_EXCL | libc::O_WRONLY));
assert!(!should_proxy_open(libc::O_CREAT | libc::O_TRUNC));
}
#[test]
fn reads_are_proxied() {
assert!(should_proxy_open(libc::O_RDONLY));
assert!(should_proxy_open(libc::O_RDONLY | libc::O_CLOEXEC));
assert!(should_proxy_open(libc::O_RDWR));
}
#[test]
fn maps_original_asar_back_to_app_asar() {
assert_eq!(
redirect_asar_path("/opt/Discord/resources/_app.asar", "/tmp/mod.asar", None),
Some("/opt/Discord/resources/app.asar".to_string())
);
assert_eq!(
redirect_asar_path(
"C:\\Users\\me\\Discord\\resources\\_app.asar",
"C:\\tmp\\mod.asar",
None
),
Some("C:\\Users\\me\\Discord\\resources\\app.asar".to_string())
);
}
#[test]
fn maps_app_asar_to_custom_asar() {
assert_eq!(
redirect_asar_path("/opt/Discord/resources/app.asar", "/tmp/mod.asar", None),
Some("/tmp/mod.asar".to_string())
);
assert_eq!(
redirect_asar_path(
"C:\\Users\\me\\Discord\\app-1.2.3\\resources\\app.asar",
"C:\\tmp\\mod.asar",
Some("app-1.2.3")
),
Some("C:\\tmp\\mod.asar".to_string())
);
}
#[test]
fn windows_app_folder_restricts_the_redirect() {
assert_eq!(
redirect_asar_path(
"C:\\Users\\me\\Discord\\app-9.9.9\\resources\\app.asar",
"C:\\tmp\\mod.asar",
Some("app-1.2.3")
),
None
);
}
#[test]
fn app_folder_scopes_redirect_on_linux() {
assert_eq!(
redirect_asar_path(
"/home/u/.config/discordcanary/app-1.0.1599/resources/app.asar",
"/tmp/mod.asar",
Some("app-1.0.1599")
),
Some("/tmp/mod.asar".to_string())
);
assert_eq!(
redirect_asar_path(
"/home/u/.config/discordcanary/app-1.0.1626/resources/app.asar",
"/tmp/mod.asar",
Some("app-1.0.1599")
),
None
);
}
#[test]
fn unrelated_paths_are_untouched() {
assert_eq!(
redirect_asar_path("/opt/Discord/resources/asar", "/tmp/mod.asar", None),
None
);
assert_eq!(redirect_asar_path("/etc/hosts", "/tmp/mod.asar", None), None);
}
#[test]
fn _app_asar_takes_precedence_over_app_asar() {
assert_eq!(
redirect_asar_path(
"/opt/Discord/resources/_app.asar",
"/tmp/mod.asar",
None
),
Some("/opt/Discord/resources/app.asar".to_string())
);
}
}