use std::collections::BTreeMap;
use std::fs;
use std::io::{Cursor, Read as _};
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
const CREDENTIALS: &[u8] = b"[default]\naws_access_key_id = test\n";
fn tmp(name: &str) -> PathBuf {
let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join(name);
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
fn write_fixture(dir: &Path, credential_keys: &str) -> PathBuf {
fs::write(dir.join("hello.exe"), b"MZ dummy payload").unwrap();
let config = format!(
r#"
[package]
name = "hello"
display-name = "Embala Hello"
version = "0.1.0"
identifier = "ca.hrndz.embala.hello"
publisher = "Carlos Hernandez"
description = "optional files fixture"
[pkg]
install-location = "/usr/local"
files = [
{{ src = "hello.exe", dest = "bin/hello" }},
{{ src = "secrets/credentials", dest = "credentials", mode = 0o600{credential_keys} }},
]
[setup]
arch = "x86_64"
main-executable = "hello.exe"
files = [
{{ src = "hello.exe", dest = "hello.exe" }},
{{ src = "secrets/credentials", dest = "credentials"{credential_keys} }},
]
"#
);
let path = dir.join("embala.toml");
fs::write(&path, config).unwrap();
path
}
fn stage_credentials(dir: &Path) {
fs::create_dir_all(dir.join("secrets")).unwrap();
fs::write(dir.join("secrets/credentials"), CREDENTIALS).unwrap();
}
fn build(config: &Path, formats: &str) -> Output {
let out = config.parent().unwrap().join("dist");
Command::new(env!("CARGO_BIN_EXE_embala"))
.args(["build", "--config"])
.arg(config)
.args(["--formats", formats, "--out-dir"])
.arg(out)
.output()
.expect("run embala")
}
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
fn pkg_payload(dir: &Path) -> BTreeMap<String, (u64, Vec<u8>)> {
let archive = fs::read(dir.join("dist/hello-0.1.0.pkg")).unwrap();
let gz = apple_xar::reader::XarReader::new(Cursor::new(archive))
.unwrap()
.get_file_data_from_path("hello.pkg/Payload")
.unwrap()
.expect("component Payload");
let mut cpio = Vec::new();
flate2::read::GzDecoder::new(&gz[..])
.read_to_end(&mut cpio)
.unwrap();
let mut entries = BTreeMap::new();
let mut rest = &cpio[..];
loop {
let field = |lo: usize, hi: usize| {
u64::from_str_radix(std::str::from_utf8(&rest[lo..hi]).unwrap(), 8).unwrap()
};
let (mode, name_size, file_size) = (field(18, 24), field(59, 65), field(65, 76));
let (name_size, file_size) = (name_size as usize, file_size as usize);
let name = String::from_utf8(rest[76..76 + name_size - 1].to_vec()).unwrap();
if name == "TRAILER!!!" {
return entries;
}
let data = rest[76 + name_size..76 + name_size + file_size].to_vec();
entries.insert(name, (mode, data));
rest = &rest[76 + name_size + file_size..];
}
}
fn setup_payload(dir: &Path) -> (BTreeMap<String, Vec<u8>>, String) {
let bytes = fs::read(dir.join("dist/hello-0.1.0-x86_64-setup.exe")).unwrap();
let trailer = embala_setup_overlay::Trailer::parse(
&bytes[bytes.len() - embala_setup_overlay::TRAILER_LEN..],
)
.unwrap();
let section = |s: embala_setup_overlay::Section| {
bytes[s.offset as usize..(s.offset + s.len) as usize].to_vec()
};
let mut zip = zip::ZipArchive::new(Cursor::new(section(trailer.payload_zip))).unwrap();
let mut files = BTreeMap::new();
for i in 0..zip.len() {
let mut entry = zip.by_index(i).unwrap();
let mut data = Vec::new();
entry.read_to_end(&mut data).unwrap();
files.insert(entry.name().to_string(), data);
}
let install_lua = String::from_utf8(section(trailer.install_lua)).unwrap();
(files, install_lua)
}
#[test]
fn absent_optional_files_are_left_out_of_every_payload() {
let dir = tmp("optional-absent");
let config = write_fixture(&dir, ", optional = true");
let result = build(&config, "pkg,setup");
assert!(result.status.success(), "build failed: {}", stderr(&result));
let err = stderr(&result);
let src = dir.join("secrets/credentials");
for section in ["pkg", "setup"] {
let note = format!("{section}: skipped optional {} (not found)", src.display());
assert!(err.contains(¬e), "missing {note:?} in: {err}");
}
let pkg = pkg_payload(&dir);
assert_eq!(
pkg.get("./bin/hello").map(|e| e.0),
Some(0o100644),
"{pkg:?}"
);
assert!(!pkg.keys().any(|n| n.contains("credentials")), "{pkg:?}");
let (files, install_lua) = setup_payload(&dir);
assert_eq!(files.keys().collect::<Vec<_>>(), ["hello.exe"]);
assert!(!install_lua.contains("credentials"), "{install_lua}");
}
#[test]
fn present_optional_files_ship_like_required_ones() {
let dir = tmp("optional-present");
let config = write_fixture(&dir, ", optional = true");
stage_credentials(&dir);
let result = build(&config, "pkg,setup");
assert!(result.status.success(), "build failed: {}", stderr(&result));
assert!(!stderr(&result).contains("skipped"), "{}", stderr(&result));
let pkg = pkg_payload(&dir);
assert_eq!(
pkg.get("./credentials"),
Some(&(0o100600, CREDENTIALS.to_vec())),
"credentials must ship unchanged with its 0o600 mode: {pkg:?}"
);
let (files, install_lua) = setup_payload(&dir);
assert_eq!(
files.get("credentials").map(Vec::as_slice),
Some(CREDENTIALS)
);
assert!(install_lua.contains("embala.fs.copy(\"credentials\""));
}
#[test]
fn missing_required_files_still_fail() {
let dir = tmp("optional-required");
let config = write_fixture(&dir, "");
for (format, needle) in [("pkg", None), ("setup", Some("credentials does not exist"))] {
let result = build(&config, format);
let err = stderr(&result);
assert!(!result.status.success(), "{format} must fail");
if let Some(needle) = needle {
assert!(err.contains(needle), "{err}");
}
assert!(!err.contains("skipped"), "{err}");
}
assert!(!dir.join("dist/hello-0.1.0.pkg").exists());
}
#[cfg(unix)]
#[test]
fn optional_dangling_symlink_fails_the_build() {
let dir = tmp("optional-dangling");
let config = write_fixture(&dir, ", optional = true");
fs::create_dir_all(dir.join("secrets")).unwrap();
std::os::unix::fs::symlink(dir.join("nowhere"), dir.join("secrets/credentials")).unwrap();
let result = build(&config, "pkg");
assert!(!result.status.success());
assert!(
stderr(&result).contains("dangling symlink"),
"{}",
stderr(&result)
);
assert!(!dir.join("dist/hello-0.1.0.pkg").exists());
}
#[test]
fn skipped_file_named_by_main_executable_fails_before_writing() {
let dir = tmp("optional-main-executable");
let config = dir.join("embala.toml");
fs::write(
&config,
r#"
[package]
name = "hello"
display-name = "Embala Hello"
version = "0.1.0"
identifier = "ca.hrndz.embala.hello"
publisher = "Carlos Hernandez"
description = "optional files fixture"
[msi]
arch = "x86_64"
main-executable = "agent.exe"
files = [{ src = "agent.exe", dest = "agent.exe", optional = true }]
"#,
)
.unwrap();
let result = build(&config, "msi");
assert!(!result.status.success());
let err = stderr(&result);
assert!(
err.contains("msi: main-executable \"agent.exe\" is an optional file that was absent"),
"{err}"
);
assert!(!dir.join("dist").exists(), "nothing may be written");
}
#[test]
fn dump_stub_tolerates_absent_optional_files() {
let dir = tmp("optional-dump-stub");
let config = write_fixture(&dir, ", optional = true");
let result = Command::new(env!("CARGO_BIN_EXE_embala"))
.args(["dump-stub", "--config"])
.arg(&config)
.arg("--out")
.arg(dir.join("stub.exe"))
.output()
.unwrap();
assert!(result.status.success(), "{}", stderr(&result));
}