use std::path::Path;
use crate::{Error, Gate};
mod inject;
mod replace;
mod section;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PackOutcome {
Packed { before: u64, after: u64 },
SkippedGate,
}
pub fn pack_executable(src: &Path, dest: &Path, gate: &Gate) -> Result<PackOutcome, Error> {
let stub = std::env::current_exe().map_err(|source| Error::Io {
context: "resolve current_exe for the pack stub",
source,
})?;
pack_executable_with_stub(&stub, src, dest, gate)
}
pub fn pack_executable_with_stub(
stub: &Path,
src: &Path,
dest: &Path,
gate: &Gate,
) -> Result<PackOutcome, Error> {
let src_bytes = std::fs::read(src).map_err(|source| Error::Io {
context: "read the source executable to pack",
source,
})?;
let src_len = src_bytes.len() as u64;
let dest_name = dest.to_string_lossy().replace('\\', "/");
if !gate.matches(&dest_name, src_len) {
return Ok(PackOutcome::SkippedGate);
}
let content_hash = section::fnv1a64(&src_bytes);
let zstd_payload =
zstd::stream::encode_all(src_bytes.as_slice(), 19).map_err(|source| Error::Io {
context: "zstd-compress the source executable",
source,
})?;
let stub_bytes = std::fs::read(stub).map_err(|source| Error::Io {
context: "read the stub executable",
source,
})?;
let section_body = if is_macho64(&stub_bytes) {
section::build_section_payload(content_hash, &zstd_payload)
} else {
section::build_footer(content_hash, &zstd_payload)
};
let packed = inject::inject_payload(&stub_bytes, §ion_body).map_err(|message| Error::Io {
context: "inject the packed payload into the stub",
source: std::io::Error::other(message),
})?;
std::fs::write(dest, &packed).map_err(|source| Error::Io {
context: "write the packed executable",
source,
})?;
#[cfg(unix)]
mark_executable(dest)?;
if is_macho64(&stub_bytes) {
inject::resign(dest).map_err(|message| Error::Io {
context: "re-sign the packed executable",
source: std::io::Error::other(message),
})?;
}
let after = std::fs::metadata(dest)
.map_err(|source| Error::Io {
context: "stat the packed executable's on-disk size",
source,
})?
.len();
Ok(PackOutcome::Packed {
before: src_len,
after,
})
}
pub(crate) fn is_macho64(bytes: &[u8]) -> bool {
bytes.get(0..4) == Some(&0xfeed_facfu32.to_le_bytes())
}
#[cfg(unix)]
fn mark_executable(path: &Path) -> Result<(), Error> {
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(path)
.map_err(|source| Error::Io {
context: "read the packed executable's permissions",
source,
})?
.permissions();
perms.set_mode(perms.mode() | 0o111);
std::fs::set_permissions(path, perms).map_err(|source| Error::Io {
context: "set the packed executable's execute bit",
source,
})
}
pub fn contains_payload(path: &Path) -> bool {
section::read_self_section_bytes(path).is_some()
}
pub fn self_replace_and_exec(argv: &[String]) -> Result<bool, Error> {
replace::materialize_and_exec(argv)
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
fn scratch_dir(label: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("decmpfs-pack-{label}-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create scratch dir");
dir
}
#[cfg(target_os = "macos")]
fn synthetic_macho_stub_with_linkedit(
first_section_offset: u32,
linkedit_fileoff: u64,
linkedit_body: &[u8],
) -> Vec<u8> {
const MH_MAGIC_64: u32 = 0xfeed_facf;
const LC_SEGMENT_64: u32 = 0x19;
const MACH_HEADER_64_SIZE: usize = 32;
const SEGMENT_COMMAND_64_SIZE: usize = 72;
const NEW_LC_SIZE: usize = SEGMENT_COMMAND_64_SIZE + 80;
let linkedit_lc_off = MACH_HEADER_64_SIZE + NEW_LC_SIZE;
let linkedit_len = linkedit_body.len() as u64;
let mut m = vec![0u8; linkedit_fileoff as usize + linkedit_body.len()];
m[0..4].copy_from_slice(&MH_MAGIC_64.to_le_bytes());
m[16..20].copy_from_slice(&2u32.to_le_bytes()); m[20..24].copy_from_slice(&((NEW_LC_SIZE + SEGMENT_COMMAND_64_SIZE) as u32).to_le_bytes());
let text = MACH_HEADER_64_SIZE;
m[text..text + 4].copy_from_slice(&LC_SEGMENT_64.to_le_bytes());
m[text + 4..text + 8].copy_from_slice(&(NEW_LC_SIZE as u32).to_le_bytes());
m[text + 8..text + 14].copy_from_slice(b"__TEXT");
m[text + 64..text + 68].copy_from_slice(&1u32.to_le_bytes()); let text_sect = text + SEGMENT_COMMAND_64_SIZE;
m[text_sect..text_sect + 6].copy_from_slice(b"__text");
m[text_sect + 16..text_sect + 22].copy_from_slice(b"__TEXT");
m[text_sect + 48..text_sect + 52].copy_from_slice(&first_section_offset.to_le_bytes());
m[linkedit_lc_off..linkedit_lc_off + 4].copy_from_slice(&LC_SEGMENT_64.to_le_bytes());
m[linkedit_lc_off + 4..linkedit_lc_off + 8]
.copy_from_slice(&(SEGMENT_COMMAND_64_SIZE as u32).to_le_bytes());
m[linkedit_lc_off + 8..linkedit_lc_off + 18].copy_from_slice(b"__LINKEDIT");
m[linkedit_lc_off + 24..linkedit_lc_off + 32]
.copy_from_slice(&(0x1_0000_0000u64 + linkedit_fileoff).to_le_bytes());
m[linkedit_lc_off + 40..linkedit_lc_off + 48].copy_from_slice(&linkedit_fileoff.to_le_bytes());
m[linkedit_lc_off + 48..linkedit_lc_off + 56].copy_from_slice(&linkedit_len.to_le_bytes());
m[linkedit_fileoff as usize..linkedit_fileoff as usize + linkedit_body.len()]
.copy_from_slice(linkedit_body);
m
}
#[cfg(target_os = "macos")]
#[test]
fn pack_into_a_macho_stub_round_trips_through_read_self_section_bytes() {
let dir = scratch_dir("macho");
let stub_path = dir.join("stub.bin");
let src_path = dir.join("src.bin");
let dest_path = dir.join("dest.bin");
let stub = synthetic_macho_stub_with_linkedit(512, 600, b"LINKEDIT-CONTENT");
std::fs::write(&stub_path, &stub).expect("write stub");
let src_bytes = b"the original executable's bytes, repeated a bit to give zstd something to chew on. the original executable's bytes.".to_vec();
std::fs::write(&src_path, &src_bytes).expect("write src");
let outcome = pack_executable_with_stub(&stub_path, &src_path, &dest_path, &Gate::any())
.expect("pack succeeds");
let Some(after) = (match outcome {
PackOutcome::Packed { before, after } => {
assert_eq!(before, src_bytes.len() as u64);
Some(after)
}
PackOutcome::SkippedGate => None,
}) else {
panic!("Gate::any() must never skip");
};
assert_eq!(
after,
std::fs::metadata(&dest_path).expect("stat dest").len()
);
let got = section::read_self_section_bytes(&dest_path).expect("section found");
assert_eq!(got.content_hash, section::fnv1a64(&src_bytes));
let decompressed = zstd::stream::decode_all(got.payload.as_slice()).expect("zstd decode");
assert_eq!(decompressed, src_bytes);
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(&dest_path)
.expect("stat dest")
.permissions()
.mode();
assert_ne!(mode & 0o111, 0, "packed executable must be executable");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn pack_into_an_elf_stub_appends_a_footer_that_round_trips() {
let dir = scratch_dir("elf");
let stub_path = dir.join("stub.bin");
let src_path = dir.join("src.bin");
let dest_path = dir.join("dest.bin");
let mut stub = vec![0u8; 64];
stub[0..4].copy_from_slice(b"\x7fELF");
std::fs::write(&stub_path, &stub).expect("write stub");
let src_bytes = b"another synthetic executable payload for the ELF/PE footer path".to_vec();
std::fs::write(&src_path, &src_bytes).expect("write src");
let outcome = pack_executable_with_stub(&stub_path, &src_path, &dest_path, &Gate::any())
.expect("pack succeeds");
assert_eq!(
outcome,
PackOutcome::Packed {
before: src_bytes.len() as u64,
after: (stub.len()
+ section::build_footer(0, &[]).len()
+ zstd::stream::encode_all(src_bytes.as_slice(), 19)
.expect("zstd encode")
.len()) as u64,
}
);
let dest_bytes = std::fs::read(&dest_path).expect("read dest");
let raw_footer = section::find_footer(&dest_bytes).expect("footer found");
let payload_len = raw_footer
.len()
.checked_sub(24)
.expect("footer long enough");
let payload = &raw_footer[0..payload_len];
let hash_bytes: [u8; 8] = raw_footer[payload_len..payload_len + 8]
.try_into()
.expect("hash slice is 8 bytes");
let content_hash = u64::from_le_bytes(hash_bytes);
assert_eq!(content_hash, section::fnv1a64(&src_bytes));
let decompressed = zstd::stream::decode_all(payload).expect("zstd decode");
assert_eq!(decompressed, src_bytes);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_gate_miss_skips_and_writes_nothing() {
let dir = scratch_dir("gate-miss");
let stub_path = dir.join("stub.bin"); let src_path = dir.join("src.bin");
let dest_path = dir.join("reject.bin");
std::fs::write(&src_path, b"some source bytes").expect("write src");
let gate = Gate::new(Some("*.selected"), None).expect("gate parses");
let outcome = pack_executable_with_stub(&stub_path, &src_path, &dest_path, &gate)
.expect("gate miss is not an error");
assert_eq!(outcome, PackOutcome::SkippedGate);
assert!(!dest_path.exists(), "a gate miss must write nothing");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn is_macho64_recognizes_only_the_64_bit_le_magic() {
assert!(is_macho64(&0xfeed_facfu32.to_le_bytes()));
assert!(!is_macho64(b"\x7fELF"));
assert!(!is_macho64(b"MZ"));
assert!(!is_macho64(b"sho"));
}
}