Skip to main content

nsis_plugin/
vars.rs

1//! The installer's 25 user variables.
2//!
3//! `variables` is a flat array of `__INST_LAST` slots, each `string_size`
4//! characters wide. The enum below mirrors the `INST_*` enum in
5//! `Contrib/ExDLL/pluginapi.h` — including its order, which puts `$0`–`$9`
6//! before `$R0`–`$R9`.
7
8#![allow(
9	clippy::undocumented_unsafe_blocks,
10	reason = "every pointer here is the installer's own, established once in \
11	          `Variables::from_raw`; `slot` is the single place bounds are \
12	          checked, and it returns `None` rather than an unchecked pointer"
13)]
14
15use alloc::string::String;
16
17use crate::error::{Error, Result};
18use crate::int;
19use crate::tchar::{self, Tchar};
20
21/// Number of user variables, i.e. `__INST_LAST`.
22pub const VAR_COUNT: usize = 25;
23
24/// A user variable, indexed exactly as the `INST_*` enum indexes it.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26#[repr(u32)]
27#[allow(
28	missing_docs,
29	reason = "each variant is its NSIS variable, e.g. `N0` is `$0`"
30)]
31pub enum Var {
32	N0 = 0,
33	N1,
34	N2,
35	N3,
36	N4,
37	N5,
38	N6,
39	N7,
40	N8,
41	N9,
42	R0,
43	R1,
44	R2,
45	R3,
46	R4,
47	R5,
48	R6,
49	R7,
50	R8,
51	R9,
52	/// `$CMDLINE`
53	CmdLine,
54	/// `$INSTDIR`
55	InstDir,
56	/// `$OUTDIR`
57	OutDir,
58	/// `$EXEDIR`
59	ExeDir,
60	/// `$LANGUAGE`
61	Language,
62}
63
64impl Var {
65	/// Slot index, always less than [`VAR_COUNT`] — this is the type-level
66	/// equivalent of `isvalidnsisvarindex`.
67	#[must_use]
68	pub const fn index(self) -> usize {
69		self as usize
70	}
71
72	/// All variables, in `INST_*` order.
73	pub const ALL: [Var; VAR_COUNT] = [
74		Var::N0,
75		Var::N1,
76		Var::N2,
77		Var::N3,
78		Var::N4,
79		Var::N5,
80		Var::N6,
81		Var::N7,
82		Var::N8,
83		Var::N9,
84		Var::R0,
85		Var::R1,
86		Var::R2,
87		Var::R3,
88		Var::R4,
89		Var::R5,
90		Var::R6,
91		Var::R7,
92		Var::R8,
93		Var::R9,
94		Var::CmdLine,
95		Var::InstDir,
96		Var::OutDir,
97		Var::ExeDir,
98		Var::Language,
99	];
100}
101
102/// Handle to the installer's `variables` array.
103pub struct Variables {
104	base: *mut Tchar,
105	string_size: usize,
106}
107
108impl Variables {
109	/// Wraps the `variables` and `string_size` arguments of a plug-in export.
110	///
111	/// # Safety
112	/// `base` must be the `variables` pointer the installer passed, pointing at
113	/// [`VAR_COUNT`] × `string_size` characters, and must outlive the result.
114	#[must_use]
115	pub unsafe fn from_raw(base: *mut Tchar, string_size: usize) -> Self {
116		Self { base, string_size }
117	}
118
119	/// The calling installer's `NSIS_MAX_STRLEN`, in characters.
120	#[must_use]
121	pub fn string_size(&self) -> usize {
122		self.string_size
123	}
124
125	/// Reads a variable.
126	///
127	/// Returns an empty string when the installer passed no variables array,
128	/// because there is nothing sensible to read and `$0` is empty by default.
129	#[must_use]
130	pub fn get(&self, var: Var) -> String {
131		match self.slot(var) {
132			Some(slot) => unsafe { tchar::read_bounded(slot, self.string_size) },
133			None => String::new(),
134		}
135	}
136
137	/// Writes a variable, bounded by the installer's `string_size`.
138	///
139	/// `setuservariable` uses an unbounded `lstrcpy` here, which overflows the
140	/// slot for a long enough value; this bounds the copy and reports
141	/// [`Error::Truncated`] instead.
142	pub fn set(&mut self, var: Var, value: &str) -> Result<()> {
143		self.set_units(var, &tchar::encode(value))
144	}
145
146	/// Reads a variable using NSIS's own integer conversion.
147	#[must_use]
148	pub fn get_int(&self, var: Var) -> isize {
149		match self.slot(var) {
150			Some(slot) => {
151				let len = unsafe { tchar::strlen_bounded(slot, self.string_size) };
152				let units = unsafe { core::slice::from_raw_parts(slot, len) };
153				int::str_to_ptr(int::parse_window(units))
154			}
155			None => 0,
156		}
157	}
158
159	/// Writes an integer to a variable, formatted as `pushintptr` formats it.
160	pub fn set_int(&mut self, var: Var, value: isize) -> Result<()> {
161		self.set_units(var, &int::format(value))
162	}
163
164	fn set_units(&mut self, var: Var, units: &[Tchar]) -> Result<()> {
165		let slot = self.slot(var).ok_or(Error::NoVariables)?;
166		if unsafe { tchar::write_bounded(slot, self.string_size, units) } {
167			Ok(())
168		} else {
169			Err(Error::Truncated)
170		}
171	}
172
173	fn slot(&self, var: Var) -> Option<*mut Tchar> {
174		if self.base.is_null() || self.string_size == 0 {
175			return None;
176		}
177		Some(unsafe { self.base.add(var.index() * self.string_size) })
178	}
179}
180
181#[cfg(test)]
182mod tests {
183	use alloc::string::String;
184
185	use super::*;
186	use crate::testing::TestInstaller;
187
188	#[test]
189	fn indices_match_the_inst_enum() {
190		// `$0`–`$9` come first, then `$R0`–`$R9`, then the named ones.
191		assert_eq!(Var::N0.index(), 0);
192		assert_eq!(Var::N9.index(), 9);
193		assert_eq!(Var::R0.index(), 10);
194		assert_eq!(Var::R9.index(), 19);
195		assert_eq!(Var::CmdLine.index(), 20);
196		assert_eq!(Var::Language.index(), 24);
197		assert_eq!(Var::ALL.len(), VAR_COUNT);
198	}
199
200	#[test]
201	fn round_trips_a_value() {
202		let mut inst = TestInstaller::stock();
203		inst.nsis().vars.set(Var::R0, "value").unwrap();
204		assert_eq!(inst.nsis().vars.get(Var::R0), "value");
205		assert_eq!(inst.var(Var::R0), "value");
206	}
207
208	#[test]
209	fn every_slot_is_independent() {
210		let mut inst = TestInstaller::new(16);
211		{
212			let mut nsis = inst.nsis();
213			for (i, &var) in Var::ALL.iter().enumerate() {
214				nsis.vars.set(var, &alloc::format!("v{i}")).unwrap();
215			}
216		}
217		let nsis = inst.nsis();
218		for (i, &var) in Var::ALL.iter().enumerate() {
219			assert_eq!(nsis.vars.get(var), alloc::format!("v{i}"));
220		}
221	}
222
223	/// `setuservariable` writes with an unbounded `lstrcpy`, so an over-long
224	/// value runs into the next variable's slot. This is the bug the crate
225	/// exists to make unrepresentable.
226	#[test]
227	fn an_over_long_value_does_not_reach_the_next_slot() {
228		const SIZE: usize = 16;
229		let mut inst = TestInstaller::new(SIZE);
230		inst.nsis().vars.set(Var::N1, "neighbour").unwrap();
231
232		let too_long: String = core::iter::repeat_n('x', SIZE * 4).collect();
233		assert_eq!(
234			inst.nsis().vars.set(Var::N0, &too_long).unwrap_err(),
235			Error::Truncated
236		);
237
238		assert_eq!(inst.var(Var::N0).len(), SIZE - 1);
239		assert_eq!(inst.var(Var::N1), "neighbour", "$1 was overwritten");
240	}
241
242	#[test]
243	fn the_boundary_is_string_size_minus_one() {
244		for size in [32, 1024, 8192] {
245			let mut inst = TestInstaller::new(size);
246
247			let exact: String = core::iter::repeat_n('a', size - 1).collect();
248			inst.nsis().vars.set(Var::InstDir, &exact).unwrap();
249			assert_eq!(inst.var(Var::InstDir), exact);
250
251			let one_too_many: String = core::iter::repeat_n('a', size).collect();
252			assert_eq!(
253				inst.nsis()
254					.vars
255					.set(Var::InstDir, &one_too_many)
256					.unwrap_err(),
257				Error::Truncated
258			);
259			assert_eq!(inst.var(Var::InstDir).len(), size - 1);
260		}
261	}
262
263	#[test]
264	fn integers_use_nsis_semantics() {
265		let mut inst = TestInstaller::stock();
266		inst.set_var(Var::R5, "0x20");
267		assert_eq!(inst.nsis().vars.get_int(Var::R5), 32);
268		inst.nsis().vars.set_int(Var::R6, -7).unwrap();
269		assert_eq!(inst.var(Var::R6), "-7");
270	}
271
272	#[test]
273	fn a_null_array_is_not_a_crash() {
274		let mut vars = unsafe { Variables::from_raw(core::ptr::null_mut(), 1024) };
275		assert_eq!(vars.get(Var::N0), "");
276		assert_eq!(vars.get_int(Var::N0), 0);
277		assert_eq!(vars.set(Var::N0, "x").unwrap_err(), Error::NoVariables);
278	}
279}