#![allow(
clippy::undocumented_unsafe_blocks,
reason = "every pointer here is the installer's own, established once in \
`Stack::from_raw` and valid for the duration of a plug-in call; \
restating it on each dereference would bury the comments that \
carry real information"
)]
use alloc::string::String;
use alloc::vec::Vec;
use crate::error::{Error, Result};
use crate::int;
use crate::raw::StackNode;
use crate::sys;
use crate::tchar::{self, Tchar};
pub struct Stack {
top: *mut *mut StackNode,
string_size: usize,
}
impl Stack {
#[must_use]
pub unsafe fn from_raw(top: *mut *mut StackNode, string_size: usize) -> Self {
Self { top, string_size }
}
#[must_use]
pub fn string_size(&self) -> usize {
self.string_size
}
#[must_use]
pub fn max_len(&self) -> usize {
self.string_size.saturating_sub(1)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.top.is_null() || unsafe { (*self.top).is_null() }
}
#[must_use]
pub fn len(&self) -> usize {
if self.top.is_null() {
return 0;
}
let mut node = unsafe { *self.top };
let mut n = 0;
while !node.is_null() {
n += 1;
node = unsafe { (*node).next };
}
n
}
pub fn pop(&mut self) -> Result<String> {
self.pop_units().map(|units| tchar::decode(&units))
}
pub fn peek(&self) -> Result<String> {
let node = self.head()?;
Ok(unsafe { tchar::read_bounded(self.text_of(node), self.string_size) })
}
pub fn discard(&mut self) -> Result<()> {
self.pop_units().map(|_| ())
}
pub fn pop_int(&mut self) -> Result<isize> {
let units = self.pop_units()?;
Ok(int::str_to_ptr(int::parse_window(&units)))
}
pub fn pop_int_or(&mut self) -> Result<isize> {
let units = self.pop_units()?;
Ok(int::atoi_or(int::parse_window(&units)))
}
pub fn push(&mut self, s: &str) -> Result<()> {
self.push_units(&tchar::encode(s))
}
pub fn push_int(&mut self, value: isize) -> Result<()> {
self.push_units(&int::format(value))
}
pub fn push_bool(&mut self, value: bool) -> Result<()> {
self.push_int(isize::from(value))
}
fn head(&self) -> Result<*mut StackNode> {
if self.top.is_null() {
return Err(Error::NoStack);
}
let node = unsafe { *self.top };
if node.is_null() {
return Err(Error::EmptyStack);
}
Ok(node)
}
fn text_of(&self, node: *mut StackNode) -> *mut Tchar {
unsafe { (&raw mut (*node).text).cast::<Tchar>() }
}
fn pop_units(&mut self) -> Result<Vec<Tchar>> {
let node = self.head()?;
let text = self.text_of(node);
let len = unsafe { tchar::strlen_bounded(text, self.string_size) };
let units = unsafe { core::slice::from_raw_parts(text, len) }.to_vec();
unsafe {
*self.top = (*node).next;
sys::free(node.cast());
}
Ok(units)
}
fn push_units(&mut self, units: &[Tchar]) -> Result<()> {
if self.top.is_null() {
return Err(Error::NoStack);
}
let node = unsafe { sys::alloc_zeroed(StackNode::alloc_size(self.string_size)) }
.cast::<StackNode>();
if node.is_null() {
return Err(Error::OutOfMemory);
}
let fit = unsafe { tchar::write_bounded(self.text_of(node), self.string_size, units) };
unsafe {
(*node).next = *self.top;
*self.top = node;
}
if fit { Ok(()) } else { Err(Error::Truncated) }
}
}
#[cfg(test)]
mod tests {
use alloc::string::String;
use alloc::vec::Vec;
use super::*;
use crate::testing::TestInstaller;
#[test]
fn round_trips_a_string() {
let mut inst = TestInstaller::stock();
inst.nsis().stack.push("hello").unwrap();
assert_eq!(inst.nsis().stack.pop().unwrap(), "hello");
}
#[test]
fn is_last_in_first_out() {
let mut inst = TestInstaller::stock();
let mut nsis = inst.nsis();
for value in ["first", "second", "third"] {
nsis.stack.push(value).unwrap();
}
assert_eq!(inst.stack(), ["third", "second", "first"]);
assert_eq!(inst.pop().unwrap(), "third");
assert_eq!(inst.pop().unwrap(), "second");
assert_eq!(inst.pop().unwrap(), "first");
assert!(inst.is_empty());
}
#[test]
fn popping_an_empty_stack_is_an_error() {
let mut inst = TestInstaller::stock();
assert_eq!(inst.nsis().stack.pop().unwrap_err(), Error::EmptyStack);
}
#[test]
fn a_null_stack_is_not_a_crash() {
let mut stack = unsafe { Stack::from_raw(core::ptr::null_mut(), 1024) };
assert!(stack.is_empty());
assert_eq!(stack.len(), 0);
assert_eq!(stack.pop().unwrap_err(), Error::NoStack);
assert_eq!(stack.push("x").unwrap_err(), Error::NoStack);
}
#[test]
fn the_boundary_is_string_size_minus_one() {
for size in [64, 1024, 8192] {
let mut inst = TestInstaller::new(size);
let exact: String = core::iter::repeat_n('x', size - 1).collect();
inst.nsis().stack.push(&exact).unwrap();
assert_eq!(inst.pop().unwrap(), exact);
let one_too_many: String = core::iter::repeat_n('x', size).collect();
assert_eq!(
inst.nsis().stack.push(&one_too_many).unwrap_err(),
Error::Truncated
);
assert_eq!(inst.pop().unwrap().len(), size - 1);
}
}
#[test]
fn a_long_string_build_holds_long_strings() {
let mut inst = TestInstaller::long_string();
let long: String = core::iter::repeat_n('z', 8191).collect();
inst.nsis().stack.push(&long).unwrap();
assert_eq!(inst.pop().unwrap(), long);
let mut stock = TestInstaller::stock();
assert_eq!(
stock.nsis().stack.push(&long).unwrap_err(),
Error::Truncated
);
assert_eq!(stock.pop().unwrap().len(), 1023);
}
#[test]
fn a_zero_string_size_still_pushes_an_empty_entry() {
let mut inst = TestInstaller::new(0);
assert_eq!(inst.nsis().stack.push("x").unwrap_err(), Error::Truncated);
assert_eq!(inst.stack(), [""]);
}
#[test]
fn pops_integers_with_nsis_semantics() {
let mut inst = TestInstaller::stock();
inst.push("0x10");
assert_eq!(inst.nsis().stack.pop_int().unwrap(), 16);
inst.push("2|4|8");
assert_eq!(inst.nsis().stack.pop_int_or().unwrap(), 14);
inst.push("not a number");
assert_eq!(inst.nsis().stack.pop_int().unwrap(), 0);
}
#[test]
fn pushes_integers_as_scripts_expect() {
let mut inst = TestInstaller::stock();
inst.nsis().stack.push_int(-42).unwrap();
assert_eq!(inst.pop().unwrap(), "-42");
inst.nsis().stack.push_bool(true).unwrap();
assert_eq!(inst.pop().unwrap(), "1");
}
#[test]
fn peek_does_not_consume() {
let mut inst = TestInstaller::stock();
inst.push("kept");
assert_eq!(inst.nsis().stack.peek().unwrap(), "kept");
assert_eq!(inst.nsis().stack.peek().unwrap(), "kept");
assert_eq!(inst.nsis().stack.len(), 1);
}
#[test]
fn survives_non_ascii() {
let mut inst = TestInstaller::stock();
#[cfg(feature = "unicode")]
let values = ["grüße", "日本語", "🦀"];
#[cfg(all(feature = "ansi", not(feature = "unicode")))]
let values = ["grüße", "façade"];
for value in values {
inst.nsis().stack.push(value).unwrap();
assert_eq!(inst.pop().unwrap(), value);
}
}
#[test]
fn many_entries_do_not_leak_into_each_other() {
let mut inst = TestInstaller::new(32);
let values: Vec<String> = (0..50).map(|i| alloc::format!("value-{i}")).collect();
{
let mut nsis = inst.nsis();
for value in &values {
nsis.stack.push(value).unwrap();
}
}
for value in values.iter().rev() {
assert_eq!(&inst.pop().unwrap(), value);
}
assert!(inst.is_empty());
}
}