#![allow(
clippy::undocumented_unsafe_blocks,
reason = "every pointer here is the installer's own, established once in \
`Variables::from_raw`; `slot` is the single place bounds are \
checked, and it returns `None` rather than an unchecked pointer"
)]
use alloc::string::String;
use crate::error::{Error, Result};
use crate::int;
use crate::tchar::{self, Tchar};
pub const VAR_COUNT: usize = 25;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u32)]
#[allow(
missing_docs,
reason = "each variant is its NSIS variable, e.g. `N0` is `$0`"
)]
pub enum Var {
N0 = 0,
N1,
N2,
N3,
N4,
N5,
N6,
N7,
N8,
N9,
R0,
R1,
R2,
R3,
R4,
R5,
R6,
R7,
R8,
R9,
CmdLine,
InstDir,
OutDir,
ExeDir,
Language,
}
impl Var {
#[must_use]
pub const fn index(self) -> usize {
self as usize
}
pub const ALL: [Var; VAR_COUNT] = [
Var::N0,
Var::N1,
Var::N2,
Var::N3,
Var::N4,
Var::N5,
Var::N6,
Var::N7,
Var::N8,
Var::N9,
Var::R0,
Var::R1,
Var::R2,
Var::R3,
Var::R4,
Var::R5,
Var::R6,
Var::R7,
Var::R8,
Var::R9,
Var::CmdLine,
Var::InstDir,
Var::OutDir,
Var::ExeDir,
Var::Language,
];
}
pub struct Variables {
base: *mut Tchar,
string_size: usize,
}
impl Variables {
#[must_use]
pub unsafe fn from_raw(base: *mut Tchar, string_size: usize) -> Self {
Self { base, string_size }
}
#[must_use]
pub fn string_size(&self) -> usize {
self.string_size
}
#[must_use]
pub fn get(&self, var: Var) -> String {
match self.slot(var) {
Some(slot) => unsafe { tchar::read_bounded(slot, self.string_size) },
None => String::new(),
}
}
pub fn set(&mut self, var: Var, value: &str) -> Result<()> {
self.set_units(var, &tchar::encode(value))
}
#[must_use]
pub fn get_int(&self, var: Var) -> isize {
match self.slot(var) {
Some(slot) => {
let len = unsafe { tchar::strlen_bounded(slot, self.string_size) };
let units = unsafe { core::slice::from_raw_parts(slot, len) };
int::str_to_ptr(int::parse_window(units))
}
None => 0,
}
}
pub fn set_int(&mut self, var: Var, value: isize) -> Result<()> {
self.set_units(var, &int::format(value))
}
fn set_units(&mut self, var: Var, units: &[Tchar]) -> Result<()> {
let slot = self.slot(var).ok_or(Error::NoVariables)?;
if unsafe { tchar::write_bounded(slot, self.string_size, units) } {
Ok(())
} else {
Err(Error::Truncated)
}
}
fn slot(&self, var: Var) -> Option<*mut Tchar> {
if self.base.is_null() || self.string_size == 0 {
return None;
}
Some(unsafe { self.base.add(var.index() * self.string_size) })
}
}
#[cfg(test)]
mod tests {
use alloc::string::String;
use super::*;
use crate::testing::TestInstaller;
#[test]
fn indices_match_the_inst_enum() {
assert_eq!(Var::N0.index(), 0);
assert_eq!(Var::N9.index(), 9);
assert_eq!(Var::R0.index(), 10);
assert_eq!(Var::R9.index(), 19);
assert_eq!(Var::CmdLine.index(), 20);
assert_eq!(Var::Language.index(), 24);
assert_eq!(Var::ALL.len(), VAR_COUNT);
}
#[test]
fn round_trips_a_value() {
let mut inst = TestInstaller::stock();
inst.nsis().vars.set(Var::R0, "value").unwrap();
assert_eq!(inst.nsis().vars.get(Var::R0), "value");
assert_eq!(inst.var(Var::R0), "value");
}
#[test]
fn every_slot_is_independent() {
let mut inst = TestInstaller::new(16);
{
let mut nsis = inst.nsis();
for (i, &var) in Var::ALL.iter().enumerate() {
nsis.vars.set(var, &alloc::format!("v{i}")).unwrap();
}
}
let nsis = inst.nsis();
for (i, &var) in Var::ALL.iter().enumerate() {
assert_eq!(nsis.vars.get(var), alloc::format!("v{i}"));
}
}
#[test]
fn an_over_long_value_does_not_reach_the_next_slot() {
const SIZE: usize = 16;
let mut inst = TestInstaller::new(SIZE);
inst.nsis().vars.set(Var::N1, "neighbour").unwrap();
let too_long: String = core::iter::repeat_n('x', SIZE * 4).collect();
assert_eq!(
inst.nsis().vars.set(Var::N0, &too_long).unwrap_err(),
Error::Truncated
);
assert_eq!(inst.var(Var::N0).len(), SIZE - 1);
assert_eq!(inst.var(Var::N1), "neighbour", "$1 was overwritten");
}
#[test]
fn the_boundary_is_string_size_minus_one() {
for size in [32, 1024, 8192] {
let mut inst = TestInstaller::new(size);
let exact: String = core::iter::repeat_n('a', size - 1).collect();
inst.nsis().vars.set(Var::InstDir, &exact).unwrap();
assert_eq!(inst.var(Var::InstDir), exact);
let one_too_many: String = core::iter::repeat_n('a', size).collect();
assert_eq!(
inst.nsis()
.vars
.set(Var::InstDir, &one_too_many)
.unwrap_err(),
Error::Truncated
);
assert_eq!(inst.var(Var::InstDir).len(), size - 1);
}
}
#[test]
fn integers_use_nsis_semantics() {
let mut inst = TestInstaller::stock();
inst.set_var(Var::R5, "0x20");
assert_eq!(inst.nsis().vars.get_int(Var::R5), 32);
inst.nsis().vars.set_int(Var::R6, -7).unwrap();
assert_eq!(inst.var(Var::R6), "-7");
}
#[test]
fn a_null_array_is_not_a_crash() {
let mut vars = unsafe { Variables::from_raw(core::ptr::null_mut(), 1024) };
assert_eq!(vars.get(Var::N0), "");
assert_eq!(vars.get_int(Var::N0), 0);
assert_eq!(vars.set(Var::N0, "x").unwrap_err(), Error::NoVariables);
}
}