Skip to main content

nsis_plugin/
stack.rs

1//! The installer's argument stack.
2//!
3//! Every allocation here is sized from the `string_size` the exehead handed us
4//! at call time, never from a constant. That is what makes long-string builds
5//! (`/DNSIS_MAX_STRLEN=8192`) work structurally rather than by luck.
6
7#![allow(
8	clippy::undocumented_unsafe_blocks,
9	reason = "every pointer here is the installer's own, established once in \
10	          `Stack::from_raw` and valid for the duration of a plug-in call; \
11	          restating it on each dereference would bury the comments that \
12	          carry real information"
13)]
14
15use alloc::string::String;
16use alloc::vec::Vec;
17
18use crate::error::{Error, Result};
19use crate::int;
20use crate::raw::StackNode;
21use crate::sys;
22use crate::tchar::{self, Tchar};
23
24/// Handle to the installer's `stack_t **`.
25///
26/// Obtained from [`Nsis::stack`](crate::Nsis::stack); not constructed directly
27/// outside of tests.
28pub struct Stack {
29	top: *mut *mut StackNode,
30	string_size: usize,
31}
32
33impl Stack {
34	/// Wraps the `stacktop` and `string_size` arguments of a plug-in export.
35	///
36	/// # Safety
37	/// `top` must be the `stacktop` pointer the installer passed, and
38	/// `string_size` its `string_size` argument. The stack must outlive the
39	/// returned value.
40	#[must_use]
41	pub unsafe fn from_raw(top: *mut *mut StackNode, string_size: usize) -> Self {
42		Self { top, string_size }
43	}
44
45	/// The calling installer's `NSIS_MAX_STRLEN`, in characters.
46	#[must_use]
47	pub fn string_size(&self) -> usize {
48		self.string_size
49	}
50
51	/// Longest string this installer can hold, excluding the NUL terminator.
52	#[must_use]
53	pub fn max_len(&self) -> usize {
54		self.string_size.saturating_sub(1)
55	}
56
57	/// Whether the stack has no entries. Also true when there is no stack.
58	#[must_use]
59	pub fn is_empty(&self) -> bool {
60		self.top.is_null() || unsafe { (*self.top).is_null() }
61	}
62
63	/// Number of entries currently on the stack.
64	///
65	/// Walks the whole list, so prefer [`is_empty`](Self::is_empty) in a loop.
66	#[must_use]
67	pub fn len(&self) -> usize {
68		if self.top.is_null() {
69			return 0;
70		}
71		let mut node = unsafe { *self.top };
72		let mut n = 0;
73		while !node.is_null() {
74			n += 1;
75			node = unsafe { (*node).next };
76		}
77		n
78	}
79
80	/// Pops the top entry and frees its node, as `popstring` does.
81	pub fn pop(&mut self) -> Result<String> {
82		self.pop_units().map(|units| tchar::decode(&units))
83	}
84
85	/// Reads the top entry without removing it.
86	pub fn peek(&self) -> Result<String> {
87		let node = self.head()?;
88		Ok(unsafe { tchar::read_bounded(self.text_of(node), self.string_size) })
89	}
90
91	/// Pops and discards the top entry.
92	pub fn discard(&mut self) -> Result<()> {
93		self.pop_units().map(|_| ())
94	}
95
96	/// Pops an integer using NSIS's own conversion.
97	///
98	/// Unparseable input is `0`, not an error — this matches `popintptr`, and
99	/// scripts rely on it. Only an *empty stack* is an error.
100	pub fn pop_int(&mut self) -> Result<isize> {
101		let units = self.pop_units()?;
102		Ok(int::str_to_ptr(int::parse_window(&units)))
103	}
104
105	/// Pops an integer, additionally accepting `2|4|8` forms, as `popint_or`.
106	pub fn pop_int_or(&mut self) -> Result<isize> {
107		let units = self.pop_units()?;
108		Ok(int::atoi_or(int::parse_window(&units)))
109	}
110
111	/// Pushes a string, bounded by the installer's `string_size`.
112	///
113	/// On [`Error::Truncated`] the clipped value has still been pushed, exactly
114	/// as `lstrcpyn` would leave it. The error exists so the caller can decide
115	/// whether the truncation matters.
116	pub fn push(&mut self, s: &str) -> Result<()> {
117		self.push_units(&tchar::encode(s))
118	}
119
120	/// Pushes an integer, formatted as `pushintptr` formats it.
121	pub fn push_int(&mut self, value: isize) -> Result<()> {
122		self.push_units(&int::format(value))
123	}
124
125	/// Pushes a boolean as the `0`/`1` NSIS scripts compare against.
126	pub fn push_bool(&mut self, value: bool) -> Result<()> {
127		self.push_int(isize::from(value))
128	}
129
130	// -- internals ----------------------------------------------------------
131
132	fn head(&self) -> Result<*mut StackNode> {
133		if self.top.is_null() {
134			return Err(Error::NoStack);
135		}
136		let node = unsafe { *self.top };
137		if node.is_null() {
138			return Err(Error::EmptyStack);
139		}
140		Ok(node)
141	}
142
143	fn text_of(&self, node: *mut StackNode) -> *mut Tchar {
144		unsafe { (&raw mut (*node).text).cast::<Tchar>() }
145	}
146
147	fn pop_units(&mut self) -> Result<Vec<Tchar>> {
148		let node = self.head()?;
149		let text = self.text_of(node);
150		let len = unsafe { tchar::strlen_bounded(text, self.string_size) };
151		let units = unsafe { core::slice::from_raw_parts(text, len) }.to_vec();
152		unsafe {
153			*self.top = (*node).next;
154			sys::free(node.cast());
155		}
156		Ok(units)
157	}
158
159	fn push_units(&mut self, units: &[Tchar]) -> Result<()> {
160		if self.top.is_null() {
161			return Err(Error::NoStack);
162		}
163		// No early return for `string_size == 0`: `pushstring` still pushes a
164		// node, empty because `lstrcpyn` with 0 writes nothing. Skipping the
165		// push would leave the script popping someone else's value.
166		let node = unsafe { sys::alloc_zeroed(StackNode::alloc_size(self.string_size)) }
167			.cast::<StackNode>();
168		if node.is_null() {
169			return Err(Error::OutOfMemory);
170		}
171
172		let fit = unsafe { tchar::write_bounded(self.text_of(node), self.string_size, units) };
173		unsafe {
174			(*node).next = *self.top;
175			*self.top = node;
176		}
177
178		if fit { Ok(()) } else { Err(Error::Truncated) }
179	}
180}
181
182#[cfg(test)]
183mod tests {
184	use alloc::string::String;
185	use alloc::vec::Vec;
186
187	use super::*;
188	use crate::testing::TestInstaller;
189
190	#[test]
191	fn round_trips_a_string() {
192		let mut inst = TestInstaller::stock();
193		inst.nsis().stack.push("hello").unwrap();
194		assert_eq!(inst.nsis().stack.pop().unwrap(), "hello");
195	}
196
197	#[test]
198	fn is_last_in_first_out() {
199		let mut inst = TestInstaller::stock();
200		let mut nsis = inst.nsis();
201		for value in ["first", "second", "third"] {
202			nsis.stack.push(value).unwrap();
203		}
204		assert_eq!(inst.stack(), ["third", "second", "first"]);
205		assert_eq!(inst.pop().unwrap(), "third");
206		assert_eq!(inst.pop().unwrap(), "second");
207		assert_eq!(inst.pop().unwrap(), "first");
208		assert!(inst.is_empty());
209	}
210
211	#[test]
212	fn popping_an_empty_stack_is_an_error() {
213		let mut inst = TestInstaller::stock();
214		assert_eq!(inst.nsis().stack.pop().unwrap_err(), Error::EmptyStack);
215	}
216
217	#[test]
218	fn a_null_stack_is_not_a_crash() {
219		let mut stack = unsafe { Stack::from_raw(core::ptr::null_mut(), 1024) };
220		assert!(stack.is_empty());
221		assert_eq!(stack.len(), 0);
222		assert_eq!(stack.pop().unwrap_err(), Error::NoStack);
223		assert_eq!(stack.push("x").unwrap_err(), Error::NoStack);
224	}
225
226	/// `lstrcpyn(dst, src, string_size)` copies `string_size - 1` characters
227	/// plus a NUL, so that is exactly what fits.
228	#[test]
229	fn the_boundary_is_string_size_minus_one() {
230		for size in [64, 1024, 8192] {
231			let mut inst = TestInstaller::new(size);
232
233			let exact: String = core::iter::repeat_n('x', size - 1).collect();
234			inst.nsis().stack.push(&exact).unwrap();
235			assert_eq!(inst.pop().unwrap(), exact);
236
237			let one_too_many: String = core::iter::repeat_n('x', size).collect();
238			assert_eq!(
239				inst.nsis().stack.push(&one_too_many).unwrap_err(),
240				Error::Truncated
241			);
242			// The clipped value is still pushed, exactly as `lstrcpyn` leaves it.
243			assert_eq!(inst.pop().unwrap().len(), size - 1);
244		}
245	}
246
247	/// The whole point of the crate: buffers come from the installer's runtime
248	/// `string_size`, so the same code is correct against a long-string build.
249	#[test]
250	fn a_long_string_build_holds_long_strings() {
251		let mut inst = TestInstaller::long_string();
252		let long: String = core::iter::repeat_n('z', 8191).collect();
253		inst.nsis().stack.push(&long).unwrap();
254		assert_eq!(inst.pop().unwrap(), long);
255
256		// The same value against a stock installer must be reported as
257		// truncated rather than written past the node.
258		let mut stock = TestInstaller::stock();
259		assert_eq!(
260			stock.nsis().stack.push(&long).unwrap_err(),
261			Error::Truncated
262		);
263		assert_eq!(stock.pop().unwrap().len(), 1023);
264	}
265
266	/// `pushstring` with `g_stringsize == 0` still pushes an (empty) node.
267	#[test]
268	fn a_zero_string_size_still_pushes_an_empty_entry() {
269		let mut inst = TestInstaller::new(0);
270		assert_eq!(inst.nsis().stack.push("x").unwrap_err(), Error::Truncated);
271		assert_eq!(inst.stack(), [""]);
272	}
273
274	#[test]
275	fn pops_integers_with_nsis_semantics() {
276		let mut inst = TestInstaller::stock();
277		inst.push("0x10");
278		assert_eq!(inst.nsis().stack.pop_int().unwrap(), 16);
279
280		inst.push("2|4|8");
281		assert_eq!(inst.nsis().stack.pop_int_or().unwrap(), 14);
282
283		// Unparseable is zero, not an error — only an empty stack is an error.
284		inst.push("not a number");
285		assert_eq!(inst.nsis().stack.pop_int().unwrap(), 0);
286	}
287
288	#[test]
289	fn pushes_integers_as_scripts_expect() {
290		let mut inst = TestInstaller::stock();
291		inst.nsis().stack.push_int(-42).unwrap();
292		assert_eq!(inst.pop().unwrap(), "-42");
293		inst.nsis().stack.push_bool(true).unwrap();
294		assert_eq!(inst.pop().unwrap(), "1");
295	}
296
297	#[test]
298	fn peek_does_not_consume() {
299		let mut inst = TestInstaller::stock();
300		inst.push("kept");
301		assert_eq!(inst.nsis().stack.peek().unwrap(), "kept");
302		assert_eq!(inst.nsis().stack.peek().unwrap(), "kept");
303		assert_eq!(inst.nsis().stack.len(), 1);
304	}
305
306	/// Unicode builds carry anything; ANSI builds are limited to whatever the
307	/// active code page can represent, so the two cases differ.
308	#[test]
309	fn survives_non_ascii() {
310		let mut inst = TestInstaller::stock();
311
312		#[cfg(feature = "unicode")]
313		let values = ["grüße", "日本語", "🦀"];
314		#[cfg(all(feature = "ansi", not(feature = "unicode")))]
315		let values = ["grüße", "façade"];
316
317		for value in values {
318			inst.nsis().stack.push(value).unwrap();
319			assert_eq!(inst.pop().unwrap(), value);
320		}
321	}
322
323	#[test]
324	fn many_entries_do_not_leak_into_each_other() {
325		let mut inst = TestInstaller::new(32);
326		let values: Vec<String> = (0..50).map(|i| alloc::format!("value-{i}")).collect();
327		{
328			let mut nsis = inst.nsis();
329			for value in &values {
330				nsis.stack.push(value).unwrap();
331			}
332		}
333		for value in values.iter().rev() {
334			assert_eq!(&inst.pop().unwrap(), value);
335		}
336		assert!(inst.is_empty());
337	}
338}