embala_setup_overlay/lib.rs
1//! Fixed-size PE-overlay trailer (spec R5).
2//!
3//! `setup.exe` is `[stub][payload.zip][install.lua][uninstall.lua][manifest][signed_stub]`
4//! followed by (optional alignment padding and) this fixed-size trailer. The
5//! runtime locates the trailer at `overlay_end − TRAILER_LEN` — the overlay end
6//! being the raw EOF for an unsigned file but the Authenticode certificate-table
7//! offset once signed (see [`pe::overlay_end`]) — reads these bytes, validates
8//! [`MAGIC`], and then locates every other section from the offset/length table.
9//!
10//! Byte layout (little-endian, [`TRAILER_LEN`] = 108 bytes):
11//!
12//! ```text
13//! offset size field
14//! 0 8 magic = b"EMBALASU"
15//! 8 4 format version (u32) = FORMAT_VERSION
16//! 12 6×16 section table: (offset u64, len u64) for
17//! stub, payload.zip, install.lua, uninstall.lua, manifest,
18//! signed_stub (zero-length = none embedded)
19//! ```
20//!
21//! This is the single source of truth shared by both sides of the overlay: the
22//! package-time writer (`embala-setup`, which [`Trailer::to_bytes`]) and the
23//! install-time runtime (`embala-setup-runtime`, which [`Trailer::parse`]). It
24//! is its own crate — with no dependency on the stubs — so the two never form a
25//! cycle (the runtime cannot depend on `embala-setup`, which `include_bytes!`s
26//! the runtime).
27
28mod manifest;
29pub mod pe;
30pub use manifest::{Manifest, Package, UninstallOption};
31
32/// Trailer magic: "EMBALA SetUp".
33pub const MAGIC: [u8; 8] = *b"EMBALASU";
34
35/// Overlay format version. Bumped on any incompatible layout change.
36pub const FORMAT_VERSION: u32 = 2;
37
38/// Sections described by the table, in serialized order.
39pub const SECTION_COUNT: usize = 6;
40
41/// Total trailer size: magic (8) + version (4) + 6 × (offset u64 + len u64).
42pub const TRAILER_LEN: usize = 8 + 4 + SECTION_COUNT * 16;
43
44/// A byte range of one overlay section within the `setup.exe` file.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct Section {
47 /// Absolute offset from the start of the file.
48 pub offset: u64,
49 /// Section length in bytes.
50 pub len: u64,
51}
52
53/// Parsed overlay trailer. `stub.len` is the overlay start (spec R5); the
54/// uninstaller copies `[0..stub.len)` as its engine prefix (Phase 3).
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct Trailer {
57 pub version: u32,
58 pub stub: Section,
59 pub payload_zip: Section,
60 pub install_lua: Section,
61 pub uninstall_lua: Section,
62 pub manifest: Section,
63 /// Pre-signed bare uninstall stub. Zero-length = none embedded; the runtime
64 /// then falls back to prefix-copying its own (unsigned) stub.
65 pub signed_stub: Section,
66}
67
68/// Why a trailer failed to parse.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum TrailerError {
71 /// The last [`TRAILER_LEN`] bytes did not carry [`MAGIC`] — no overlay.
72 BadMagic,
73 /// Fewer than [`TRAILER_LEN`] bytes were supplied.
74 TooShort,
75 /// The magic matched but the version is one this stub cannot read.
76 UnsupportedVersion(u32),
77}
78
79impl Trailer {
80 /// Parse the trailing [`TRAILER_LEN`] bytes of a `setup.exe`.
81 ///
82 /// `bytes` must be exactly the last [`TRAILER_LEN`] bytes of the file.
83 pub fn parse(bytes: &[u8]) -> Result<Trailer, TrailerError> {
84 if bytes.len() != TRAILER_LEN {
85 return Err(TrailerError::TooShort);
86 }
87 if bytes[0..8] != MAGIC {
88 return Err(TrailerError::BadMagic);
89 }
90 let version = read_u32(&bytes[8..12]);
91 if version != FORMAT_VERSION {
92 return Err(TrailerError::UnsupportedVersion(version));
93 }
94 let mut off = 12;
95 let mut section = || {
96 let s = Section {
97 offset: read_u64(&bytes[off..off + 8]),
98 len: read_u64(&bytes[off + 8..off + 16]),
99 };
100 off += 16;
101 s
102 };
103 Ok(Trailer {
104 version,
105 stub: section(),
106 payload_zip: section(),
107 install_lua: section(),
108 uninstall_lua: section(),
109 manifest: section(),
110 signed_stub: section(),
111 })
112 }
113
114 /// Serialize to the fixed-size on-disk form. The writer appends this.
115 pub fn to_bytes(self) -> [u8; TRAILER_LEN] {
116 let mut out = [0u8; TRAILER_LEN];
117 out[0..8].copy_from_slice(&MAGIC);
118 out[8..12].copy_from_slice(&self.version.to_le_bytes());
119 let mut off = 12;
120 for s in [
121 self.stub,
122 self.payload_zip,
123 self.install_lua,
124 self.uninstall_lua,
125 self.manifest,
126 self.signed_stub,
127 ] {
128 out[off..off + 8].copy_from_slice(&s.offset.to_le_bytes());
129 out[off + 8..off + 16].copy_from_slice(&s.len.to_le_bytes());
130 off += 16;
131 }
132 out
133 }
134}
135
136fn read_u32(b: &[u8]) -> u32 {
137 u32::from_le_bytes([b[0], b[1], b[2], b[3]])
138}
139
140fn read_u64(b: &[u8]) -> u64 {
141 u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]])
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 fn sample() -> Trailer {
149 Trailer {
150 version: FORMAT_VERSION,
151 stub: Section {
152 offset: 0,
153 len: 1024,
154 },
155 payload_zip: Section {
156 offset: 1024,
157 len: 42,
158 },
159 install_lua: Section {
160 offset: 1066,
161 len: 7,
162 },
163 uninstall_lua: Section {
164 offset: 1073,
165 len: 0,
166 },
167 manifest: Section {
168 offset: 1073,
169 len: 99,
170 },
171 signed_stub: Section {
172 offset: 1172,
173 len: 512,
174 },
175 }
176 }
177
178 #[test]
179 fn trailer_len_is_fixed() {
180 assert_eq!(TRAILER_LEN, 108);
181 assert_eq!(sample().to_bytes().len(), TRAILER_LEN);
182 }
183
184 #[test]
185 fn round_trips_every_section() {
186 // Why: the runtime locates payload/scripts/manifest solely from this
187 // table; any offset/len that does not survive serialize→parse would
188 // make the self-extractor read the wrong bytes.
189 let t = sample();
190 let parsed = Trailer::parse(&t.to_bytes()).expect("valid trailer");
191 assert_eq!(parsed, t);
192 }
193
194 #[test]
195 fn absent_magic_reports_no_overlay() {
196 // Why: a bare stub (uninstall.exe / an un-appended build) must be
197 // detected as "no overlay", not misparsed into garbage sections.
198 let mut bytes = sample().to_bytes();
199 bytes[0] ^= 0xFF;
200 assert_eq!(Trailer::parse(&bytes), Err(TrailerError::BadMagic));
201 }
202
203 #[test]
204 fn wrong_length_is_rejected() {
205 assert_eq!(Trailer::parse(&[0u8; 8]), Err(TrailerError::TooShort));
206 }
207
208 #[test]
209 fn unknown_version_is_surfaced() {
210 // Why: a future overlay format must fail loudly on an old stub rather
211 // than silently misreading a changed layout.
212 let mut bytes = sample().to_bytes();
213 bytes[8..12].copy_from_slice(&99u32.to_le_bytes());
214 assert_eq!(
215 Trailer::parse(&bytes),
216 Err(TrailerError::UnsupportedVersion(99))
217 );
218 }
219}