aboutsummaryrefslogtreecommitdiff
path: root/mingling_cli/src/bin/wrapper.rs
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_cli/src/bin/wrapper.rs')
-rw-r--r--mingling_cli/src/bin/wrapper.rs115
1 files changed, 114 insertions, 1 deletions
diff --git a/mingling_cli/src/bin/wrapper.rs b/mingling_cli/src/bin/wrapper.rs
index b1aecf6..a47b926 100644
--- a/mingling_cli/src/bin/wrapper.rs
+++ b/mingling_cli/src/bin/wrapper.rs
@@ -1,9 +1,16 @@
use std::env;
use std::ffi::OsString;
-use std::path::PathBuf;
+use std::fs;
+use std::io;
+use std::path::{Component, Path, PathBuf};
use std::process::{self, Command};
+use flate2::read::GzDecoder;
+use sha2::{Digest, Sha256};
+use tar::Archive;
+
fn main() {
+ apply_update_if_present();
exec();
}
@@ -45,3 +52,109 @@ fn exec() {
}
}
}
+
+/// The update package staged by `mling update` at `{data_dir}/mingling/update.tar.gz`.
+fn update_package_path() -> Option<PathBuf> {
+ dirs::data_dir().map(|data_dir| data_dir.join("mingling").join("update.tar.gz"))
+}
+
+/// Apply a staged update, if any: unpack it over the installation directory
+/// (never replacing the running wrapper itself), then remove the staged file.
+/// This runs before forwarding to `mingling-cli`, so the new version is loaded
+/// by this very invocation.
+fn apply_update_if_present() {
+ let Some(update_path) = update_package_path() else {
+ return;
+ };
+ if !update_path.is_file() {
+ return;
+ }
+ let Ok(current_exe) = env::current_exe() else {
+ return;
+ };
+
+ // The package mirrors the install layout: the wrapper lives at
+ // `<root>/bin/mling` and the archive root maps onto `<root>`, so entries
+ // like `bin/mingling-cli` replace the files next to this wrapper.
+ let Some(exe_dir) = current_exe.parent() else {
+ return;
+ };
+ let Some(install_root) = exe_dir.parent() else {
+ return;
+ };
+
+ match unpack_update(&update_path, &current_exe, install_root) {
+ Ok(()) => {
+ // Record the applied package's checksum so `mling update` can tell
+ // that this installation is already up to date.
+ if let Some(checksum_path) = last_update_checksum_path()
+ && let Ok(checksum) = sha256_file(&update_path)
+ {
+ let _ = fs::write(checksum_path, checksum);
+ }
+ let _ = fs::remove_file(update_path);
+ }
+ Err(e) => eprintln!("mling: failed to apply update: {e}"),
+ }
+}
+
+/// Extract `update.tar.gz` into `install_root`, skipping the running wrapper.
+fn unpack_update(
+ update_path: &Path,
+ current_exe: &Path,
+ install_root: &Path,
+) -> std::io::Result<()> {
+ let file = fs::File::open(update_path)?;
+ let mut archive = Archive::new(GzDecoder::new(file));
+
+ for entry in archive.entries()? {
+ let mut entry = entry?;
+ let entry_type = entry.header().entry_type();
+ let rel_path = sanitize_relative_path(&entry.path()?);
+ if rel_path.as_os_str().is_empty() {
+ continue;
+ }
+ let dest = install_root.join(rel_path);
+ // The running executable cannot (and must not) replace itself.
+ if dest == current_exe {
+ continue;
+ }
+ if entry_type.is_dir() {
+ fs::create_dir_all(dest)?;
+ continue;
+ }
+ if !entry_type.is_file() {
+ continue;
+ }
+ if let Some(parent) = dest.parent() {
+ fs::create_dir_all(parent)?;
+ }
+ entry.unpack(dest)?;
+ }
+ Ok(())
+}
+
+/// Keep only normal path components so entries cannot escape `install_root`.
+fn sanitize_relative_path(path: &Path) -> PathBuf {
+ let mut out = PathBuf::new();
+ for component in path.components() {
+ if let Component::Normal(part) = component {
+ out.push(part);
+ }
+ }
+ out
+}
+
+/// `{data_dir}/mingling/last-update.sha256`, where the wrapper records the
+/// checksum of the update it applied.
+fn last_update_checksum_path() -> Option<PathBuf> {
+ dirs::data_dir().map(|data_dir| data_dir.join("mingling").join("last-update.sha256"))
+}
+
+/// The sha256 hex digest of a file.
+fn sha256_file(path: &Path) -> io::Result<String> {
+ let mut file = fs::File::open(path)?;
+ let mut hasher = Sha256::new();
+ io::copy(&mut file, &mut hasher)?;
+ Ok(format!("{:x}", hasher.finalize()))
+}