Skip to main content

nsis_plugin/
int.rs

1//! NSIS integer semantics, ported line-for-line from `Contrib/ExDLL/pluginapi.c`.
2//!
3//! `str::parse` is *not* equivalent and must not be used. NSIS accepts `0x`/`0X`
4//! hex, leading-zero octal, and signed decimal; it stops at the first character
5//! it does not recognise instead of failing; it wraps on overflow; and an
6//! unparseable string is `0`, not an error. Scripts depend on all of that.
7
8use alloc::vec::Vec;
9
10use crate::tchar::Tchar;
11
12/// The `TCHAR buf[128]` in `popintptr`/`popint_or`, which `popstringn` fills
13/// with `lstrcpyn`. Anything past 127 characters is invisible to NSIS, and is
14/// invisible here too.
15const PARSE_BUFFER: usize = 128;
16
17/// The part of `units` NSIS would parse: at most `PARSE_BUFFER - 1` characters.
18pub(crate) fn parse_window(units: &[Tchar]) -> &[Tchar] {
19	&units[..units.len().min(PARSE_BUFFER - 1)]
20}
21
22/// Character at `i`, or NUL past the end — the C reads a NUL-terminated buffer.
23#[inline]
24fn at(s: &[Tchar], i: usize) -> u32 {
25	s.get(i).map_or(0, |&c| u32::from(c))
26}
27
28/// Scans one integer, returning its value and the index that stopped the scan.
29///
30/// This is `nsishelper_str_to_ptr` with the terminating position exposed, which
31/// is what `myatoi_or` needs in order to look for a `|`.
32fn scan(s: &[Tchar]) -> (isize, usize) {
33	const ZERO: u32 = b'0' as u32;
34	let mut v: isize = 0;
35
36	// 0x / 0X hexadecimal
37	if at(s, 0) == ZERO && (at(s, 1) == b'x' as u32 || at(s, 1) == b'X' as u32) {
38		let mut i = 1;
39		loop {
40			i += 1;
41			let c = at(s, i);
42			let digit = match c {
43				0x30..=0x39 => c - 0x30,
44				0x61..=0x66 => c - 0x61 + 10,
45				0x41..=0x46 => c - 0x41 + 10,
46				_ => break,
47			};
48			v = (v << 4).wrapping_add(digit as isize);
49		}
50		return (v, i);
51	}
52
53	// Leading-zero octal
54	if at(s, 0) == ZERO && (ZERO..=b'7' as u32).contains(&at(s, 1)) {
55		let mut i = 0;
56		loop {
57			i += 1;
58			let c = at(s, i);
59			if !(ZERO..=b'7' as u32).contains(&c) {
60				break;
61			}
62			v = (v << 3).wrapping_add((c - ZERO) as isize);
63		}
64		return (v, i);
65	}
66
67	// Signed decimal. The C does `if (*s == '-') sign++; else s--;` so that the
68	// following `*(++s)` lands on the first digit either way.
69	let negative = at(s, 0) == b'-' as u32;
70	let mut i = usize::from(negative);
71	loop {
72		let c = at(s, i);
73		if !(ZERO..=b'9' as u32).contains(&c) {
74			break;
75		}
76		v = v.wrapping_mul(10).wrapping_add((c - ZERO) as isize);
77		i += 1;
78	}
79	if negative {
80		v = v.wrapping_neg();
81	}
82	(v, i)
83}
84
85/// `nsishelper_str_to_ptr`: NSIS's own string-to-integer conversion.
86pub fn str_to_ptr(s: &[Tchar]) -> isize {
87	scan(s).0
88}
89
90/// `myatoi_or`: like [`str_to_ptr`], but ORs together `2|4|8` forms.
91///
92/// The C recurses; this iterates, which is the same result without a stack
93/// depth proportional to the input.
94pub fn atoi_or(s: &[Tchar]) -> isize {
95	let mut acc: isize = 0;
96	let mut rest = s;
97	loop {
98		let (v, end) = scan(rest);
99		acc |= v;
100		if at(rest, end) != b'|' as u32 {
101			return acc;
102		}
103		rest = &rest[end + 1..];
104	}
105}
106
107/// Formats an integer the way `pushintptr` does: plain signed decimal.
108pub fn format(v: isize) -> Vec<Tchar> {
109	// `isize::MIN` has no positive counterpart, so work in the unsigned domain.
110	let negative = v < 0;
111	let mut magnitude = if negative {
112		(v as usize).wrapping_neg()
113	} else {
114		v as usize
115	};
116
117	let mut digits = [0u8; 24];
118	let mut n = 0;
119	loop {
120		digits[n] = b'0' + (magnitude % 10) as u8;
121		magnitude /= 10;
122		n += 1;
123		if magnitude == 0 {
124			break;
125		}
126	}
127
128	let mut out = Vec::with_capacity(n + 1);
129	if negative {
130		out.push(b'-' as Tchar);
131	}
132	for i in (0..n).rev() {
133		out.push(digits[i] as Tchar);
134	}
135	out
136}
137
138#[cfg(test)]
139mod tests {
140	use super::*;
141	use crate::tchar::encode;
142
143	fn parse(s: &str) -> isize {
144		str_to_ptr(&encode(s))
145	}
146
147	fn parse_or(s: &str) -> isize {
148		atoi_or(&encode(s))
149	}
150
151	#[test]
152	fn decimal() {
153		assert_eq!(parse("0"), 0);
154		assert_eq!(parse("42"), 42);
155		assert_eq!(parse("-42"), -42);
156		assert_eq!(parse("9abc"), 9);
157	}
158
159	#[test]
160	fn hexadecimal() {
161		assert_eq!(parse("0x1f"), 31);
162		assert_eq!(parse("0X1F"), 31);
163		assert_eq!(parse("0xdeadg"), 0xdead);
164		// `0x` with no digits is zero, not an error.
165		assert_eq!(parse("0x"), 0);
166	}
167
168	#[test]
169	fn octal() {
170		assert_eq!(parse("0755"), 0o755);
171		assert_eq!(parse("017"), 15);
172		// `08` is not octal: the second character is out of range, so the
173		// decimal branch runs and consumes both digits.
174		assert_eq!(parse("08"), 8);
175	}
176
177	#[test]
178	fn garbage_is_zero_not_an_error() {
179		assert_eq!(parse(""), 0);
180		assert_eq!(parse("hello"), 0);
181		assert_eq!(parse("-"), 0);
182		assert_eq!(parse(" 12"), 0);
183	}
184
185	#[test]
186	fn ored_forms() {
187		assert_eq!(parse_or("2|4|8"), 14);
188		assert_eq!(parse_or("0x1|0x2"), 3);
189		assert_eq!(parse_or("5"), 5);
190		// A trailing bar contributes nothing.
191		assert_eq!(parse_or("1|"), 1);
192	}
193
194	#[test]
195	fn or_is_not_applied_by_the_plain_parser() {
196		assert_eq!(parse("2|4|8"), 2);
197	}
198
199	#[test]
200	fn round_trip() {
201		for v in [0isize, 1, -1, 12345, -12345, isize::MAX, isize::MIN] {
202			assert_eq!(str_to_ptr(&format(v)), v, "round-tripping {v}");
203		}
204	}
205}