Skip to main content

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}