mod function;
mod scopes;
mod variable;
use std::collections::HashMap;
pub(super) use function::FunctionMap;
use phf::phf_map;
pub(super) use scopes::SessionScope;
use thiserror_no_std::Error;
pub(super) use variable::{Scope, VarName};
use crate::parser::{RuntimeTypeTrait, Val, value::ScriptBlock};
#[derive(Error, Debug, PartialEq, Clone)]
pub enum VariableError {
#[error("Variable \"{0}\" is not defined")]
NotDefined(String),
#[error("Cannot overwrite variable \"{0}\" because it is read-only or constant.")]
ReadOnly(String),
}
pub type VariableResult<T> = core::result::Result<T, VariableError>;
pub type VariableMap = HashMap<String, Val>;
#[derive(Clone, Default)]
pub struct Variables {
env: VariableMap,
global_scope: VariableMap,
script_scope: VariableMap,
local_scopes_stack: Vec<VariableMap>,
state: State,
force_var_eval: bool,
values_persist: bool,
global_functions: FunctionMap,
script_functions: FunctionMap,
top_scope: TopScope,
}
#[derive(Debug, Default, Clone)]
pub(super) enum TopScope {
#[default]
Session,
Script,
}
impl From<Scope> for TopScope {
fn from(scope: Scope) -> Self {
match scope {
Scope::Global => TopScope::Session,
Scope::Script => TopScope::Script,
_ => TopScope::Script,
}
}
}
#[derive(Clone)]
enum State {
TopScope(TopScope),
Stack(u32),
}
impl Default for State {
fn default() -> Self {
State::TopScope(TopScope::default())
}
}
impl Variables {
const PREDEFINED_VARIABLES: phf::Map<&'static str, Val> = phf_map! {
"true" => Val::Bool(true),
"false" => Val::Bool(false),
"null" => Val::Null,
};
pub(crate) fn set_ps_item(&mut self, ps_item: Val) {
let _ = self.set(
&VarName::new_with_scope(Scope::Special, "$PSItem".into()),
ps_item.clone(),
);
let _ = self.set(
&VarName::new_with_scope(Scope::Special, "$_".into()),
ps_item,
);
}
pub(crate) fn reset_ps_item(&mut self) {
let _ = self.set(
&VarName::new_with_scope(Scope::Special, "$PSItem".into()),
Val::Null,
);
let _ = self.set(
&VarName::new_with_scope(Scope::Special, "$_".into()),
Val::Null,
);
}
pub fn set_status(&mut self, b: bool) {
let _ = self.set(
&VarName::new_with_scope(Scope::Special, "$?".into()),
Val::Bool(b),
);
}
pub fn status(&mut self) -> bool {
let Some(Val::Bool(b)) =
self.get_without_types(&VarName::new_with_scope(Scope::Special, "$?".into()))
else {
return false;
};
b
}
pub fn load_from_file(
&mut self,
path: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
let mut config_parser = configparser::ini::Ini::new();
let map = config_parser.load(path)?;
self.load(map)
}
pub fn load_from_string(&mut self, ini_string: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut config_parser = configparser::ini::Ini::new();
let map = config_parser.read(ini_string.into())?;
self.load(map)
}
pub(super) fn init(&mut self, scope: TopScope) {
if !self.values_persist {
self.script_scope.clear();
}
self.local_scopes_stack.clear();
self.state = State::TopScope(scope.clone());
self.top_scope = scope;
}
fn load(
&mut self,
conf_map: HashMap<String, HashMap<String, Option<String>>>,
) -> Result<(), Box<dyn std::error::Error>> {
for (section_name, properties) in conf_map {
for (key, value) in properties {
let Some(value) = value else {
continue;
};
let var_name = match section_name.as_str() {
"global" => VarName::new_with_scope(Scope::Global, key.to_lowercase()),
"script" => VarName::new_with_scope(Scope::Script, key.to_lowercase()),
"env" => VarName::new_with_scope(Scope::Env, key.to_lowercase()),
_ => {
continue;
}
};
let parsed_value = if let Ok(bool_val) = value.parse::<bool>() {
Val::Bool(bool_val)
} else if let Ok(int_val) = value.parse::<i64>() {
Val::Int(int_val)
} else if let Ok(float_val) = value.parse::<f64>() {
Val::Float(float_val)
} else if value.is_empty() {
Val::Null
} else {
Val::String(value.clone().into())
};
if let Err(err) = self.set(&var_name, parsed_value.clone()) {
log::error!("Failed to set variable {:?}: {}", var_name, err);
}
}
}
Ok(())
}
pub(crate) fn script_scope(&self) -> VariableMap {
self.script_scope.clone()
}
pub(crate) fn get_env(&self) -> VariableMap {
self.env.clone()
}
pub(crate) fn get_global(&self) -> VariableMap {
self.global_scope.clone()
}
pub(crate) fn add_script_function(&mut self, name: String, func: ScriptBlock) {
self.script_functions.insert(name, func);
}
pub(crate) fn add_global_function(&mut self, name: String, func: ScriptBlock) {
self.global_functions.insert(name, func);
}
pub(crate) fn clear_script_functions(&mut self) {
self.script_functions.clear();
}
pub fn new() -> Variables {
Default::default()
}
pub fn force_eval() -> Self {
Self {
force_var_eval: true,
..Default::default()
}
}
#[allow(dead_code)]
pub(crate) fn values_persist(mut self) -> Self {
self.values_persist = true;
self
}
pub fn env() -> Variables {
let mut vars = Variables::new();
for (key, value) in std::env::vars() {
vars.env
.insert(key.to_lowercase(), Val::String(value.into()));
}
vars
}
pub fn from_ini_string(ini_string: &str) -> Result<Self, Box<dyn std::error::Error>> {
let mut variables = Self::new();
variables.load_from_string(ini_string)?;
Ok(variables)
}
pub fn from_ini_file(path: &std::path::Path) -> Result<Self, Box<dyn std::error::Error>> {
let mut variables = Self::new();
variables.load_from_file(path)?;
Ok(variables)
}
fn top_scope(&self, scope: Option<&TopScope>) -> &VariableMap {
let scope = if let Some(s) = scope {
s
} else {
&self.top_scope
};
match scope {
TopScope::Session => &self.global_scope,
TopScope::Script => &self.script_scope,
}
}
fn mut_top_scope(&mut self, scope: Option<&TopScope>) -> &mut VariableMap {
let scope = if let Some(s) = scope {
s
} else {
&self.top_scope
};
match scope {
TopScope::Session => &mut self.global_scope,
TopScope::Script => &mut self.script_scope,
}
}
fn const_map_from_scope(&self, scope: &Scope) -> &VariableMap {
match scope {
Scope::Global => &self.global_scope,
Scope::Script => &self.script_scope,
Scope::Env => &self.env,
Scope::Local => match &self.state {
State::TopScope(scope) => self.top_scope(Some(scope)),
State::Stack(depth) => {
if *depth < self.local_scopes_stack.len() as u32 {
&self.local_scopes_stack[*depth as usize]
} else {
&self.script_scope
}
}
},
Scope::Special => {
&self.global_scope }
}
}
fn local_scope(&mut self) -> &mut VariableMap {
match &mut self.state {
State::TopScope(scope) => {
let scope = scope.clone();
self.mut_top_scope(Some(&scope))
}
State::Stack(depth) => {
if *depth < self.local_scopes_stack.len() as u32 {
&mut self.local_scopes_stack[*depth as usize]
} else {
&mut self.global_scope
}
}
}
}
fn map_from_scope(&mut self, scope: Option<&Scope>) -> &mut VariableMap {
match scope {
Some(Scope::Global) => &mut self.global_scope,
Some(Scope::Script) => &mut self.script_scope,
Some(Scope::Env) => &mut self.env,
Some(Scope::Local) => self.local_scope(),
Some(Scope::Special) => {
&mut self.global_scope }
None => self.mut_top_scope(None),
}
}
pub(crate) fn set(&mut self, var_name: &VarName, val: Val) -> VariableResult<()> {
let var = self.find_mut_variable_in_scopes(var_name)?;
if let Some(variable) = var {
*variable = val;
} else {
let map = self.map_from_scope(var_name.scope.as_ref());
map.insert(var_name.name.to_ascii_lowercase(), val);
}
Ok(())
}
pub(crate) fn set_local(&mut self, name: &str, val: Val) -> VariableResult<()> {
let var_name = VarName::new_with_scope(Scope::Local, name.to_ascii_lowercase());
self.set(&var_name, val)
}
fn find_mut_variable_in_scopes(
&mut self,
var_name: &VarName,
) -> VariableResult<Option<&mut Val>> {
let name = var_name.name.to_ascii_lowercase();
let name_str = name.as_str();
if let Some(scope) = &var_name.scope
&& self.const_map_from_scope(scope).contains_key(name_str)
{
Ok(self.map_from_scope(Some(scope)).get_mut(name_str))
} else {
if Self::PREDEFINED_VARIABLES.contains_key(name_str) {
return Err(VariableError::ReadOnly(name.clone()));
}
for local_scope in self.local_scopes_stack.iter_mut().rev() {
if local_scope.contains_key(name_str) {
return Ok(local_scope.get_mut(name_str));
}
}
if self.script_scope.contains_key(name_str) {
return Ok(self.script_scope.get_mut(name_str));
}
if self.global_scope.contains_key(name_str) {
return Ok(self.global_scope.get_mut(name_str));
}
Ok(None)
}
}
pub(crate) fn get(
&self,
var_name: &VarName,
types_map: &HashMap<String, Box<dyn RuntimeTypeTrait>>,
) -> Option<Val> {
let var = self.find_variable_in_scopes(var_name);
if self.force_var_eval && var.is_none() {
if let Some(rt) = types_map.get(var_name.name.as_str()) {
Some(Val::RuntimeType(rt.clone_rt()))
} else {
Some(Val::Null)
}
} else {
var.cloned()
}
}
pub(crate) fn get_without_types(&self, var_name: &VarName) -> Option<Val> {
let var = self.find_variable_in_scopes(var_name);
if self.force_var_eval && var.is_none() {
Some(Val::Null)
} else {
var.cloned()
}
}
fn find_variable_in_scopes(&self, var_name: &VarName) -> Option<&Val> {
let name = var_name.name.to_ascii_lowercase();
let name_str = name.as_str();
if let Some(scope) = &var_name.scope {
let map = self.const_map_from_scope(scope);
let x = map.get(name_str);
if x.is_some() {
return x;
}
}
if Self::PREDEFINED_VARIABLES.contains_key(name_str) {
return Self::PREDEFINED_VARIABLES.get(name_str);
}
for local_scope in self.local_scopes_stack.iter().rev() {
if local_scope.contains_key(name_str) {
return local_scope.get(name_str);
}
}
if self.script_scope.contains_key(name_str) {
return self.script_scope.get(name_str);
}
if self.global_scope.contains_key(name_str) {
return self.global_scope.get(name_str);
}
None
}
pub(crate) fn push_scope_session(&mut self) {
let current_map = self.local_scope();
let new_map = current_map.clone();
self.local_scopes_stack.push(new_map);
self.state = State::Stack(self.local_scopes_stack.len() as u32 - 1);
}
pub(crate) fn pop_scope_session(&mut self) {
match self.local_scopes_stack.len() {
0 => {}
1 => {
self.local_scopes_stack.pop();
self.state = State::TopScope(self.top_scope.clone());
}
_ => {
self.local_scopes_stack.pop();
self.state = State::Stack(self.local_scopes_stack.len() as u32 - 1);
}
}
}
}
#[cfg(test)]
mod tests {
use super::Variables;
use crate::{PowerShellSession, PsValue};
#[test]
fn test_builtin_variables() {
let mut p = PowerShellSession::new();
assert_eq!(p.safe_eval(r#" $true "#).unwrap().as_str(), "True");
assert_eq!(p.safe_eval(r#" $false "#).unwrap().as_str(), "False");
assert_eq!(p.safe_eval(r#" $null "#).unwrap().as_str(), "");
}
#[test]
fn test_env_variables() {
let v = Variables::env();
let mut p = PowerShellSession::new().with_variables(v);
assert_eq!(
p.safe_eval(r#" $env:path "#).unwrap().as_str(),
std::env::var("PATH").unwrap()
);
assert_eq!(
p.safe_eval(r#" $env:username "#).unwrap().as_str(),
std::env::var("USERNAME").unwrap()
);
assert_eq!(
p.safe_eval(r#" $env:tEMp "#).unwrap().as_str(),
std::env::var("TEMP").unwrap()
);
assert_eq!(
p.safe_eval(r#" $env:tMp "#).unwrap().as_str(),
std::env::var("TMP").unwrap()
);
assert_eq!(
p.safe_eval(r#" $env:cOmputername "#).unwrap().as_str(),
std::env::var("COMPUTERNAME").unwrap()
);
assert_eq!(
p.safe_eval(r#" $env:programfiles "#).unwrap().as_str(),
std::env::var("PROGRAMFILES").unwrap()
);
assert_eq!(
p.safe_eval(r#" $env:temp "#).unwrap().as_str(),
std::env::var("TEMP").unwrap()
);
assert_eq!(
p.safe_eval(r#" ${Env:ProgramFiles(x86)} "#)
.unwrap()
.as_str(),
std::env::var("ProgramFiles(x86)").unwrap()
);
let env_variables = p.env_variables();
assert_eq!(
env_variables.get("path").unwrap().to_string(),
std::env::var("PATH").unwrap()
);
assert_eq!(
env_variables.get("tmp").unwrap().to_string(),
std::env::var("TMP").unwrap()
);
assert_eq!(
env_variables.get("temp").unwrap().to_string(),
std::env::var("TMP").unwrap()
);
assert_eq!(
env_variables.get("appdata").unwrap().to_string(),
std::env::var("APPDATA").unwrap()
);
assert_eq!(
env_variables.get("username").unwrap().to_string(),
std::env::var("USERNAME").unwrap()
);
assert_eq!(
env_variables.get("programfiles").unwrap().to_string(),
std::env::var("PROGRAMFILES").unwrap()
);
assert_eq!(
env_variables.get("programfiles(x86)").unwrap().to_string(),
std::env::var("PROGRAMFILES(x86)").unwrap()
);
}
#[test]
fn test_global_variables() {
let v = Variables::env();
let mut p = PowerShellSession::new().with_variables(v);
p.parse_script(r#" $global:var_int = 5 "#).unwrap();
p.parse_script(r#" $global:var_string = "global";$script:var_string = "script";$local:var_string = "local" "#).unwrap();
assert_eq!(
p.parse_script(r#" $var_int "#).unwrap().result(),
PsValue::Int(5)
);
assert_eq!(
p.parse_script(r#" $var_string "#).unwrap().result(),
PsValue::String("local".into())
);
let global_variables = p.session_variables();
assert_eq!(global_variables.get("var_int").unwrap(), &PsValue::Int(5));
assert_eq!(
global_variables.get("var_string").unwrap(),
&PsValue::String("local".into())
);
}
#[test]
fn test_script_variables() {
let v = Variables::env();
let mut p = PowerShellSession::new().with_variables(v);
let script_res = p
.parse_script(r#" $script:var_int = 5;$var_string = "assdfa" "#)
.unwrap();
let script_variables = script_res.script_variables();
assert_eq!(script_variables.get("var_int"), Some(&PsValue::Int(5)));
assert_eq!(
script_variables.get("var_string"),
Some(&PsValue::String("assdfa".into()))
);
}
#[test]
fn test_env_special_cases() {
let v = Variables::env();
let mut p = PowerShellSession::new().with_variables(v);
p.safe_eval(r#" $global:program = $env:programfiles + "\program" "#)
.unwrap();
assert_eq!(
p.safe_eval(r#" $global:program "#).unwrap().as_str(),
format!("{}\\program", std::env::var("PROGRAMFILES").unwrap())
);
assert_eq!(
p.safe_eval(r#" $program "#).unwrap().as_str(),
format!("{}\\program", std::env::var("PROGRAMFILES").unwrap())
);
assert_eq!(
p.safe_eval(r#" ${Env:ProgramFiles(x86):adsf} = 5;${Env:ProgramFiles(x86):adsf} "#)
.unwrap()
.as_str(),
5.to_string()
);
assert_eq!(
p.safe_eval(r#" ${Env:ProgramFiles(x86)} "#)
.unwrap()
.as_str(),
std::env::var("ProgramFiles(x86)").unwrap()
);
}
#[test]
fn special_last_error() {
let input = r#"3+"01234 ?";$a=5;$a;$?"#;
let mut p = PowerShellSession::new();
assert_eq!(p.safe_eval(input).unwrap().as_str(), "True");
let input = r#"3+"01234 ?";$?"#;
assert_eq!(p.safe_eval(input).unwrap().as_str(), "False");
}
#[test]
fn test_from_ini() {
let input = r#"[global]
name = radek
age = 30
is_admin = true
height = 5.9
empty_value =
[script]
local_var = "local_value"
"#;
let mut variables = Variables::new().values_persist();
variables.load_from_string(input).unwrap();
let mut p = PowerShellSession::new().with_variables(variables);
assert_eq!(
p.parse_script(r#" $global:name "#).unwrap().result(),
PsValue::String("radek".into())
);
assert_eq!(
p.parse_script(r#" $global:age "#).unwrap().result(),
PsValue::Int(30)
);
assert_eq!(p.safe_eval(r#" $false "#).unwrap().as_str(), "False");
assert_eq!(p.safe_eval(r#" $null "#).unwrap().as_str(), "");
assert_eq!(
p.safe_eval(r#" $script:local_var "#).unwrap().as_str(),
"\"local_value\""
);
assert_eq!(
p.safe_eval(r#" $local:local_var "#).unwrap().as_str(),
"\"local_value\""
);
}
#[test]
fn test_from_ini_string() {
let input = r#"[global]
name = radek
age = 30
is_admin = true
height = 5.9
empty_value =
[script]
local_var = "local_value"
"#;
let variables = Variables::from_ini_string(input).unwrap().values_persist();
let mut p = PowerShellSession::new().with_variables(variables);
assert_eq!(
p.parse_script(r#" $global:name "#).unwrap().result(),
PsValue::String("radek".into())
);
assert_eq!(
p.parse_script(r#" $global:age "#).unwrap().result(),
PsValue::Int(30)
);
assert_eq!(p.safe_eval(r#" $false "#).unwrap().as_str(), "False");
assert_eq!(p.safe_eval(r#" $null "#).unwrap().as_str(), "");
assert_eq!(
p.safe_eval(r#" $script:local_var "#).unwrap().as_str(),
"\"local_value\""
);
assert_eq!(
p.safe_eval(r#" $local_var "#).unwrap().as_str(),
"\"local_value\""
);
assert_eq!(
p.safe_eval(r#" $local:local_var "#).unwrap().as_str(),
"\"local_value\""
);
}
}