use std::io::{Cursor, Write as _};
use std::path::{Path, PathBuf};
mod brand;
pub use brand::Branding;
use embala_setup_overlay::{
FORMAT_VERSION, Manifest, Package, Section, TRAILER_LEN, Trailer, UninstallOption,
};
pub use embala_setup_overlay::UninstallOption as UninstallOptionSpec;
use zip::write::SimpleFileOptions;
const STUB_X86_64: &[u8] = include_bytes!("../stubs/x86_64-pc-windows-gnu/setup-stub.exe");
const STUB_AARCH64: &[u8] = include_bytes!("../stubs/aarch64-pc-windows-gnullvm/setup-stub.exe");
const FIXED_DOS_TIME: (u16, u8, u8, u8, u8, u8) = (2020, 1, 1, 0, 0, 0);
const INSTALL_LUA_PLACEHOLDER: &[u8] = b"-- embala phase-2 placeholder: extraction is native\n";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SetupArch {
X86_64,
Aarch64,
}
impl SetupArch {
pub fn as_str(self) -> &'static str {
match self {
SetupArch::X86_64 => "x86_64",
SetupArch::Aarch64 => "aarch64",
}
}
fn stub(self) -> &'static [u8] {
match self {
SetupArch::X86_64 => STUB_X86_64,
SetupArch::Aarch64 => STUB_AARCH64,
}
}
}
pub fn stub_bytes(arch: SetupArch) -> &'static [u8] {
arch.stub()
}
pub fn patched_stub_bytes(arch: SetupArch, branding: &Branding) -> Result<Vec<u8>> {
brand::patch_stub(arch.stub(), branding)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstallMode {
PerUser,
PerMachine,
UserChoice,
}
impl InstallMode {
fn as_str(self) -> &'static str {
match self {
InstallMode::PerUser => "per-user",
InstallMode::PerMachine => "per-machine",
InstallMode::UserChoice => "user-choice",
}
}
}
#[derive(Debug, Clone)]
pub struct ProductInfo {
pub name: String,
pub display_name: String,
pub version: String,
pub identifier: String,
pub publisher: String,
pub description: String,
pub homepage: Option<String>,
pub license: Option<String>,
}
#[derive(Debug, Clone)]
pub struct FileSpec {
pub src: PathBuf,
pub dest: String,
}
#[derive(Debug, Clone)]
pub struct SetupSpec {
pub arch: SetupArch,
pub install_mode: InstallMode,
pub product: ProductInfo,
pub branding: Branding,
pub uninstall_options: Vec<UninstallOption>,
pub files: Vec<FileSpec>,
pub install_lua: Option<Vec<u8>>,
pub uninstall_lua: Option<Vec<u8>>,
pub signed_stub: Option<Vec<u8>>,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("setup: no files to package")]
NoFiles,
#[error(
"setup dest {0:?} must be a relative /-separated path without empty, '..', or drive segments"
)]
InvalidDest(String),
#[error("setup: two files share the dest {0:?}")]
DuplicateDest(String),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Zip(#[from] zip::result::ZipError),
#[error("setup: branding the stub failed: {0}")]
Brand(String),
}
pub type Result<T> = std::result::Result<T, Error>;
pub fn build(spec: &SetupSpec, out: &Path) -> Result<()> {
if spec.files.is_empty() {
return Err(Error::NoFiles);
}
let mut entries: Vec<(String, Vec<u8>)> = Vec::with_capacity(spec.files.len());
let mut seen = std::collections::BTreeSet::new();
for file in &spec.files {
let dest = validate_dest(&file.dest)?;
if !seen.insert(dest.clone()) {
return Err(Error::DuplicateDest(dest));
}
entries.push((dest, std::fs::read(&file.src)?));
}
entries.sort_by(|a, b| a.0.cmp(&b.0));
let payload = build_payload_zip(&entries)?;
let stub = brand::patch_stub(spec.arch.stub(), &spec.branding)?;
let stub: &[u8] = &stub;
let install: &[u8] = spec
.install_lua
.as_deref()
.unwrap_or(INSTALL_LUA_PLACEHOLDER);
let uninstall: &[u8] = spec.uninstall_lua.as_deref().unwrap_or(b"");
let manifest = manifest_bytes(spec);
let signed_stub: &[u8] = spec.signed_stub.as_deref().unwrap_or(b"");
let stub_sec = Section {
offset: 0,
len: stub.len() as u64,
};
let payload_sec = after(&stub_sec, payload.len());
let install_sec = after(&payload_sec, install.len());
let uninstall_sec = after(&install_sec, uninstall.len());
let manifest_sec = after(&uninstall_sec, manifest.len());
let signed_stub_sec = after(&manifest_sec, signed_stub.len());
let trailer = Trailer {
version: FORMAT_VERSION,
stub: stub_sec,
payload_zip: payload_sec,
install_lua: install_sec,
uninstall_lua: uninstall_sec,
manifest: manifest_sec,
signed_stub: signed_stub_sec,
};
let sections_end = signed_stub_sec.offset + signed_stub_sec.len;
let pad = (8 - (sections_end + TRAILER_LEN as u64) % 8) % 8;
let mut bytes =
Vec::with_capacity(sections_end as usize + pad as usize + trailer.to_bytes().len());
bytes.extend_from_slice(stub);
bytes.extend_from_slice(&payload);
bytes.extend_from_slice(install);
bytes.extend_from_slice(uninstall);
bytes.extend_from_slice(&manifest);
bytes.extend_from_slice(signed_stub);
bytes.resize(bytes.len() + pad as usize, 0);
bytes.extend_from_slice(&trailer.to_bytes());
if let Some(parent) = out.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent)?;
}
}
std::fs::write(out, bytes)?;
Ok(())
}
fn after(prev: &Section, len: usize) -> Section {
Section {
offset: prev.offset + prev.len,
len: len as u64,
}
}
fn validate_dest(dest: &str) -> Result<String> {
let normalized = dest.replace('\\', "/");
if normalized.contains(':') {
return Err(Error::InvalidDest(dest.to_string()));
}
if normalized.split('/').any(|s| s.is_empty() || s == "..") {
return Err(Error::InvalidDest(dest.to_string()));
}
Ok(normalized)
}
fn build_payload_zip(entries: &[(String, Vec<u8>)]) -> Result<Vec<u8>> {
let (year, month, day, hour, minute, second) = FIXED_DOS_TIME;
let mtime = zip::DateTime::from_date_and_time(year, month, day, hour, minute, second)
.expect("fixed timestamp is a valid DOS datetime");
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.last_modified_time(mtime);
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
for (name, data) in entries {
writer.start_file(name, options)?;
writer.write_all(data)?;
}
Ok(writer.finish()?.into_inner())
}
fn manifest_bytes(spec: &SetupSpec) -> Vec<u8> {
let p = &spec.product;
Manifest {
format_version: FORMAT_VERSION,
arch: spec.arch.as_str().to_string(),
install_mode: spec.install_mode.as_str().to_string(),
package: Package {
name: p.name.clone(),
display_name: p.display_name.clone(),
version: p.version.clone(),
identifier: p.identifier.clone(),
publisher: p.publisher.clone(),
description: p.description.clone(),
homepage: p.homepage.clone(),
license: p.license.clone(),
},
uninstall_options: spec.uninstall_options.clone(),
}
.to_bytes()
}