#[cfg(windows)]
use std::path::Path;
use crate::Error;
#[cfg(windows)]
use crate::Gate;
use super::section::SectionData;
pub(crate) fn read_self_payload() -> Result<Option<SectionData>, Error> {
let exe = std::env::current_exe().map_err(|source| Error::Io {
context: "resolve current_exe to read the self payload",
source,
})?;
Ok(super::section::read_self_section_bytes(&exe))
}
fn decode_verified(section: &SectionData) -> Result<Vec<u8>, Error> {
let raw = zstd::stream::decode_all(section.payload.as_slice()).map_err(|source| Error::Io {
context: "zstd-decompress the self payload",
source,
})?;
if super::section::fnv1a64(&raw) != section.content_hash {
return Err(Error::Io {
context: "verify self payload integrity",
source: std::io::Error::other("content hash mismatch: the packed payload is corrupt"),
});
}
Ok(raw)
}
fn resolve_exe_and_dir() -> Result<(std::path::PathBuf, std::path::PathBuf), Error> {
let exe = std::env::current_exe().map_err(|source| Error::Io {
context: "resolve current_exe for the runtime swap",
source,
})?;
let dir = exe
.parent()
.ok_or_else(|| Error::Io {
context: "resolve current_exe's parent directory for the runtime swap",
source: std::io::Error::other("current_exe has no parent directory"),
})?
.to_path_buf();
Ok((exe, dir))
}
#[cfg(unix)]
struct SelfReplaceLock {
_file: Option<std::fs::File>,
}
#[cfg(unix)]
impl SelfReplaceLock {
fn acquire(dir: &std::path::Path, file_name: &str) -> Self {
let path = dir.join(format!(".{file_name}.decmpfs.lock"));
let file = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(&path)
.ok();
if let Some(f) = file.as_ref() {
let _ = f.lock();
}
Self { _file: file }
}
}
#[cfg(unix)]
pub(crate) fn materialize_and_exec(argv: &[String]) -> Result<bool, Error> {
use std::os::unix::process::CommandExt;
if read_self_payload()?.is_none() {
return Ok(false);
}
let (exe, dir) = resolve_exe_and_dir()?;
let file_name = exe
.file_name()
.ok_or_else(|| Error::Io {
context: "resolve current_exe's file name for the runtime swap",
source: std::io::Error::other("current_exe has no file name"),
})?
.to_string_lossy()
.into_owned();
let _lock = SelfReplaceLock::acquire(&dir, &file_name);
let Some(section) = read_self_payload()? else {
let source = std::process::Command::new(&exe)
.args(argv.get(1..).unwrap_or(&[]))
.exec();
return Err(Error::Io {
context: "exec the already-materialized binary",
source,
});
};
let raw = decode_verified(§ion)?;
let temp = dir.join(format!(
".{file_name}.decmpfs-materializing-{}",
std::process::id()
));
std::fs::write(&temp, &raw).map_err(|source| Error::Io {
context: "write the materialized binary before compressing",
source,
})?;
let mode = std::fs::metadata(&exe)
.map_err(|source| Error::Io {
context: "read current_exe's mode to preserve it on the materialized binary",
source,
})?
.permissions();
std::fs::set_permissions(&temp, mode).map_err(|source| Error::Io {
context: "copy current_exe's mode onto the materialized binary",
source,
})?;
#[cfg(target_os = "macos")]
if super::is_macho64(&raw) {
super::inject::resign(&temp).map_err(|message| Error::Io {
context: "re-sign the materialized binary",
source: std::io::Error::other(message),
})?;
}
crate::compress_file(&temp)?;
std::fs::rename(&temp, &exe).map_err(|source| Error::Io {
context: "atomically rename the materialized binary over argv[0]",
source,
})?;
let source = std::process::Command::new(&exe)
.args(argv.get(1..).unwrap_or(&[]))
.exec();
Err(Error::Io {
context: "exec the materialized binary",
source,
})
}
#[cfg(windows)]
pub(crate) fn materialize_and_exec(argv: &[String]) -> Result<bool, Error> {
let Some(section) = read_self_payload()? else {
return Ok(false);
};
let raw = decode_verified(§ion)?;
let (exe, _) = resolve_exe_and_dir()?;
let mut pending = exe.clone().into_os_string();
pending.push(".decmpfs-pending");
let pending = Path::new(&pending).to_path_buf();
crate::compress_bytes(&pending, &raw, &Gate::any())?;
std::process::Command::new(&pending)
.args(argv.get(1..).unwrap_or(&[]))
.spawn()
.map_err(|source| Error::Io {
context: "spawn the pending materialized binary",
source,
})?;
Ok(true)
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn decode_verified_round_trips_a_valid_payload() {
let raw = b"hello from the materialized executable".to_vec();
let compressed = zstd::stream::encode_all(raw.as_slice(), 3).expect("zstd encode");
let section = SectionData {
content_hash: super::super::section::fnv1a64(&raw),
payload: compressed,
};
let got = decode_verified(§ion).expect("decodes and verifies");
assert_eq!(got, raw);
}
#[test]
fn decode_verified_rejects_a_corrupted_hash() {
let raw = b"hello from the materialized executable".to_vec();
let compressed = zstd::stream::encode_all(raw.as_slice(), 3).expect("zstd encode");
let section = SectionData {
content_hash: super::super::section::fnv1a64(&raw) ^ 1,
payload: compressed,
};
assert!(decode_verified(§ion).is_err());
}
#[test]
fn decode_verified_rejects_a_non_zstd_payload() {
let section = SectionData {
content_hash: 0,
payload: b"not zstd at all".to_vec(),
};
assert!(decode_verified(§ion).is_err());
}
#[test]
fn read_self_payload_returns_none_for_a_plain_test_binary() {
assert!(read_self_payload().expect("reads current_exe").is_none());
}
#[cfg(unix)]
#[test]
fn self_replace_lock_is_exclusive_and_releases_on_drop() {
let dir = std::env::temp_dir().join(format!("decmpfs-srlock-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let name = "toolstub";
let lock_path = dir.join(format!(".{name}.decmpfs.lock"));
{
let _guard = SelfReplaceLock::acquire(&dir, name);
let other = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(&lock_path)
.unwrap();
assert!(
other.try_lock().is_err(),
"lock is held exclusively while the guard is alive"
);
}
let other = std::fs::OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(&lock_path)
.unwrap();
assert!(
other.try_lock().is_ok(),
"lock released when the guard drops"
);
other.unlock().ok();
std::fs::remove_dir_all(&dir).ok();
}
#[cfg(unix)]
#[test]
fn self_replace_lock_falls_through_when_the_dir_is_unwritable() {
let guard = SelfReplaceLock::acquire(std::path::Path::new("/no/such/decmpfs/dir"), "x");
drop(guard);
}
}