diff options
Diffstat (limited to 'mingling_cli/src/bin/wrapper.rs')
| -rw-r--r-- | mingling_cli/src/bin/wrapper.rs | 115 |
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, ¤t_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())) +} |
