1#![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
21pub const VAR_COUNT: usize = 25;
23
24#[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,
54 InstDir,
56 OutDir,
58 ExeDir,
60 Language,
62}
63
64impl Var {
65 #[must_use]
68 pub const fn index(self) -> usize {
69 self as usize
70 }
71
72 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
102pub struct Variables {
104 base: *mut Tchar,
105 string_size: usize,
106}
107
108impl Variables {
109 #[must_use]
115 pub unsafe fn from_raw(base: *mut Tchar, string_size: usize) -> Self {
116 Self { base, string_size }
117 }
118
119 #[must_use]
121 pub fn string_size(&self) -> usize {
122 self.string_size
123 }
124
125 #[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 pub fn set(&mut self, var: Var, value: &str) -> Result<()> {
143 self.set_units(var, &tchar::encode(value))
144 }
145
146 #[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 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 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 #[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}