electron_hook/asar.rs
1//! Module for creating asar archives
2//!
3//! Provides an API for creating ASAR entrypoints from a template.
4//!
5//! This module requires the `asar` feature to be enabled.
6
7fn make_package_json(wm_class: &Option<String>) -> String {
8 if let Some(wm_class) = wm_class {
9 format!(r#"{{"main": "index.js", "desktopName": "{wm_class}", "name": "{wm_class}"}}"#)
10 } else {
11 r#"{"main": "index.js"}"#.to_string()
12 }
13}
14
15/// A builder for creating ASAR archives and writing them to the filesystem.
16///
17/// # Usage
18///
19/// ```rust,ignore
20/// use electron_hook::asar::Asar;
21/// use electron_hook::paths::{mod_artifact_dir, data_profile_dir};
22///
23/// let entrypoint = mod_artifact_dir("vencord").join("patcher.js");
24/// let profile_dir = data_profile_dir("vencord");
25///
26/// let asar = Asar::new()
27/// .with_id("vencord-release")
28/// .with_template("require(process.env.MODLOADER_MOD_ENTRYPOINT);")
29/// .with_mod_entrypoint(entrypoint.to_str().unwrap())
30/// .with_profile_dir(profile_dir.to_str().unwrap()) // Optional
31/// .create();
32///
33/// // Linux: /home/CoolPerson/.cache/electron-hook/asar/vencord-release.asar
34/// // Windows: C:/Users/CoolPerson/AppData/Local/electron-hook/asar/vencord-release.asar
35/// // MacOS: TODO
36/// ```
37#[derive(Debug, Default)]
38pub struct Asar {
39 /// The unique identifier for the ASAR archive.
40 /// e.g. if this is set to `my-mod-name`, the final name will be `{id}.asar`.
41 ///
42 /// This can either be a random UUID (with the `uuid` feature) or a custom reusable ID.
43 ///
44 /// The final path will be something like:
45 ///
46 /// Linux: `/home/CoolPerson/.cache/electron-hook/asar/my-mod-name.asar`
47 ///
48 /// Windows: `C:/Users/CoolPerson/AppData/Local/electron-hook/asar/my-mod-name.asar`
49 ///
50 /// MacOS: TODO
51 pub id: String,
52
53 /// The template for the index.js that will go into the ASAR archive.
54 ///
55 /// There are multiple environment variables that can be used in the template:
56 ///
57 /// | Environment Variable | Description | Notes |
58 /// | ---------------------------------- | ----------------------------------------------- | ------------------------ |
59 /// | `MODLOADER_EXECUTABLE` | Specifies the path to the Modloader executable. | |
60 /// | `MODLOADER_MOD_ENTRYPOINT` | The path to the entrypoint of the mod. | |
61 /// | `MODLOADER_ASAR_ID` | The name of the ASAR ID selected | |
62 /// | `MODLOADER_ASAR_PATH` | The path to the ASAR file. | |
63 /// | `MODLOADER_LIBRARY_PATH` | The path to the `.dll` or `.so`. | |
64 /// | `MODLOADER_ORIGINAL_ASAR_RELATIVE` | The relative path to the original ASAR file | Is always `../_app.asar` |
65 /// | `MODLOADER_PROFILE_DIR` | The path to the custom profile directory | Optional |
66 /// | `MODLOADER_WM_CLASS` | The WM_CLASS of the Electron application. | Optional |
67 /// | `MODLOADER_FOLDER_NAME` | the app-<version> folder name | Windows only |
68 ///
69 /// For a basic implementation, you want to at least require your mod, e.g.:
70 ///
71 /// ```javascript
72 /// require(process.env.MODLOADER_MOD_ENTRYPOINT);
73 /// ```
74 pub template: String,
75
76 /// The WM_CLASS of the application that the mod is for.
77 ///
78 /// You can use this to make it show as a different application on your Linux taskbar.
79 ///
80 /// This (probably) has no effect on Windows.
81 pub wm_class: Option<String>,
82
83 /// The entrypoint for the mod. This should be the path to the main file for your mod.
84 ///
85 /// Preferably, you should get the path using [electron_hook::paths::mod_artifact_dir]
86 ///
87 /// You can use it like so:
88 ///
89 /// ```rust
90 /// use electron_hook::paths::mod_artifact_dir;
91 /// let entrypoint = mod_artifact_dir("vencord").join("patcher.js");
92 /// // Linux: /home/CoolPerson/.cache/electron-hook/mods/vencord/patcher.js
93 /// // Windows: C:/Users/CoolPerson/AppData/Local/electron-hook/mods/vencord/patcher.js
94 /// // MacOS: TODO
95 /// ```
96 pub mod_entrypoint: String,
97
98 /// An optional alternative profile for the mod.
99 ///
100 /// A profile is a unique instance of an application's data directory - meaning separate settings, cache, chromium instance, etc.
101 /// You do not need to use this for basic installs, but if you want to run multiple instances of the same client with different mods or settings, you can use this.
102 ///
103 /// Preferably, you should get the path using [electron_hook::paths::data_profile_dir]
104 ///
105 /// You can use it like so:
106 ///
107 /// ```rust
108 /// use electron_hook::paths::data_profile_dir;
109 /// let profile_dir = data_profile_dir("moonlight");
110 /// // Linux: /home/CoolPerson/.local/share/electron-hook/profiles/moonlight
111 /// // Windows: C:/Users/CoolPerson/AppData/Roaming/electron-hook/profiles/moonlight
112 /// // MacOS: TODO
113 /// ```
114 pub profile_dir: Option<String>,
115}
116
117impl Asar {
118 /// Create a new Asar builder.
119 pub fn new() -> Self {
120 Self::default()
121 }
122
123 /// Get the path to the ASAR archive.
124 pub fn get_path(&self) -> Option<std::path::PathBuf> {
125 (!self.id.is_empty()).then(|| crate::paths::asar_cache_path(&self.id))
126 }
127
128 /// Generate a random UUID for the ASAR archive to use.
129 #[cfg(feature = "uuid")]
130 pub fn with_uuid(mut self) -> Self {
131 self.id = uuid::Uuid::new_v4().to_string();
132 std::env::set_var("MODLOADER_ASAR_ID", self.id.clone());
133 self
134 }
135
136 /// Provide a reusable ID for the ASAR archive to use.
137 ///
138 /// See [Asar::id]
139 pub fn with_id(mut self, id: &str) -> Self {
140 self.id = id.to_string();
141 std::env::set_var("MODLOADER_ASAR_ID", id);
142 self
143 }
144
145 /// Provide the template for your index.js to use
146 ///
147 /// See [Asar::template]
148 pub fn with_template(mut self, template: &str) -> Self {
149 self.template = template.to_string();
150 self
151 }
152
153 /// Provide the entrypoint for your mod.
154 ///
155 /// See [Asar::mod_entrypoint]
156 pub fn with_mod_entrypoint(mut self, mod_entrypoint: &str) -> Self {
157 self.mod_entrypoint = mod_entrypoint.to_string();
158 std::env::set_var("MODLOADER_MOD_ENTRYPOINT", mod_entrypoint);
159 self
160 }
161
162 /// Provide the WM_CLASS of the application on launch.
163 ///
164 /// See [Asar::wm_class]
165 pub fn with_wm_class(mut self, wm_class: &str) -> Self {
166 self.wm_class = Some(wm_class.to_string());
167 std::env::set_var("MODLOADER_WM_CLASS", wm_class);
168 self
169 }
170
171 /// Provide the profile directory for your mod.
172 ///
173 /// See [Asar::profile_dir]
174 pub fn with_profile_dir(mut self, profile_dir: &str) -> Self {
175 self.profile_dir = Some(profile_dir.to_string());
176 std::env::set_var("MODLOADER_PROFILE_DIR", profile_dir);
177 self
178 }
179
180 /// Create the ASAR file and write it to disk, returning the path to the ASAR file.
181 ///
182 /// See [Usage](crate::asar::Asar#usage) for how the path is generated.
183 pub fn create(&self) -> Result<std::path::PathBuf, String> {
184 use crate::paths::asar_cache_path;
185
186 let asar_path = asar_cache_path(&self.id);
187
188 let mut asar = asar::AsarWriter::new();
189
190 asar.write_file("index.js", self.template.clone(), false)
191 .map_err(|e| format!("Failed to write index.js: {e}"))?;
192
193 asar.write_file("package.json", make_package_json(&self.wm_class), false)
194 .map_err(|e| format!("Failed to write package.json: {e}"))?;
195
196 let file = std::fs::File::create(&asar_path)
197 .map_err(|e| format!("Failed to create file at {}: {e}", asar_path.display()))?;
198
199 asar.finalize(file)
200 .map_err(|e| format!("Failed to write asar to disk with error: {e}"))?;
201
202 Ok(asar_path)
203 }
204}