Skip to main content

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}