diff --git a/CHANGELOG.md b/CHANGELOG.md index 382ad3f70..b32e74aef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ Bug fixes --------- * Fixes a panic when using `this` as the first parameter in a namespace-qualified function call. +* `max` and `min` for integers, strings and characters were missing from the standard library. They are now added. + +New features +------------ + +* Added `Engine::max_variables` and `Engine::set_max_variables` to limit the maximum number of variables allowed within a scope at any time. This is to guard against defining a huge number of variables containing large data just beyond individual data size limits. When `max_variables` is exceeded a new error, `ErrorTooManyVariables`, is returned. Version 1.15.1 diff --git a/src/api/limits.rs b/src/api/limits.rs index 6754edb28..09e13f417 100644 --- a/src/api/limits.rs +++ b/src/api/limits.rs @@ -44,6 +44,10 @@ pub struct Limits { pub max_function_expr_depth: Option, /// Maximum number of operations allowed to run. pub max_operations: Option, + /// Maximum number of variables allowed at any instant. + /// + /// Set to zero to effectively disable creating variables. + pub max_variables: usize, /// Maximum number of [modules][crate::Module] allowed to load. /// /// Set to zero to effectively disable loading any [module][crate::Module]. @@ -78,6 +82,7 @@ impl Limits { #[cfg(not(feature = "no_function"))] max_function_expr_depth: NonZeroUsize::new(default_limits::MAX_FUNCTION_EXPR_DEPTH), max_operations: None, + max_variables: usize::MAX, #[cfg(not(feature = "no_module"))] max_modules: usize::MAX, max_string_len: None, @@ -150,8 +155,6 @@ impl Engine { self } /// The maximum number of operations allowed for a script to run (0 for unlimited). - /// - /// Not available under `unchecked`. #[inline] #[must_use] pub const fn max_operations(&self) -> u64 { @@ -160,6 +163,20 @@ impl Engine { None => 0, } } + /// Set the maximum number of imported variables allowed for a script at any instant. + /// + /// Not available under `unchecked`. + #[inline(always)] + pub fn set_max_variables(&mut self, modules: usize) -> &mut Self { + self.limits.max_variables = modules; + self + } + /// The maximum number of imported variables allowed for a script at any instant. + #[inline(always)] + #[must_use] + pub const fn max_variables(&self) -> usize { + self.limits.max_variables + } /// Set the maximum number of imported [modules][crate::Module] allowed for a script. /// /// Not available under `unchecked` or `no_module`. diff --git a/src/api/limits_unchecked.rs b/src/api/limits_unchecked.rs index 975162c43..87e673550 100644 --- a/src/api/limits_unchecked.rs +++ b/src/api/limits_unchecked.rs @@ -20,6 +20,14 @@ impl Engine { pub const fn max_operations(&self) -> u64 { 0 } + /// The maximum number of variables allowed for a script at any instant. + /// + /// Always returns [`usize::MAX`]. + #[inline(always)] + #[must_use] + pub const fn max_variables(&self) -> usize { + usize::MAX + } /// The maximum number of imported [modules][crate::Module] allowed for a script. /// /// Always returns [`usize::MAX`]. diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index e747d8fcb..ef106b7f4 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -464,6 +464,9 @@ impl Engine { if let Some(index) = index { value.set_access_mode(access); *scope.get_mut_by_index(scope.len() - index.get()) = value; + } else if !cfg!(feature = "unchecked") && scope.len() >= self.max_variables() { + // Guard against too many variables + return Err(ERR::ErrorTooManyVariables(*pos).into()); } else { scope.push_entry(var_name.name.clone(), access, value); } @@ -879,7 +882,7 @@ impl Engine { let (expr, export) = &**x; // Guard against too many modules - if global.num_modules_loaded >= self.max_modules() { + if !cfg!(feature = "unchecked") && global.num_modules_loaded >= self.max_modules() { return Err(ERR::ErrorTooManyModules(*_pos).into()); } diff --git a/src/func/script.rs b/src/func/script.rs index 8da0e8baa..fc75ec984 100644 --- a/src/func/script.rs +++ b/src/func/script.rs @@ -63,6 +63,12 @@ impl Engine { .as_ref() .map_or(0, |dbg| dbg.call_stack().len()); + // Guard against too many variables + if !cfg!(feature = "unchecked") && scope.len() + fn_def.params.len() > self.max_variables() + { + return Err(ERR::ErrorTooManyVariables(pos).into()); + } + // Put arguments into scope as variables scope.extend(fn_def.params.iter().cloned().zip(args.iter_mut().map(|v| { // Actually consume the arguments instead of cloning them diff --git a/src/packages/logic.rs b/src/packages/logic.rs index e163eff6f..9d3cc634e 100644 --- a/src/packages/logic.rs +++ b/src/packages/logic.rs @@ -49,7 +49,6 @@ def_package! { reg_functions!(lib += numbers; i8, u8, i16, u16, i32, u32, u64); #[cfg(not(target_family = "wasm"))] - reg_functions!(lib += num_128; i128, u128); } @@ -73,6 +72,8 @@ def_package! { combine_with_exported_module!(lib, "decimal", decimal_functions); combine_with_exported_module!(lib, "logic", logic_functions); + + combine_with_exported_module!(lib, "min_max", min_max_functions); } } @@ -102,6 +103,40 @@ mod logic_functions { } } +#[export_module] +mod min_max_functions { + use crate::INT; + + /// Return the number that is larger than the other number. + /// + /// # Example + /// + /// ```rhai + /// max(42, 123); // returns 132 + /// ``` + pub fn max(x: INT, y: INT) -> INT { + if x >= y { + x + } else { + y + } + } + /// Return the number that is smaller than the other number. + /// + /// # Example + /// + /// ```rhai + /// min(42, 123); // returns 42 + /// ``` + pub fn min(x: INT, y: INT) -> INT { + if x <= y { + x + } else { + y + } + } +} + #[cfg(not(feature = "no_float"))] #[allow(clippy::cast_precision_loss)] #[export_module] diff --git a/src/packages/string_more.rs b/src/packages/string_more.rs index 7de4fecd9..bc82bdc89 100644 --- a/src/packages/string_more.rs +++ b/src/packages/string_more.rs @@ -1348,6 +1348,66 @@ mod string_functions { Ok(()) } + /// Return the string that is lexically greater than the other string. + /// + /// # Example + /// + /// ```rhai + /// max("hello", "world"); // returns "world" + /// ``` + #[rhai_fn(name = "max")] + pub fn max_string(string1: ImmutableString, string2: ImmutableString) -> ImmutableString { + if string1 >= string2 { + string1 + } else { + string2 + } + } + /// Return the string that is lexically smaller than the other string. + /// + /// # Example + /// + /// ```rhai + /// min("hello", "world"); // returns "hello" + /// ``` + #[rhai_fn(name = "min")] + pub fn min_string(string1: ImmutableString, string2: ImmutableString) -> ImmutableString { + if string1 <= string2 { + string1 + } else { + string2 + } + } + /// Return the character that is lexically greater than the other character. + /// + /// # Example + /// + /// ```rhai + /// max('h', 'w'); // returns 'w' + /// ``` + #[rhai_fn(name = "max")] + pub fn max_char(char1: char, char2: char) -> char { + if char1 >= char2 { + char1 + } else { + char2 + } + } + /// Return the character that is lexically smaller than the other character. + /// + /// # Example + /// + /// ```rhai + /// max('h', 'w'); // returns 'h' + /// ``` + #[rhai_fn(name = "min")] + pub fn min_char(char1: char, char2: char) -> char { + if char1 <= char2 { + char1 + } else { + char2 + } + } #[cfg(not(feature = "no_index"))] pub mod arrays { diff --git a/src/types/error.rs b/src/types/error.rs index c60edce50..7ce3b230e 100644 --- a/src/types/error.rs +++ b/src/types/error.rs @@ -95,6 +95,8 @@ pub enum EvalAltResult { /// Number of operations over maximum limit. ErrorTooManyOperations(Position), + /// Number of variables over maximum limit. + ErrorTooManyVariables(Position), /// [Modules][crate::Module] over maximum limit. ErrorTooManyModules(Position), /// Call stack over maximum limit. @@ -166,6 +168,7 @@ impl fmt::Display for EvalAltResult { Self::ErrorUnboundThis(..) => f.write_str("'this' not bound")?, Self::ErrorFor(..) => f.write_str("For loop expects iterable type")?, Self::ErrorTooManyOperations(..) => f.write_str("Too many operations")?, + Self::ErrorTooManyVariables(..) => f.write_str("Too many variables defined")?, Self::ErrorTooManyModules(..) => f.write_str("Too many modules imported")?, Self::ErrorStackOverflow(..) => f.write_str("Stack overflow")?, Self::ErrorTerminated(..) => f.write_str("Script terminated")?, @@ -331,6 +334,7 @@ impl EvalAltResult { Self::ErrorCustomSyntax(..) => false, Self::ErrorTooManyOperations(..) + | Self::ErrorTooManyVariables(..) | Self::ErrorTooManyModules(..) | Self::ErrorStackOverflow(..) | Self::ErrorDataTooLarge(..) @@ -350,6 +354,7 @@ impl EvalAltResult { | Self::ErrorParsing(..) | Self::ErrorCustomSyntax(..) | Self::ErrorTooManyOperations(..) + | Self::ErrorTooManyVariables(..) | Self::ErrorTooManyModules(..) | Self::ErrorStackOverflow(..) | Self::ErrorDataTooLarge(..) @@ -379,6 +384,7 @@ impl EvalAltResult { | Self::ErrorFor(..) | Self::ErrorArithmetic(..) | Self::ErrorTooManyOperations(..) + | Self::ErrorTooManyVariables(..) | Self::ErrorTooManyModules(..) | Self::ErrorStackOverflow(..) | Self::ErrorRuntime(..) => (), @@ -483,6 +489,7 @@ impl EvalAltResult { | Self::ErrorDotExpr(.., pos) | Self::ErrorArithmetic(.., pos) | Self::ErrorTooManyOperations(pos) + | Self::ErrorTooManyVariables(pos) | Self::ErrorTooManyModules(pos) | Self::ErrorStackOverflow(pos) | Self::ErrorDataTooLarge(.., pos) @@ -543,6 +550,7 @@ impl EvalAltResult { | Self::ErrorDotExpr(.., pos) | Self::ErrorArithmetic(.., pos) | Self::ErrorTooManyOperations(pos) + | Self::ErrorTooManyVariables(pos) | Self::ErrorTooManyModules(pos) | Self::ErrorStackOverflow(pos) | Self::ErrorDataTooLarge(.., pos) diff --git a/tests/var_scope.rs b/tests/var_scope.rs index 776bf2157..41922ba32 100644 --- a/tests/var_scope.rs +++ b/tests/var_scope.rs @@ -93,6 +93,155 @@ fn test_var_scope() -> Result<(), Box> { Ok(()) } +#[cfg(not(feature = "unchecked"))] +#[test] +fn test_var_scope_max() -> Result<(), Box> { + let mut engine = Engine::new(); + let mut scope = Scope::new(); + + engine.set_max_variables(5); + + engine.run_with_scope( + &mut scope, + " + let a = 0; + let b = 0; + let c = 0; + let d = 0; + let e = 0; + ", + )?; + + scope.clear(); + + engine.run_with_scope( + &mut scope, + " + let a = 0; + let b = 0; + let c = 0; + let d = 0; + let e = 0; + let a = 42; // reuse variable + ", + )?; + + scope.clear(); + + #[cfg(not(feature = "no_function"))] + engine.run_with_scope( + &mut scope, + " + fn foo(n) { + if n > 3 { return; } + + let w = 0; + let x = 0; + let y = 0; + let z = 0; + + foo(n + 1); + } + + let a = 0; + let b = 0; + let c = 0; + let d = 0; + let e = 0; + + foo(0); + ", + )?; + + scope.clear(); + + #[cfg(not(feature = "no_function"))] + engine.run_with_scope( + &mut scope, + " + fn foo(a, b, c, d, e) { + 42 + } + + foo(0, 0, 0, 0, 0); + ", + )?; + + scope.clear(); + + assert!(matches!( + *engine + .run_with_scope( + &mut scope, + " + let a = 0; + let b = 0; + let c = 0; + let d = 0; + let e = 0; + let f = 0; + " + ) + .unwrap_err(), + EvalAltResult::ErrorTooManyVariables(..) + )); + + scope.clear(); + + #[cfg(not(feature = "no_function"))] + assert!(matches!( + *engine + .run_with_scope( + &mut scope, + " + fn foo(n) { + if n > 3 { return; } + + let v = 0; + let w = 0; + let x = 0; + let y = 0; + let z = 0; + + foo(n + 1); + } + + let a = 0; + let b = 0; + let c = 0; + let d = 0; + let e = 0; + let f = 0; + + foo(0); + " + ) + .unwrap_err(), + EvalAltResult::ErrorTooManyVariables(..) + )); + + scope.clear(); + + #[cfg(not(feature = "no_function"))] + assert!(matches!( + *engine + .run_with_scope( + &mut scope, + " + fn foo(a, b, c, d, e, f) { + 42 + } + + foo(0, 0, 0, 0, 0, 0); + " + ) + .unwrap_err(), + EvalAltResult::ErrorTooManyVariables(..) + )); + + Ok(()) +} + #[cfg(not(feature = "no_module"))] #[test] fn test_var_scope_alias() -> Result<(), Box> {