1use std::io::{Cursor, Write as _};
22use std::path::{Path, PathBuf};
23
24mod brand;
25pub use brand::Branding;
26
27use embala_setup_overlay::{
28 FORMAT_VERSION, Manifest, Package, Section, TRAILER_LEN, Trailer, UninstallOption,
29};
30pub use embala_setup_overlay::UninstallOption as UninstallOptionSpec;
33use zip::write::SimpleFileOptions;
34
35const STUB_X86_64: &[u8] = include_bytes!("../stubs/x86_64-pc-windows-gnu/setup-stub.exe");
38const STUB_AARCH64: &[u8] = include_bytes!("../stubs/aarch64-pc-windows-gnullvm/setup-stub.exe");
39
40const FIXED_DOS_TIME: (u16, u8, u8, u8, u8, u8) = (2020, 1, 1, 0, 0, 0);
43
44const INSTALL_LUA_PLACEHOLDER: &[u8] = b"-- embala phase-2 placeholder: extraction is native\n";
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum SetupArch {
51 X86_64,
52 Aarch64,
53}
54
55impl SetupArch {
56 pub fn as_str(self) -> &'static str {
58 match self {
59 SetupArch::X86_64 => "x86_64",
60 SetupArch::Aarch64 => "aarch64",
61 }
62 }
63
64 fn stub(self) -> &'static [u8] {
66 match self {
67 SetupArch::X86_64 => STUB_X86_64,
68 SetupArch::Aarch64 => STUB_AARCH64,
69 }
70 }
71}
72
73pub fn stub_bytes(arch: SetupArch) -> &'static [u8] {
77 arch.stub()
78}
79
80pub fn patched_stub_bytes(arch: SetupArch, branding: &Branding) -> Result<Vec<u8>> {
83 brand::patch_stub(arch.stub(), branding)
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum InstallMode {
91 PerUser,
92 PerMachine,
93 UserChoice,
94}
95
96impl InstallMode {
97 fn as_str(self) -> &'static str {
99 match self {
100 InstallMode::PerUser => "per-user",
101 InstallMode::PerMachine => "per-machine",
102 InstallMode::UserChoice => "user-choice",
103 }
104 }
105}
106
107#[derive(Debug, Clone)]
110pub struct ProductInfo {
111 pub name: String,
112 pub display_name: String,
113 pub version: String,
114 pub identifier: String,
115 pub publisher: String,
116 pub description: String,
117 pub homepage: Option<String>,
118 pub license: Option<String>,
119}
120
121#[derive(Debug, Clone)]
124pub struct FileSpec {
125 pub src: PathBuf,
126 pub dest: String,
127}
128
129#[derive(Debug, Clone)]
132pub struct SetupSpec {
133 pub arch: SetupArch,
134 pub install_mode: InstallMode,
135 pub product: ProductInfo,
136 pub branding: Branding,
139 pub uninstall_options: Vec<UninstallOption>,
142 pub files: Vec<FileSpec>,
143 pub install_lua: Option<Vec<u8>>,
146 pub uninstall_lua: Option<Vec<u8>>,
149 pub signed_stub: Option<Vec<u8>>,
153}
154
155#[derive(Debug, thiserror::Error)]
156pub enum Error {
157 #[error("setup: no files to package")]
158 NoFiles,
159 #[error(
160 "setup dest {0:?} must be a relative /-separated path without empty, '..', or drive segments"
161 )]
162 InvalidDest(String),
163 #[error("setup: two files share the dest {0:?}")]
164 DuplicateDest(String),
165 #[error(transparent)]
166 Io(#[from] std::io::Error),
167 #[error(transparent)]
168 Zip(#[from] zip::result::ZipError),
169 #[error("setup: branding the stub failed: {0}")]
170 Brand(String),
171}
172
173pub type Result<T> = std::result::Result<T, Error>;
174
175pub fn build(spec: &SetupSpec, out: &Path) -> Result<()> {
177 if spec.files.is_empty() {
178 return Err(Error::NoFiles);
179 }
180
181 let mut entries: Vec<(String, Vec<u8>)> = Vec::with_capacity(spec.files.len());
184 let mut seen = std::collections::BTreeSet::new();
185 for file in &spec.files {
186 let dest = validate_dest(&file.dest)?;
187 if !seen.insert(dest.clone()) {
188 return Err(Error::DuplicateDest(dest));
189 }
190 entries.push((dest, std::fs::read(&file.src)?));
191 }
192 entries.sort_by(|a, b| a.0.cmp(&b.0));
193
194 let payload = build_payload_zip(&entries)?;
195 let stub = brand::patch_stub(spec.arch.stub(), &spec.branding)?;
198 let stub: &[u8] = &stub;
199 let install: &[u8] = spec
202 .install_lua
203 .as_deref()
204 .unwrap_or(INSTALL_LUA_PLACEHOLDER);
205 let uninstall: &[u8] = spec.uninstall_lua.as_deref().unwrap_or(b"");
206 let manifest = manifest_bytes(spec);
207 let signed_stub: &[u8] = spec.signed_stub.as_deref().unwrap_or(b"");
209
210 let stub_sec = Section {
213 offset: 0,
214 len: stub.len() as u64,
215 };
216 let payload_sec = after(&stub_sec, payload.len());
217 let install_sec = after(&payload_sec, install.len());
218 let uninstall_sec = after(&install_sec, uninstall.len());
219 let manifest_sec = after(&uninstall_sec, manifest.len());
220 let signed_stub_sec = after(&manifest_sec, signed_stub.len());
221 let trailer = Trailer {
222 version: FORMAT_VERSION,
223 stub: stub_sec,
224 payload_zip: payload_sec,
225 install_lua: install_sec,
226 uninstall_lua: uninstall_sec,
227 manifest: manifest_sec,
228 signed_stub: signed_stub_sec,
229 };
230
231 let sections_end = signed_stub_sec.offset + signed_stub_sec.len;
235 let pad = (8 - (sections_end + TRAILER_LEN as u64) % 8) % 8;
236
237 let mut bytes =
238 Vec::with_capacity(sections_end as usize + pad as usize + trailer.to_bytes().len());
239 bytes.extend_from_slice(stub);
240 bytes.extend_from_slice(&payload);
241 bytes.extend_from_slice(install);
242 bytes.extend_from_slice(uninstall);
243 bytes.extend_from_slice(&manifest);
244 bytes.extend_from_slice(signed_stub);
245 bytes.resize(bytes.len() + pad as usize, 0);
246 bytes.extend_from_slice(&trailer.to_bytes());
247
248 if let Some(parent) = out.parent() {
249 if !parent.as_os_str().is_empty() {
250 std::fs::create_dir_all(parent)?;
251 }
252 }
253 std::fs::write(out, bytes)?;
254 Ok(())
255}
256
257fn after(prev: &Section, len: usize) -> Section {
259 Section {
260 offset: prev.offset + prev.len,
261 len: len as u64,
262 }
263}
264
265fn validate_dest(dest: &str) -> Result<String> {
268 let normalized = dest.replace('\\', "/");
269 if normalized.contains(':') {
271 return Err(Error::InvalidDest(dest.to_string()));
272 }
273 if normalized.split('/').any(|s| s.is_empty() || s == "..") {
274 return Err(Error::InvalidDest(dest.to_string()));
275 }
276 Ok(normalized)
277}
278
279fn build_payload_zip(entries: &[(String, Vec<u8>)]) -> Result<Vec<u8>> {
281 let (year, month, day, hour, minute, second) = FIXED_DOS_TIME;
282 let mtime = zip::DateTime::from_date_and_time(year, month, day, hour, minute, second)
283 .expect("fixed timestamp is a valid DOS datetime");
284 let options = SimpleFileOptions::default()
285 .compression_method(zip::CompressionMethod::Deflated)
286 .last_modified_time(mtime);
287
288 let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
289 for (name, data) in entries {
290 writer.start_file(name, options)?;
291 writer.write_all(data)?;
292 }
293 Ok(writer.finish()?.into_inner())
294}
295
296fn manifest_bytes(spec: &SetupSpec) -> Vec<u8> {
299 let p = &spec.product;
300 Manifest {
301 format_version: FORMAT_VERSION,
302 arch: spec.arch.as_str().to_string(),
303 install_mode: spec.install_mode.as_str().to_string(),
304 package: Package {
305 name: p.name.clone(),
306 display_name: p.display_name.clone(),
307 version: p.version.clone(),
308 identifier: p.identifier.clone(),
309 publisher: p.publisher.clone(),
310 description: p.description.clone(),
311 homepage: p.homepage.clone(),
312 license: p.license.clone(),
313 },
314 uninstall_options: spec.uninstall_options.clone(),
315 }
316 .to_bytes()
317}