Skip to main content

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}