1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
use std::env;
use std::ffi::OsString;
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();
}
fn exec() {
fn target_exe_path() -> PathBuf {
let mut path = env::current_exe().expect("Fail to read current_exe");
path.pop();
let target_name = if cfg!(target_os = "windows") {
"mingling-cli.exe"
} else {
"mingling-cli"
};
path.push(target_name);
path
}
let mut args: Vec<OsString> = env::args_os().collect();
let pass_args = if args.len() > 1 {
args.split_off(1)
} else {
vec![]
};
let target = target_exe_path();
let status = Command::new(&target)
.args(&pass_args)
.status()
.unwrap_or_else(|_| {
process::exit(1);
});
match status.code() {
Some(code) => process::exit(code),
None => {
process::exit(1);
}
}
}
/// 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()))
}
|