nsis_plugin/lib.rs
1//! Write NSIS plug-ins in Rust, with all four target variants building
2//! correctly by default.
3//!
4//! ```ignore
5//! #![cfg_attr(target_os = "windows", no_std)]
6//! extern crate alloc;
7//!
8//! use nsis_plugin::{Nsis, Result, nsis_fn, nsis_plugin};
9//!
10//! nsis_plugin!();
11//!
12//! nsis_fn! {
13//! fn Add(nsis: &mut Nsis) -> Result<()> {
14//! let b = nsis.stack.pop_int()?;
15//! let a = nsis.stack.pop_int()?;
16//! nsis.stack.push_int(a + b)?;
17//! Ok(())
18//! }
19//! }
20//! ```
21//!
22//! ```nsis
23//! Push 2
24//! Push 40
25//! example::Add
26//! Pop $0 ; 42
27//! ```
28//!
29//! # What the wrapper is for
30//!
31//! A plug-in export is a single C function with five arguments and a trailing
32//! ellipsis. The two facts that make it easy to get wrong:
33//!
34//! - **`string_size` is a runtime parameter**, the calling installer's
35//! `NSIS_MAX_STRLEN`. Every buffer holding installer strings is sized from
36//! it, so long-string builds (`/DNSIS_MAX_STRLEN=8192`) work structurally
37//! rather than as a feature.
38//! - **Stack nodes are `GlobalAlloc`'d and the caller frees on pop.** Pushing a
39//! longer string than `string_size` is a heap overflow. [`Stack::push`] is
40//! bounded and reports [`Error::Truncated`].
41//!
42//! # The error flag
43//!
44//! Returning `Err` from a [`nsis_fn!`] body sets `exec_flags->exec_error`,
45//! which is what `IfErrors` reads. `?` on an empty stack therefore does the
46//! NSIS-native thing with no ceremony.
47//!
48//! # Character width
49//!
50//! [`Tchar`] is `u16` under the default `unicode` feature and `u8` under
51//! `ansi`; the two are mutually exclusive. The public API traffics in
52//! `String`/`&str` and converts at the boundary. 64-bit NSIS targets are always
53//! Unicode, so the build matrix is four combinations, not eight.
54
55#![cfg_attr(not(any(test, feature = "std")), no_std)]
56#![warn(missing_docs)]
57#![warn(clippy::undocumented_unsafe_blocks)]
58
59extern crate alloc;
60
61#[cfg(all(feature = "std", not(test)))]
62extern crate std;
63
64use alloc::string::String;
65use core::ffi::c_int;
66
67pub mod error;
68pub mod int;
69mod macros;
70pub mod raw;
71pub mod rt;
72mod stack;
73mod sys;
74mod tchar;
75mod vars;
76
77#[cfg(any(test, feature = "testing"))]
78pub mod testing;
79
80pub use crate::error::{Error, Result};
81pub use crate::raw::{
82 ExecFlags, ExtraParameters, Hmodule, Hwnd, NSISPIAPIVER_1_0, NSISPIAPIVER_CURR,
83 NSPIM_GUIUNLOAD, NSPIM_UNLOAD, NsisPluginCallback, StackNode,
84};
85pub use crate::stack::Stack;
86pub use crate::tchar::Tchar;
87pub use crate::vars::{VAR_COUNT, Var, Variables};
88
89/// Everything a plug-in export receives, in one place.
90///
91/// Constructed for you by [`nsis_fn!`]; the equivalent of `EXDLL_INIT()`.
92pub struct Nsis {
93 /// The installer's argument stack.
94 pub stack: Stack,
95 /// The installer's 25 user variables.
96 pub vars: Variables,
97 /// The installer's parent window.
98 pub hwnd: Hwnd,
99 extra: *mut ExtraParameters,
100}
101
102impl Nsis {
103 /// Builds an `Nsis` from the five arguments of a plug-in export.
104 ///
105 /// # Safety
106 /// All five arguments must be exactly what the installer passed, and the
107 /// result must not outlive the call.
108 #[must_use]
109 pub unsafe fn from_raw(
110 hwnd: Hwnd,
111 string_size: c_int,
112 variables: *mut Tchar,
113 stacktop: *mut *mut StackNode,
114 extra: *mut ExtraParameters,
115 ) -> Self {
116 let string_size = string_size.max(0) as usize;
117 Self {
118 // SAFETY: the caller guarantees these are the installer's own
119 // pointers, valid for the duration of the call.
120 stack: unsafe { Stack::from_raw(stacktop, string_size) },
121 // SAFETY: as above.
122 vars: unsafe { Variables::from_raw(variables, string_size) },
123 hwnd,
124 extra,
125 }
126 }
127
128 /// The calling installer's `NSIS_MAX_STRLEN`, in characters.
129 ///
130 /// This is a *runtime* value. Never assume 1024.
131 #[must_use]
132 pub fn string_size(&self) -> usize {
133 self.stack.string_size()
134 }
135
136 /// The raw `extra_parameters` block, if the installer supplied one.
137 #[must_use]
138 pub fn extra(&self) -> Option<&ExtraParameters> {
139 // SAFETY: `extra` is either null or the installer's own block, valid
140 // for the duration of the call.
141 unsafe { self.extra.as_ref() }
142 }
143
144 /// The installer's live [`ExecFlags`], if available.
145 #[must_use]
146 pub fn exec_flags(&self) -> Option<&ExecFlags> {
147 // SAFETY: `exec_flags` points into the installer's own state.
148 unsafe { self.extra()?.exec_flags.as_ref() }
149 }
150
151 /// The installer's live [`ExecFlags`], mutably.
152 #[must_use]
153 pub fn exec_flags_mut(&mut self) -> Option<&mut ExecFlags> {
154 // SAFETY: as above; `&mut self` keeps this exclusive on our side, and
155 // the installer is single-threaded while a plug-in call is running.
156 unsafe { self.extra.as_ref()?.exec_flags.as_mut() }
157 }
158
159 /// `exec_flags->plugin_api_version`.
160 ///
161 /// Only meaningful from NSIS 2.42 onward; older installers leave unrelated
162 /// data in this field. Compare with `>=`, never `==`.
163 #[must_use]
164 pub fn plugin_api_version(&self) -> c_int {
165 self.exec_flags().map_or(0, |f| f.plugin_api_version)
166 }
167
168 /// Fails unless the installer reports at least `required`.
169 pub fn require_api_version(&self, required: c_int) -> Result<()> {
170 let found = self.plugin_api_version();
171 if found >= required {
172 Ok(())
173 } else {
174 Err(Error::UnsupportedApiVersion { found, required })
175 }
176 }
177
178 /// Runs a code segment in the installer.
179 ///
180 /// An honest passthrough of `ExecuteCodeSegment`, with the same contract:
181 /// `position` is zero-based, so a function address obtained from
182 /// `GetFunctionAddress` needs `- 1`.
183 ///
184 /// # Safety
185 /// The installer will run arbitrary script code, which can re-enter this
186 /// plug-in. Nothing here makes that safe.
187 pub unsafe fn execute_code_segment(&mut self, position: c_int) -> Result<c_int> {
188 let f = self
189 .extra()
190 .and_then(|e| e.execute_code_segment)
191 .ok_or(Error::Unavailable("ExecuteCodeSegment"))?;
192 // SAFETY: delegated to the caller, per this function's contract.
193 Ok(unsafe { f(position, self.hwnd) })
194 }
195
196 /// Replaces characters that are invalid in a filename, via the installer's
197 /// own `validate_filename`.
198 pub fn validate_filename(&mut self, name: &str) -> Result<String> {
199 let f = self
200 .extra()
201 .and_then(|e| e.validate_filename)
202 .ok_or(Error::Unavailable("validate_filename"))?;
203
204 // `validate_filename` (`Source/exehead/util.c`) takes no length and only
205 // ever shortens the string, so a buffer the size of the input is always
206 // enough. The buffer is ours, not the installer's, so `string_size` does
207 // not apply; it bounds the result wherever the caller stores it.
208 let mut buf = tchar::encode(name);
209 buf.push(0);
210 // SAFETY: `buf` is NUL-terminated and the installer never lengthens it.
211 unsafe { f(buf.as_mut_ptr()) };
212 // SAFETY: `buf` is still `buf.len()` elements long.
213 Ok(unsafe { tchar::read_bounded(buf.as_ptr(), buf.len()) })
214 }
215
216 /// Registers an unload callback, as built by [`nsis_unload!`].
217 ///
218 /// `/NOUNLOAD` and `SetPluginsUnload` were deprecated in NSIS 3 and
219 /// plug-ins now stay loaded for the life of the installer, so this is where
220 /// cleanup belongs. Registering the same callback twice is not an error.
221 pub fn register_callback(&mut self, callback: NsisPluginCallback) -> Result<()> {
222 self.require_api_version(NSISPIAPIVER_1_0)?;
223 let f = self
224 .extra()
225 .and_then(|e| e.register_plugin_callback)
226 .ok_or(Error::Unavailable("RegisterPluginCallback"))?;
227
228 let module = rt::hinstance();
229 if module.is_null() {
230 return Err(Error::Unavailable("HINSTANCE"));
231 }
232 // SAFETY: `module` is this DLL's own handle and `callback` is a valid
233 // `extern "C"` function pointer.
234 match unsafe { f(module, callback) } {
235 // 0 is success; 1 means it was already registered, which is fine.
236 0 | 1 => Ok(()),
237 _ => Err(Error::Failed),
238 }
239 }
240}
241
242/// Generates a paired getter and setter for an `exec_flags_t` field.
243macro_rules! flag_accessors {
244 ($(
245 $(#[$attr:meta])*
246 $get:ident / $set:ident => $field:ident : bool
247 ),* $(,)?) => {
248 impl Nsis {$(
249 $(#[$attr])*
250 #[must_use]
251 pub fn $get(&self) -> bool {
252 self.exec_flags().is_some_and(|f| f.$field != 0)
253 }
254
255 $(#[$attr])*
256 pub fn $set(&mut self, value: bool) {
257 if let Some(f) = self.exec_flags_mut() {
258 f.$field = c_int::from(value);
259 }
260 }
261 )*}
262 };
263 ($(
264 $(#[$attr:meta])*
265 $get:ident / $set:ident => $field:ident : int
266 ),* $(,)?) => {
267 impl Nsis {$(
268 $(#[$attr])*
269 #[must_use]
270 pub fn $get(&self) -> c_int {
271 self.exec_flags().map_or(0, |f| f.$field)
272 }
273
274 $(#[$attr])*
275 pub fn $set(&mut self, value: c_int) {
276 if let Some(f) = self.exec_flags_mut() {
277 f.$field = value;
278 }
279 }
280 )*}
281 };
282}
283
284flag_accessors! {
285 /// The error flag `IfErrors` reads. Set automatically when a
286 /// [`nsis_fn!`] body returns `Err`.
287 error / set_error => exec_error: bool,
288 /// `IfSilent` / `SetSilent`.
289 silent / set_silent => silent: bool,
290 /// `IfAbort`.
291 abort / set_abort => abort: bool,
292 /// `IfRebootFlag` / `SetRebootFlag`.
293 reboot_flag / set_reboot_flag => exec_reboot: bool,
294 /// `SetAutoClose`.
295 autoclose / set_autoclose => autoclose: bool,
296 /// `IfRtlLanguage`: whether `$LANGUAGE` is right-to-left.
297 rtl / set_rtl => rtl: bool,
298 /// `SetShellVarContext`: false is user context, true is machine context.
299 all_users / set_all_users => all_user_var: bool,
300}
301
302flag_accessors! {
303 /// `SetErrorLevel`.
304 errlvl / set_errlvl => errlvl: int,
305 /// `SetRegView`: 0 is the default view.
306 alter_reg_view / set_alter_reg_view => alter_reg_view: int,
307 /// `SetDetailsPrint`.
308 status_update / set_status_update => status_update: int,
309 /// `GetInstDirError`.
310 instdir_error / set_instdir_error => instdir_error: int,
311}
312
313#[cfg(test)]
314mod tests {
315 use alloc::string::String;
316
317 use crate::tchar::Tchar;
318 use crate::testing::TestInstaller;
319
320 /// Stands in for the exehead's `validate_filename`: drops every `:` in
321 /// place, shortening the string as the real one does.
322 unsafe extern "system" fn strip_colons(s: *mut Tchar) {
323 let (mut read, mut write) = (s, s);
324 // SAFETY: `s` is NUL-terminated; `write` never passes `read`.
325 unsafe {
326 while *read != 0 {
327 if *read != b':' as Tchar {
328 *write = *read;
329 write = write.add(1);
330 }
331 read = read.add(1);
332 }
333 *write = 0;
334 }
335 }
336
337 /// The buffer is sized from the input, so a name longer than `string_size`
338 /// is validated in full rather than rejected.
339 #[test]
340 fn validate_filename_is_not_bounded_by_string_size() {
341 let mut inst = TestInstaller::stock();
342 inst.extra_mut().validate_filename = Some(strip_colons);
343
344 let name: String = "a:".repeat(2000);
345 let clean = inst.nsis().validate_filename(&name).unwrap();
346 assert_eq!(clean, "a".repeat(2000));
347 }
348}