Add progress tracking and operations limit.

This commit is contained in:
Stephen Chung 2020-05-15 11:43:32 +08:00
parent 5d5ceb4049
commit 55c97eb649
7 changed files with 489 additions and 88 deletions

150
README.md
View File

@ -21,11 +21,15 @@ Rhai's current features set:
* Fairly efficient (1 million iterations in 0.75 sec on my 5 year old laptop) * Fairly efficient (1 million iterations in 0.75 sec on my 5 year old laptop)
* Low compile-time overhead (~0.6 sec debug/~3 sec release for script runner app) * Low compile-time overhead (~0.6 sec debug/~3 sec release for script runner app)
* Relatively little `unsafe` code (yes there are some for performance reasons) * Relatively little `unsafe` code (yes there are some for performance reasons)
* Sand-boxed (the scripting [`Engine`] can be declared immutable which cannot mutate the containing environment
unless explicitly allowed via `RefCell` etc.)
* Rugged (protection against [stack-overflow](#maximum-stack-depth) and [runaway scripts](#maximum-number-of-operations))
* Able to set limits on script resource usage (e.g. see [tracking progress](#tracking-progress))
* [`no-std`](#optional-features) support * [`no-std`](#optional-features) support
* [Function overloading](#function-overloading) * [Function overloading](#function-overloading)
* [Operator overloading](#operator-overloading) * [Operator overloading](#operator-overloading)
* [Modules] * [Modules]
* Compiled script is [optimized](#script-optimization) for repeat evaluations * Compiled script is [optimized](#script-optimization) for repeated evaluations
* Support for [minimal builds](#minimal-builds) by excluding unneeded language [features](#optional-features) * Support for [minimal builds](#minimal-builds) by excluding unneeded language [features](#optional-features)
* Very few additional dependencies (right now only [`num-traits`](https://crates.io/crates/num-traits/) * Very few additional dependencies (right now only [`num-traits`](https://crates.io/crates/num-traits/)
to do checked arithmetic operations); for [`no-std`](#optional-features) builds, a number of additional dependencies are to do checked arithmetic operations); for [`no-std`](#optional-features) builds, a number of additional dependencies are
@ -63,19 +67,19 @@ Beware that in order to use pre-releases (e.g. alpha and beta), the exact versio
Optional features Optional features
----------------- -----------------
| Feature | Description | | Feature | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `unchecked` | Exclude arithmetic checking (such as overflows and division by zero). Beware that a bad script may panic the entire system! | | `unchecked` | Exclude arithmetic checking (such as over-flows and division by zero), stack depth limit and operations count limit. Beware that a bad script may panic the entire system! |
| `no_function` | Disable script-defined functions if not needed. | | `no_function` | Disable script-defined functions if not needed. |
| `no_index` | Disable [arrays] and indexing features if not needed. | | `no_index` | Disable [arrays] and indexing features if not needed. |
| `no_object` | Disable support for custom types and objects. | | `no_object` | Disable support for custom types and objects. |
| `no_float` | Disable floating-point numbers and math if not needed. | | `no_float` | Disable floating-point numbers and math if not needed. |
| `no_optimize` | Disable the script optimizer. | | `no_optimize` | Disable the script optimizer. |
| `no_module` | Disable modules. | | `no_module` | Disable modules. |
| `only_i32` | Set the system integer type to `i32` and disable all other integer types. `INT` is set to `i32`. | | `only_i32` | Set the system integer type to `i32` and disable all other integer types. `INT` is set to `i32`. |
| `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. | | `only_i64` | Set the system integer type to `i64` and disable all other integer types. `INT` is set to `i64`. |
| `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. | | `no_std` | Build for `no-std`. Notice that additional dependencies will be pulled in to replace `std` features. |
| `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, [`Engine`], [`Scope`] and `AST` are all `Send + Sync`. | | `sync` | Restrict all values types to those that are `Send + Sync`. Under this feature, [`Engine`], [`Scope`] and `AST` are all `Send + Sync`. |
By default, Rhai includes all the standard functionalities in a small, tight package. By default, Rhai includes all the standard functionalities in a small, tight package.
Most features are here to opt-**out** of certain functionalities that are not needed. Most features are here to opt-**out** of certain functionalities that are not needed.
@ -269,7 +273,7 @@ let ast = engine.compile_file("hello_world.rhai".into())?;
### Calling Rhai functions from Rust ### Calling Rhai functions from Rust
Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust via `call_fn`. Rhai also allows working _backwards_ from the other direction - i.e. calling a Rhai-scripted function from Rust via `Engine::call_fn`.
Functions declared with `private` are hidden and cannot be called from Rust (see also [modules]). Functions declared with `private` are hidden and cannot be called from Rust (see also [modules]).
```rust ```rust
@ -372,7 +376,7 @@ Use `Engine::new_raw` to create a _raw_ `Engine`, in which _nothing_ is added, n
### Packages ### Packages
Rhai functional features are provided in different _packages_ that can be loaded via a call to `load_package`. Rhai functional features are provided in different _packages_ that can be loaded via a call to `Engine::load_package`.
Packages reside under `rhai::packages::*` and the trait `rhai::packages::Package` must be loaded in order for Packages reside under `rhai::packages::*` and the trait `rhai::packages::Package` must be loaded in order for
packages to be used. packages to be used.
@ -759,7 +763,7 @@ Because they [_short-circuit_](#boolean-operators), `&&` and `||` are handled sp
overriding them has no effect at all. overriding them has no effect at all.
Operator functions cannot be defined as a script function (because operators syntax are not valid function names). Operator functions cannot be defined as a script function (because operators syntax are not valid function names).
However, operator functions _can_ be registered to the [`Engine`] via `register_fn`, `register_result_fn` etc. However, operator functions _can_ be registered to the [`Engine`] via the methods `Engine::register_fn`, `Engine::register_result_fn` etc.
When a custom operator function is registered with the same name as an operator, it _overloads_ (or overrides) the built-in version. When a custom operator function is registered with the same name as an operator, it _overloads_ (or overrides) the built-in version.
```rust ```rust
@ -2043,7 +2047,7 @@ debug("world!"); // prints "world!" to stdout using debug formatting
### Overriding `print` and `debug` with callback functions ### Overriding `print` and `debug` with callback functions
When embedding Rhai into an application, it is usually necessary to trap `print` and `debug` output When embedding Rhai into an application, it is usually necessary to trap `print` and `debug` output
(for logging into a tracking log, for example). (for logging into a tracking log, for example) with the `Engine::on_print` and `Engine::on_debug` methods:
```rust ```rust
// Any function or closure that takes an '&str' argument can be used to override // Any function or closure that takes an '&str' argument can be used to override
@ -2237,7 +2241,7 @@ Built-in module resolvers are grouped under the `rhai::module_resolvers` module
| `FileModuleResolver` | The default module resolution service, not available under the [`no_std`] feature. Loads a script file (based off the current directory) with `.rhai` extension.<br/>The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.<br/>`FileModuleResolver::create_module()` loads a script file and returns a module. | | `FileModuleResolver` | The default module resolution service, not available under the [`no_std`] feature. Loads a script file (based off the current directory) with `.rhai` extension.<br/>The base directory can be changed via the `FileModuleResolver::new_with_path()` constructor function.<br/>`FileModuleResolver::create_module()` loads a script file and returns a module. |
| `StaticModuleResolver` | Loads modules that are statically added. This can be used when the [`no_std`] feature is turned on. | | `StaticModuleResolver` | Loads modules that are statically added. This can be used when the [`no_std`] feature is turned on. |
An [`Engine`]'s module resolver is set via a call to `set_module_resolver`: An [`Engine`]'s module resolver is set via a call to `Engine::set_module_resolver`:
```rust ```rust
// Use the 'StaticModuleResolver' // Use the 'StaticModuleResolver'
@ -2248,6 +2252,112 @@ engine.set_module_resolver(Some(resolver));
engine.set_module_resolver(None); engine.set_module_resolver(None);
``` ```
Ruggedization - protect against DoS attacks
------------------------------------------
For scripting systems open to user-land scripts, it is always best to limit the amount of resources used by a script
so that it does not crash the system by consuming all resources.
The most important resources to watch out for are:
* **Memory**: A malignant script may continuously grow an [array] or [object map] until all memory is consumed.
* **CPU**: A malignant script may run an infinite tight loop that consumes all CPU cycles.
* **Time**: A malignant script may run indefinitely, thereby blocking the calling system waiting for a result.
* **Stack**: A malignant script may consume attempt an infinite recursive call that exhausts the call stack.
* **Overflows**: A malignant script may deliberately cause numeric over-flows and/or under-flows, and/or bad
floating-point representations, in order to crash the system.
* **Files**: A malignant script may continuously `import` an external module within an infinite loop,
thereby putting heavy load on the file-system (or even the network if the file is not local).
* **Data**: A malignant script may attempt to read from and/or write to data that it does not own. If this happens,
it is a severe security breach and may put the entire system at risk.
### Maximum number of operations
Rhai by default does not limit how much time or CPU a script consumes.
This can be changed via the `Engine::set_max_operations` method, with zero being unlimited (the default).
```rust
let mut engine = Engine::new();
engine.set_max_operations(500); // allow only up to 500 operations for this script
engine.set_max_operations(0); // allow unlimited operations
```
The concept of one single _operation_ in Rhai is volatile - it roughly equals one expression node,
one statement, one iteration of a loop, or one function call etc. with sub-expressions and statement
blocks executed inside these contexts accumulated on top.
One _operation_ can take an unspecified amount of time and CPU cycles, depending on the particular operation
involved. For example, loading a constant consumes very few CPU cycles, while calling an external Rust function,
though also counted as only one operation, may consume much more resources.
The _operation count_ is intended to be a very course-grained measurement of the amount of CPU that a script
is consuming, and allows the system to impose a hard upper limit.
A script exceeding the maximum operations count will terminate with an error result.
This check can be disabled via the [`unchecked`] feature for higher performance
(but higher risks as well).
### Tracking progress
To track script evaluation progress and to force-terminate a script prematurely (for any reason),
provide a closure to the `Engine::on_progress` method:
```rust
let mut engine = Engine::new();
engine.on_progress(|count| { // 'count' is the number of operations performed
if count % 1000 == 0 {
println!("{}", count); // print out a progress log every 1,000 operations
}
true // return 'true' to continue the script
// returning 'false' will terminate the script
});
```
The closure passed to `Engine::on_progress` will be called once every operation.
Return `false` to terminate the script immediately.
### Maximum stack depth
Rhai by default limits function calls to a maximum depth of 256 levels (28 levels in debug build).
This limit may be changed via the `Engine::set_max_call_levels` method.
The limit can be disabled via the [`unchecked`] feature for higher performance
(but higher risks as well).
```rust
let mut engine = Engine::new();
engine.set_max_call_levels(10); // allow only up to 10 levels of function calls
engine.set_max_call_levels(0); // allow no function calls at all (max depth = zero)
```
A script exceeding the maximum call stack depth will terminate with an error result.
### Checked arithmetic
All arithmetic calculations in Rhai are _checked_, meaning that the script terminates with an error whenever
it detects a numeric over-flow/under-flow condition or an invalid floating-point operation, instead of
crashing the entire system. This checking can be turned off via the [`unchecked`] feature for higher performance
(but higher risks as well).
### Access to external data
Rhai is _sand-boxed_ so a script can never read from outside its own environment.
Furthermore, an [`Engine`] created non-`mut` cannot mutate any state outside of itself;
so it is highly recommended that [`Engine`]'s are created immutable as much as possible.
```rust
let mut engine = Engine::new(); // create mutable 'Engine'
engine.register_get("add", add); // configure 'engine'
let engine = engine; // shadow the variable so that 'engine' is now immutable
```
Script optimization Script optimization
=================== ===================
@ -2358,7 +2468,7 @@ There are actually three levels of optimizations: `None`, `Simple` and `Full`.
* `Full` is _much_ more aggressive, _including_ running functions on constant arguments to determine their result. * `Full` is _much_ more aggressive, _including_ running functions on constant arguments to determine their result.
One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators. One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators.
An [`Engine`]'s optimization level is set via a call to `set_optimization_level`: An [`Engine`]'s optimization level is set via a call to `Engine::set_optimization_level`:
```rust ```rust
// Turn on aggressive optimizations // Turn on aggressive optimizations

View File

@ -865,7 +865,7 @@ impl Engine {
scope: &mut Scope, scope: &mut Scope,
ast: &AST, ast: &AST,
) -> Result<T, Box<EvalAltResult>> { ) -> Result<T, Box<EvalAltResult>> {
let result = self.eval_ast_with_scope_raw(scope, ast)?; let (result, _) = self.eval_ast_with_scope_raw(scope, ast)?;
let return_type = self.map_type_name(result.type_name()); let return_type = self.map_type_name(result.type_name());
@ -881,7 +881,7 @@ impl Engine {
&self, &self,
scope: &mut Scope, scope: &mut Scope,
ast: &AST, ast: &AST,
) -> Result<Dynamic, Box<EvalAltResult>> { ) -> Result<(Dynamic, u64), Box<EvalAltResult>> {
let mut state = State::new(ast.fn_lib()); let mut state = State::new(ast.fn_lib());
ast.statements() ast.statements()
@ -893,6 +893,7 @@ impl Engine {
EvalAltResult::Return(out, _) => Ok(out), EvalAltResult::Return(out, _) => Ok(out),
_ => Err(err), _ => Err(err),
}) })
.map(|v| (v, state.operations))
} }
/// Evaluate a file, but throw away the result and only return error (if any). /// Evaluate a file, but throw away the result and only return error (if any).
@ -1015,10 +1016,11 @@ impl Engine {
.get_function_by_signature(name, args.len(), true) .get_function_by_signature(name, args.len(), true)
.ok_or_else(|| Box::new(EvalAltResult::ErrorFunctionNotFound(name.into(), pos)))?; .ok_or_else(|| Box::new(EvalAltResult::ErrorFunctionNotFound(name.into(), pos)))?;
let state = State::new(fn_lib); let mut state = State::new(fn_lib);
let args = args.as_mut();
let result = let (result, _) =
self.call_script_fn(Some(scope), &state, name, fn_def, args.as_mut(), pos, 0)?; self.call_script_fn(Some(scope), &mut state, name, fn_def, args, pos, 0, 0)?;
let return_type = self.map_type_name(result.type_name()); let return_type = self.map_type_name(result.type_name());
@ -1058,6 +1060,84 @@ impl Engine {
optimize_into_ast(self, scope, stmt, fn_lib, optimization_level) optimize_into_ast(self, scope, stmt, fn_lib, optimization_level)
} }
/// Register a callback for script evaluation progress.
///
/// # Example
///
/// ```
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
/// # use std::sync::RwLock;
/// # use std::sync::Arc;
/// use rhai::Engine;
///
/// let result = Arc::new(RwLock::new(0_u64));
/// let logger = result.clone();
///
/// let mut engine = Engine::new();
///
/// engine.on_progress(move |ops| {
/// if ops > 10000 {
/// false
/// } else if ops % 800 == 0 {
/// *logger.write().unwrap() = ops;
/// true
/// } else {
/// true
/// }
/// });
///
/// engine.consume("for x in range(0, 50000) {}")
/// .expect_err("should error");
///
/// assert_eq!(*result.read().unwrap(), 9600);
///
/// # Ok(())
/// # }
/// ```
#[cfg(feature = "sync")]
pub fn on_progress(&mut self, callback: impl Fn(u64) -> bool + Send + Sync + 'static) {
self.progress = Some(Box::new(callback));
}
/// Register a callback for script evaluation progress.
///
/// # Example
///
/// ```
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
/// # use std::cell::Cell;
/// # use std::rc::Rc;
/// use rhai::Engine;
///
/// let result = Rc::new(Cell::new(0_u64));
/// let logger = result.clone();
///
/// let mut engine = Engine::new();
///
/// engine.on_progress(move |ops| {
/// if ops > 10000 {
/// false
/// } else if ops % 800 == 0 {
/// logger.set(ops);
/// true
/// } else {
/// true
/// }
/// });
///
/// engine.consume("for x in range(0, 50000) {}")
/// .expect_err("should error");
///
/// assert_eq!(result.get(), 9600);
///
/// # Ok(())
/// # }
/// ```
#[cfg(not(feature = "sync"))]
pub fn on_progress(&mut self, callback: impl Fn(u64) -> bool + 'static) {
self.progress = Some(Box::new(callback));
}
/// Override default action of `print` (print to stdout using `println!`) /// Override default action of `print` (print to stdout using `println!`)
/// ///
/// # Example /// # Example
@ -1092,21 +1172,21 @@ impl Engine {
/// ///
/// ``` /// ```
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> { /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
/// # use std::sync::RwLock; /// # use std::cell::RefCell;
/// # use std::sync::Arc; /// # use std::rc::Rc;
/// use rhai::Engine; /// use rhai::Engine;
/// ///
/// let result = Arc::new(RwLock::new(String::from(""))); /// let result = Rc::new(RefCell::new(String::from("")));
/// ///
/// let mut engine = Engine::new(); /// let mut engine = Engine::new();
/// ///
/// // Override action of 'print' function /// // Override action of 'print' function
/// let logger = result.clone(); /// let logger = result.clone();
/// engine.on_print(move |s| logger.write().unwrap().push_str(s)); /// engine.on_print(move |s| logger.borrow_mut().push_str(s));
/// ///
/// engine.consume("print(40 + 2);")?; /// engine.consume("print(40 + 2);")?;
/// ///
/// assert_eq!(*result.read().unwrap(), "42"); /// assert_eq!(*result.borrow(), "42");
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
@ -1149,21 +1229,21 @@ impl Engine {
/// ///
/// ``` /// ```
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> { /// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
/// # use std::sync::RwLock; /// # use std::cell::RefCell;
/// # use std::sync::Arc; /// # use std::rc::Rc;
/// use rhai::Engine; /// use rhai::Engine;
/// ///
/// let result = Arc::new(RwLock::new(String::from(""))); /// let result = Rc::new(RefCell::new(String::from("")));
/// ///
/// let mut engine = Engine::new(); /// let mut engine = Engine::new();
/// ///
/// // Override action of 'print' function /// // Override action of 'print' function
/// let logger = result.clone(); /// let logger = result.clone();
/// engine.on_debug(move |s| logger.write().unwrap().push_str(s)); /// engine.on_debug(move |s| logger.borrow_mut().push_str(s));
/// ///
/// engine.consume(r#"debug("hello");"#)?; /// engine.consume(r#"debug("hello");"#)?;
/// ///
/// assert_eq!(*result.read().unwrap(), r#""hello""#); /// assert_eq!(*result.borrow(), r#""hello""#);
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```

View File

@ -3,7 +3,7 @@
use crate::any::{Dynamic, Union}; use crate::any::{Dynamic, Union};
use crate::calc_fn_hash; use crate::calc_fn_hash;
use crate::error::ParseErrorType; use crate::error::ParseErrorType;
use crate::fn_native::{FnCallArgs, NativeFunctionABI, PrintCallback}; use crate::fn_native::{FnCallArgs, NativeFunctionABI, PrintCallback, ProgressCallback};
use crate::module::Module; use crate::module::Module;
use crate::optimize::OptimizationLevel; use crate::optimize::OptimizationLevel;
use crate::packages::{CorePackage, Package, PackageLibrary, PackagesCollection, StandardPackage}; use crate::packages::{CorePackage, Package, PackageLibrary, PackagesCollection, StandardPackage};
@ -28,7 +28,7 @@ use crate::stdlib::{
format, format,
iter::{empty, once, repeat}, iter::{empty, once, repeat},
mem, mem,
num::NonZeroUsize, num::{NonZeroU64, NonZeroUsize},
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
rc::Rc, rc::Rc,
string::{String, ToString}, string::{String, ToString},
@ -48,12 +48,17 @@ pub type Array = Vec<Dynamic>;
#[cfg(not(feature = "no_object"))] #[cfg(not(feature = "no_object"))]
pub type Map = HashMap<String, Dynamic>; pub type Map = HashMap<String, Dynamic>;
#[cfg(not(feature = "unchecked"))]
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub const MAX_CALL_STACK_DEPTH: usize = 28; pub const MAX_CALL_STACK_DEPTH: usize = 28;
#[cfg(not(feature = "unchecked"))]
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
pub const MAX_CALL_STACK_DEPTH: usize = 256; pub const MAX_CALL_STACK_DEPTH: usize = 256;
#[cfg(feature = "unchecked")]
pub const MAX_CALL_STACK_DEPTH: usize = usize::MAX;
pub const KEYWORD_PRINT: &str = "print"; pub const KEYWORD_PRINT: &str = "print";
pub const KEYWORD_DEBUG: &str = "debug"; pub const KEYWORD_DEBUG: &str = "debug";
pub const KEYWORD_TYPE_OF: &str = "type_of"; pub const KEYWORD_TYPE_OF: &str = "type_of";
@ -146,6 +151,9 @@ pub struct State<'a> {
/// Level of the current scope. The global (root) level is zero, a new block (or function call) /// Level of the current scope. The global (root) level is zero, a new block (or function call)
/// is one level higher, and so on. /// is one level higher, and so on.
pub scope_level: usize, pub scope_level: usize,
/// Number of operations performed.
pub operations: u64,
} }
impl<'a> State<'a> { impl<'a> State<'a> {
@ -155,6 +163,7 @@ impl<'a> State<'a> {
always_search: false, always_search: false,
fn_lib, fn_lib,
scope_level: 0, scope_level: 0,
operations: 0,
} }
} }
/// Does a certain script-defined function exist in the `State`? /// Does a certain script-defined function exist in the `State`?
@ -304,6 +313,8 @@ pub struct Engine {
pub(crate) print: Box<PrintCallback>, pub(crate) print: Box<PrintCallback>,
/// Closure for implementing the `debug` command. /// Closure for implementing the `debug` command.
pub(crate) debug: Box<PrintCallback>, pub(crate) debug: Box<PrintCallback>,
/// Closure for progress reporting.
pub(crate) progress: Option<Box<ProgressCallback>>,
/// Optimize the AST after compilation. /// Optimize the AST after compilation.
pub(crate) optimization_level: OptimizationLevel, pub(crate) optimization_level: OptimizationLevel,
@ -311,6 +322,8 @@ pub struct Engine {
/// ///
/// Defaults to 28 for debug builds and 256 for non-debug builds. /// Defaults to 28 for debug builds and 256 for non-debug builds.
pub(crate) max_call_stack_depth: usize, pub(crate) max_call_stack_depth: usize,
/// Maximum number of operations to run.
pub(crate) max_operations: Option<NonZeroU64>,
} }
impl Default for Engine { impl Default for Engine {
@ -333,6 +346,9 @@ impl Default for Engine {
print: Box::new(default_print), print: Box::new(default_print),
debug: Box::new(default_print), debug: Box::new(default_print),
// progress callback
progress: None,
// optimization level // optimization level
#[cfg(feature = "no_optimize")] #[cfg(feature = "no_optimize")]
optimization_level: OptimizationLevel::None, optimization_level: OptimizationLevel::None,
@ -346,6 +362,7 @@ impl Default for Engine {
optimization_level: OptimizationLevel::Full, optimization_level: OptimizationLevel::Full,
max_call_stack_depth: MAX_CALL_STACK_DEPTH, max_call_stack_depth: MAX_CALL_STACK_DEPTH,
max_operations: None,
}; };
#[cfg(feature = "no_stdlib")] #[cfg(feature = "no_stdlib")]
@ -471,6 +488,7 @@ impl Engine {
type_names: Default::default(), type_names: Default::default(),
print: Box::new(|_| {}), print: Box::new(|_| {}),
debug: Box::new(|_| {}), debug: Box::new(|_| {}),
progress: None,
#[cfg(feature = "no_optimize")] #[cfg(feature = "no_optimize")]
optimization_level: OptimizationLevel::None, optimization_level: OptimizationLevel::None,
@ -484,6 +502,7 @@ impl Engine {
optimization_level: OptimizationLevel::Full, optimization_level: OptimizationLevel::Full,
max_call_stack_depth: MAX_CALL_STACK_DEPTH, max_call_stack_depth: MAX_CALL_STACK_DEPTH,
max_operations: None,
} }
} }
@ -515,10 +534,17 @@ impl Engine {
/// Set the maximum levels of function calls allowed for a script in order to avoid /// Set the maximum levels of function calls allowed for a script in order to avoid
/// infinite recursion and stack overflows. /// infinite recursion and stack overflows.
#[cfg(not(feature = "unchecked"))]
pub fn set_max_call_levels(&mut self, levels: usize) { pub fn set_max_call_levels(&mut self, levels: usize) {
self.max_call_stack_depth = levels self.max_call_stack_depth = levels
} }
/// Set the maximum number of operations allowed for a script to run to avoid
/// consuming too much resources (0 for unlimited).
#[cfg(not(feature = "unchecked"))]
pub fn set_max_operations(&mut self, operations: u64) {
self.max_operations = NonZeroU64::new(operations);
}
/// Set the module resolution service used by the `Engine`. /// Set the module resolution service used by the `Engine`.
/// ///
/// Not available under the `no_module` feature. /// Not available under the `no_module` feature.
@ -537,7 +563,7 @@ impl Engine {
pub(crate) fn call_fn_raw( pub(crate) fn call_fn_raw(
&self, &self,
scope: Option<&mut Scope>, scope: Option<&mut Scope>,
state: &State, state: &mut State,
fn_name: &str, fn_name: &str,
hashes: (u64, u64), hashes: (u64, u64),
args: &mut FnCallArgs, args: &mut FnCallArgs,
@ -546,6 +572,8 @@ impl Engine {
pos: Position, pos: Position,
level: usize, level: usize,
) -> Result<(Dynamic, bool), Box<EvalAltResult>> { ) -> Result<(Dynamic, bool), Box<EvalAltResult>> {
self.inc_operations(state, pos)?;
// Check for stack overflow // Check for stack overflow
if level > self.max_call_stack_depth { if level > self.max_call_stack_depth {
return Err(Box::new(EvalAltResult::ErrorStackOverflow(pos))); return Err(Box::new(EvalAltResult::ErrorStackOverflow(pos)));
@ -554,9 +582,12 @@ impl Engine {
// First search in script-defined functions (can override built-in) // First search in script-defined functions (can override built-in)
if hashes.1 > 0 { if hashes.1 > 0 {
if let Some(fn_def) = state.get_function(hashes.1) { if let Some(fn_def) = state.get_function(hashes.1) {
return self let ops = state.operations;
.call_script_fn(scope, state, fn_name, fn_def, args, pos, level) let (result, operations) =
.map(|v| (v, false)); self.call_script_fn(scope, &state, fn_name, fn_def, args, pos, level, ops)?;
state.operations = operations;
self.inc_operations(state, pos)?;
return Ok((result, false));
} }
} }
@ -677,14 +708,17 @@ impl Engine {
args: &mut FnCallArgs, args: &mut FnCallArgs,
pos: Position, pos: Position,
level: usize, level: usize,
) -> Result<Dynamic, Box<EvalAltResult>> { operations: u64,
) -> Result<(Dynamic, u64), Box<EvalAltResult>> {
match scope { match scope {
// Extern scope passed in which is not empty // Extern scope passed in which is not empty
Some(scope) if scope.len() > 0 => { Some(scope) if scope.len() > 0 => {
let scope_len = scope.len(); let scope_len = scope.len();
let mut state = State::new(state.fn_lib); let mut local_state = State::new(state.fn_lib);
state.scope_level += 1; local_state.operations = operations;
self.inc_operations(&mut local_state, pos)?;
local_state.scope_level += 1;
// Put arguments into scope as variables // Put arguments into scope as variables
scope.extend( scope.extend(
@ -696,14 +730,14 @@ impl Engine {
args.into_iter().map(|v| mem::take(*v)), args.into_iter().map(|v| mem::take(*v)),
) )
.map(|(name, value)| { .map(|(name, value)| {
let var_name = unsafe_cast_var_name(name.as_str(), &state); let var_name = unsafe_cast_var_name(name.as_str(), &local_state);
(var_name, ScopeEntryType::Normal, value) (var_name, ScopeEntryType::Normal, value)
}), }),
); );
// Evaluate the function at one higher level of call depth // Evaluate the function at one higher level of call depth
let result = self let result = self
.eval_stmt(scope, &mut state, &fn_def.body, level + 1) .eval_stmt(scope, &mut local_state, &fn_def.body, level + 1)
.or_else(|err| match *err { .or_else(|err| match *err {
// Convert return statement to return value // Convert return statement to return value
EvalAltResult::Return(x, _) => Ok(x), EvalAltResult::Return(x, _) => Ok(x),
@ -725,13 +759,16 @@ impl Engine {
// No need to reset `state.scope_level` because it is thrown away // No need to reset `state.scope_level` because it is thrown away
scope.rewind(scope_len); scope.rewind(scope_len);
return result; return result.map(|v| (v, local_state.operations));
} }
// No new scope - create internal scope // No new scope - create internal scope
_ => { _ => {
let mut scope = Scope::new(); let mut scope = Scope::new();
let mut state = State::new(state.fn_lib); let mut local_state = State::new(state.fn_lib);
state.scope_level += 1;
local_state.operations = operations;
self.inc_operations(&mut local_state, pos)?;
local_state.scope_level += 1;
// Put arguments into scope as variables // Put arguments into scope as variables
scope.extend( scope.extend(
@ -748,7 +785,7 @@ impl Engine {
// Evaluate the function at one higher level of call depth // Evaluate the function at one higher level of call depth
// No need to reset `state.scope_level` because it is thrown away // No need to reset `state.scope_level` because it is thrown away
return self return self
.eval_stmt(&mut scope, &mut state, &fn_def.body, level + 1) .eval_stmt(&mut scope, &mut local_state, &fn_def.body, level + 1)
.or_else(|err| match *err { .or_else(|err| match *err {
// Convert return statement to return value // Convert return statement to return value
EvalAltResult::Return(x, _) => Ok(x), EvalAltResult::Return(x, _) => Ok(x),
@ -764,7 +801,8 @@ impl Engine {
err, err,
pos, pos,
))), ))),
}); })
.map(|v| (v, local_state.operations));
} }
} }
} }
@ -788,7 +826,7 @@ impl Engine {
/// **DO NOT** reuse the argument values unless for the first `&mut` argument - all others are silently replaced by `()`! /// **DO NOT** reuse the argument values unless for the first `&mut` argument - all others are silently replaced by `()`!
fn exec_fn_call( fn exec_fn_call(
&self, &self,
state: &State, state: &mut State,
fn_name: &str, fn_name: &str,
hash_fn_def: u64, hash_fn_def: u64,
args: &mut FnCallArgs, args: &mut FnCallArgs,
@ -827,7 +865,7 @@ impl Engine {
fn eval_script_expr( fn eval_script_expr(
&self, &self,
scope: &mut Scope, scope: &mut Scope,
state: &State, state: &mut State,
script: &Dynamic, script: &Dynamic,
pos: Position, pos: Position,
) -> Result<Dynamic, Box<EvalAltResult>> { ) -> Result<Dynamic, Box<EvalAltResult>> {
@ -854,14 +892,20 @@ impl Engine {
let ast = AST::new(statements, state.fn_lib.clone()); let ast = AST::new(statements, state.fn_lib.clone());
// Evaluate the AST // Evaluate the AST
self.eval_ast_with_scope_raw(scope, &ast) let (result, operations) = self
.map_err(|err| err.new_position(pos)) .eval_ast_with_scope_raw(scope, &ast)
.map_err(|err| err.new_position(pos))?;
state.operations += operations;
self.inc_operations(state, pos)?;
return Ok(result);
} }
/// Chain-evaluate a dot/index chain. /// Chain-evaluate a dot/index chain.
fn eval_dot_index_chain_helper( fn eval_dot_index_chain_helper(
&self, &self,
state: &State, state: &mut State,
mut target: Target, mut target: Target,
rhs: &Expr, rhs: &Expr,
idx_values: &mut StaticVec<Dynamic>, idx_values: &mut StaticVec<Dynamic>,
@ -1145,7 +1189,7 @@ impl Engine {
/// Get the value at the indexed position of a base type /// Get the value at the indexed position of a base type
fn get_indexed_mut<'a>( fn get_indexed_mut<'a>(
&self, &self,
state: &State, state: &mut State,
val: &'a mut Dynamic, val: &'a mut Dynamic,
is_ref: bool, is_ref: bool,
mut idx: Dynamic, mut idx: Dynamic,
@ -1153,6 +1197,8 @@ impl Engine {
op_pos: Position, op_pos: Position,
create: bool, create: bool,
) -> Result<Target<'a>, Box<EvalAltResult>> { ) -> Result<Target<'a>, Box<EvalAltResult>> {
self.inc_operations(state, op_pos)?;
match val { match val {
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
Dynamic(Union::Array(arr)) => { Dynamic(Union::Array(arr)) => {
@ -1234,6 +1280,8 @@ impl Engine {
rhs: &Expr, rhs: &Expr,
level: usize, level: usize,
) -> Result<Dynamic, Box<EvalAltResult>> { ) -> Result<Dynamic, Box<EvalAltResult>> {
self.inc_operations(state, rhs.position())?;
let mut lhs_value = self.eval_expr(scope, state, lhs, level)?; let mut lhs_value = self.eval_expr(scope, state, lhs, level)?;
let rhs_value = self.eval_expr(scope, state, rhs, level)?; let rhs_value = self.eval_expr(scope, state, rhs, level)?;
@ -1288,6 +1336,8 @@ impl Engine {
expr: &Expr, expr: &Expr,
level: usize, level: usize,
) -> Result<Dynamic, Box<EvalAltResult>> { ) -> Result<Dynamic, Box<EvalAltResult>> {
self.inc_operations(state, expr.position())?;
match expr { match expr {
Expr::IntegerConstant(x) => Ok(x.0.into()), Expr::IntegerConstant(x) => Ok(x.0.into()),
#[cfg(not(feature = "no_float"))] #[cfg(not(feature = "no_float"))]
@ -1461,9 +1511,16 @@ impl Engine {
// First search in script-defined functions (can override built-in) // First search in script-defined functions (can override built-in)
if let Some(fn_def) = module.get_qualified_scripted_fn(*hash_fn_def) { if let Some(fn_def) = module.get_qualified_scripted_fn(*hash_fn_def) {
self.call_script_fn(None, state, name, fn_def, args.as_mut(), *pos, level) let args = args.as_mut();
let ops = state.operations;
let (result, operations) =
self.call_script_fn(None, state, name, fn_def, args, *pos, level, ops)?;
state.operations = operations;
self.inc_operations(state, *pos)?;
Ok(result)
} else { } else {
// Then search in Rust functions // Then search in Rust functions
self.inc_operations(state, *pos)?;
// Rust functions are indexed in two steps: // Rust functions are indexed in two steps:
// 1) Calculate a hash in a similar manner to script-defined functions, // 1) Calculate a hash in a similar manner to script-defined functions,
@ -1538,6 +1595,8 @@ impl Engine {
stmt: &Stmt, stmt: &Stmt,
level: usize, level: usize,
) -> Result<Dynamic, Box<EvalAltResult>> { ) -> Result<Dynamic, Box<EvalAltResult>> {
self.inc_operations(state, stmt.position())?;
match stmt { match stmt {
// No-op // No-op
Stmt::Noop(_) => Ok(Default::default()), Stmt::Noop(_) => Ok(Default::default()),
@ -1546,11 +1605,10 @@ impl Engine {
Stmt::Expr(expr) => { Stmt::Expr(expr) => {
let result = self.eval_expr(scope, state, expr, level)?; let result = self.eval_expr(scope, state, expr, level)?;
Ok(if let Expr::Assignment(_) = *expr.as_ref() { Ok(match expr.as_ref() {
// If it is an assignment, erase the result at the root // If it is an assignment, erase the result at the root
Default::default() Expr::Assignment(_) => Default::default(),
} else { _ => result,
result
}) })
} }
@ -1574,25 +1632,32 @@ impl Engine {
} }
// If-else statement // If-else statement
Stmt::IfThenElse(x) => self Stmt::IfThenElse(x) => {
.eval_expr(scope, state, &x.0, level)? let (expr, if_block, else_block) = x.as_ref();
.as_bool()
.map_err(|_| Box::new(EvalAltResult::ErrorLogicGuard(x.0.position()))) self.eval_expr(scope, state, expr, level)?
.and_then(|guard_val| { .as_bool()
if guard_val { .map_err(|_| Box::new(EvalAltResult::ErrorLogicGuard(expr.position())))
self.eval_stmt(scope, state, &x.1, level) .and_then(|guard_val| {
} else if let Some(stmt) = &x.2 { if guard_val {
self.eval_stmt(scope, state, stmt, level) self.eval_stmt(scope, state, if_block, level)
} else { } else if let Some(stmt) = else_block {
Ok(Default::default()) self.eval_stmt(scope, state, stmt, level)
} } else {
}), Ok(Default::default())
}
})
}
// While loop // While loop
Stmt::While(x) => loop { Stmt::While(x) => loop {
match self.eval_expr(scope, state, &x.0, level)?.as_bool() { let (expr, body) = x.as_ref();
Ok(true) => match self.eval_stmt(scope, state, &x.1, level) {
Ok(_) => (), match self.eval_expr(scope, state, expr, level)?.as_bool() {
Ok(true) => match self.eval_stmt(scope, state, body, level) {
Ok(_) => {
self.inc_operations(state, body.position())?;
}
Err(err) => match *err { Err(err) => match *err {
EvalAltResult::ErrorLoopBreak(false, _) => (), EvalAltResult::ErrorLoopBreak(false, _) => (),
EvalAltResult::ErrorLoopBreak(true, _) => return Ok(Default::default()), EvalAltResult::ErrorLoopBreak(true, _) => return Ok(Default::default()),
@ -1600,14 +1665,18 @@ impl Engine {
}, },
}, },
Ok(false) => return Ok(Default::default()), Ok(false) => return Ok(Default::default()),
Err(_) => return Err(Box::new(EvalAltResult::ErrorLogicGuard(x.0.position()))), Err(_) => {
return Err(Box::new(EvalAltResult::ErrorLogicGuard(expr.position())))
}
} }
}, },
// Loop statement // Loop statement
Stmt::Loop(body) => loop { Stmt::Loop(body) => loop {
match self.eval_stmt(scope, state, body, level) { match self.eval_stmt(scope, state, body, level) {
Ok(_) => (), Ok(_) => {
self.inc_operations(state, body.position())?;
}
Err(err) => match *err { Err(err) => match *err {
EvalAltResult::ErrorLoopBreak(false, _) => (), EvalAltResult::ErrorLoopBreak(false, _) => (),
EvalAltResult::ErrorLoopBreak(true, _) => return Ok(Default::default()), EvalAltResult::ErrorLoopBreak(true, _) => return Ok(Default::default()),
@ -1618,7 +1687,8 @@ impl Engine {
// For loop // For loop
Stmt::For(x) => { Stmt::For(x) => {
let iter_type = self.eval_expr(scope, state, &x.1, level)?; let (name, expr, stmt) = x.as_ref();
let iter_type = self.eval_expr(scope, state, expr, level)?;
let tid = iter_type.type_id(); let tid = iter_type.type_id();
if let Some(iter_fn) = self if let Some(iter_fn) = self
@ -1627,15 +1697,16 @@ impl Engine {
.or_else(|| self.packages.get_iter(tid)) .or_else(|| self.packages.get_iter(tid))
{ {
// Add the loop variable // Add the loop variable
let var_name = unsafe_cast_var_name(&x.0, &state); let var_name = unsafe_cast_var_name(name, &state);
scope.push(var_name, ()); scope.push(var_name, ());
let index = scope.len() - 1; let index = scope.len() - 1;
state.scope_level += 1; state.scope_level += 1;
for loop_var in iter_fn(iter_type) { for loop_var in iter_fn(iter_type) {
*scope.get_mut(index).0 = loop_var; *scope.get_mut(index).0 = loop_var;
self.inc_operations(state, stmt.position())?;
match self.eval_stmt(scope, state, &x.2, level) { match self.eval_stmt(scope, state, stmt, level) {
Ok(_) => (), Ok(_) => (),
Err(err) => match *err { Err(err) => match *err {
EvalAltResult::ErrorLoopBreak(false, _) => (), EvalAltResult::ErrorLoopBreak(false, _) => (),
@ -1690,26 +1761,29 @@ impl Engine {
// Let statement // Let statement
Stmt::Let(x) if x.1.is_some() => { Stmt::Let(x) if x.1.is_some() => {
let ((var_name, _), expr) = x.as_ref(); let ((var_name, pos), expr) = x.as_ref();
let val = self.eval_expr(scope, state, expr.as_ref().unwrap(), level)?; let val = self.eval_expr(scope, state, expr.as_ref().unwrap(), level)?;
let var_name = unsafe_cast_var_name(var_name, &state); let var_name = unsafe_cast_var_name(var_name, &state);
scope.push_dynamic_value(var_name, ScopeEntryType::Normal, val, false); scope.push_dynamic_value(var_name, ScopeEntryType::Normal, val, false);
self.inc_operations(state, *pos)?;
Ok(Default::default()) Ok(Default::default())
} }
Stmt::Let(x) => { Stmt::Let(x) => {
let ((var_name, _), _) = x.as_ref(); let ((var_name, pos), _) = x.as_ref();
let var_name = unsafe_cast_var_name(var_name, &state); let var_name = unsafe_cast_var_name(var_name, &state);
scope.push(var_name, ()); scope.push(var_name, ());
self.inc_operations(state, *pos)?;
Ok(Default::default()) Ok(Default::default())
} }
// Const statement // Const statement
Stmt::Const(x) if x.1.is_constant() => { Stmt::Const(x) if x.1.is_constant() => {
let ((var_name, _), expr) = x.as_ref(); let ((var_name, pos), expr) = x.as_ref();
let val = self.eval_expr(scope, state, &expr, level)?; let val = self.eval_expr(scope, state, &expr, level)?;
let var_name = unsafe_cast_var_name(var_name, &state); let var_name = unsafe_cast_var_name(var_name, &state);
scope.push_dynamic_value(var_name, ScopeEntryType::Constant, val, true); scope.push_dynamic_value(var_name, ScopeEntryType::Constant, val, true);
self.inc_operations(state, *pos)?;
Ok(Default::default()) Ok(Default::default())
} }
@ -1718,7 +1792,7 @@ impl Engine {
// Import statement // Import statement
Stmt::Import(x) => { Stmt::Import(x) => {
let (expr, (name, _)) = x.as_ref(); let (expr, (name, pos)) = x.as_ref();
#[cfg(feature = "no_module")] #[cfg(feature = "no_module")]
unreachable!(); unreachable!();
@ -1736,6 +1810,7 @@ impl Engine {
let mod_name = unsafe_cast_var_name(name, &state); let mod_name = unsafe_cast_var_name(name, &state);
scope.push_module(mod_name, module); scope.push_module(mod_name, module);
self.inc_operations(state, *pos)?;
Ok(Default::default()) Ok(Default::default())
} else { } else {
Err(Box::new(EvalAltResult::ErrorModuleNotFound( Err(Box::new(EvalAltResult::ErrorModuleNotFound(
@ -1776,6 +1851,31 @@ impl Engine {
} }
} }
/// Check if the number of operations stay within limit.
fn inc_operations(&self, state: &mut State, pos: Position) -> Result<(), Box<EvalAltResult>> {
state.operations += 1;
#[cfg(not(feature = "unchecked"))]
{
// Guard against too many operations
if let Some(max) = self.max_operations {
if state.operations > max.get() {
return Err(Box::new(EvalAltResult::ErrorTooManyOperations(pos)));
}
}
}
// Report progress - only in steps
if let Some(progress) = self.progress.as_ref() {
if !progress(state.operations) {
// Terminate script if progress returns false
return Err(Box::new(EvalAltResult::ErrorTerminated(pos)));
}
}
Ok(())
}
/// Map a type_name into a pretty-print name /// Map a type_name into a pretty-print name
pub(crate) fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str { pub(crate) fn map_type_name<'a>(&'a self, name: &'a str) -> &'a str {
self.type_names self.type_names

View File

@ -20,6 +20,11 @@ pub type PrintCallback = dyn Fn(&str) + Send + Sync + 'static;
#[cfg(not(feature = "sync"))] #[cfg(not(feature = "sync"))]
pub type PrintCallback = dyn Fn(&str) + 'static; pub type PrintCallback = dyn Fn(&str) + 'static;
#[cfg(feature = "sync")]
pub type ProgressCallback = dyn Fn(u64) -> bool + Send + Sync + 'static;
#[cfg(not(feature = "sync"))]
pub type ProgressCallback = dyn Fn(u64) -> bool + 'static;
// Define callback function types // Define callback function types
#[cfg(feature = "sync")] #[cfg(feature = "sync")]
pub trait ObjectGetCallback<T, U>: Fn(&mut T) -> U + Send + Sync + 'static {} pub trait ObjectGetCallback<T, U>: Fn(&mut T) -> U + Send + Sync + 'static {}

View File

@ -79,8 +79,12 @@ pub enum EvalAltResult {
ErrorDotExpr(String, Position), ErrorDotExpr(String, Position),
/// Arithmetic error encountered. Wrapped value is the error message. /// Arithmetic error encountered. Wrapped value is the error message.
ErrorArithmetic(String, Position), ErrorArithmetic(String, Position),
/// Number of operations over maximum limit.
ErrorTooManyOperations(Position),
/// Call stack over maximum limit. /// Call stack over maximum limit.
ErrorStackOverflow(Position), ErrorStackOverflow(Position),
/// The script is prematurely terminated.
ErrorTerminated(Position),
/// Run-time error encountered. Wrapped value is the error message. /// Run-time error encountered. Wrapped value is the error message.
ErrorRuntime(String, Position), ErrorRuntime(String, Position),
@ -137,7 +141,9 @@ impl EvalAltResult {
Self::ErrorInExpr(_) => "Malformed 'in' expression", Self::ErrorInExpr(_) => "Malformed 'in' expression",
Self::ErrorDotExpr(_, _) => "Malformed dot expression", Self::ErrorDotExpr(_, _) => "Malformed dot expression",
Self::ErrorArithmetic(_, _) => "Arithmetic error", Self::ErrorArithmetic(_, _) => "Arithmetic error",
Self::ErrorTooManyOperations(_) => "Too many operations",
Self::ErrorStackOverflow(_) => "Stack overflow", Self::ErrorStackOverflow(_) => "Stack overflow",
Self::ErrorTerminated(_) => "Script terminated.",
Self::ErrorRuntime(_, _) => "Runtime error", Self::ErrorRuntime(_, _) => "Runtime error",
Self::ErrorLoopBreak(true, _) => "Break statement not inside a loop", Self::ErrorLoopBreak(true, _) => "Break statement not inside a loop",
Self::ErrorLoopBreak(false, _) => "Continue statement not inside a loop", Self::ErrorLoopBreak(false, _) => "Continue statement not inside a loop",
@ -183,7 +189,9 @@ impl fmt::Display for EvalAltResult {
| Self::ErrorAssignmentToUnknownLHS(pos) | Self::ErrorAssignmentToUnknownLHS(pos)
| Self::ErrorInExpr(pos) | Self::ErrorInExpr(pos)
| Self::ErrorDotExpr(_, pos) | Self::ErrorDotExpr(_, pos)
| Self::ErrorStackOverflow(pos) => write!(f, "{} ({})", desc, pos), | Self::ErrorTooManyOperations(pos)
| Self::ErrorStackOverflow(pos)
| Self::ErrorTerminated(pos) => write!(f, "{} ({})", desc, pos),
Self::ErrorRuntime(s, pos) => { Self::ErrorRuntime(s, pos) => {
write!(f, "{} ({})", if s.is_empty() { desc } else { s }, pos) write!(f, "{} ({})", if s.is_empty() { desc } else { s }, pos)
@ -299,7 +307,9 @@ impl EvalAltResult {
| Self::ErrorInExpr(pos) | Self::ErrorInExpr(pos)
| Self::ErrorDotExpr(_, pos) | Self::ErrorDotExpr(_, pos)
| Self::ErrorArithmetic(_, pos) | Self::ErrorArithmetic(_, pos)
| Self::ErrorTooManyOperations(pos)
| Self::ErrorStackOverflow(pos) | Self::ErrorStackOverflow(pos)
| Self::ErrorTerminated(pos)
| Self::ErrorRuntime(_, pos) | Self::ErrorRuntime(_, pos)
| Self::ErrorLoopBreak(_, pos) | Self::ErrorLoopBreak(_, pos)
| Self::Return(_, pos) => *pos, | Self::Return(_, pos) => *pos,
@ -335,7 +345,9 @@ impl EvalAltResult {
| Self::ErrorInExpr(pos) | Self::ErrorInExpr(pos)
| Self::ErrorDotExpr(_, pos) | Self::ErrorDotExpr(_, pos)
| Self::ErrorArithmetic(_, pos) | Self::ErrorArithmetic(_, pos)
| Self::ErrorTooManyOperations(pos)
| Self::ErrorStackOverflow(pos) | Self::ErrorStackOverflow(pos)
| Self::ErrorTerminated(pos)
| Self::ErrorRuntime(_, pos) | Self::ErrorRuntime(_, pos)
| Self::ErrorLoopBreak(_, pos) | Self::ErrorLoopBreak(_, pos)
| Self::Return(_, pos) => *pos = new_position, | Self::Return(_, pos) => *pos = new_position,

93
tests/operations.rs Normal file
View File

@ -0,0 +1,93 @@
#![cfg(not(feature = "unchecked"))]
use rhai::{Engine, EvalAltResult};
#[test]
fn test_max_operations() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
engine.set_max_operations(500);
engine.on_progress(|count| {
if count % 100 == 0 {
println!("{}", count);
}
true
});
engine.eval::<()>("let x = 0; while x < 20 { x += 1; }")?;
assert!(matches!(
*engine
.eval::<()>("for x in range(0, 500) {}")
.expect_err("should error"),
EvalAltResult::ErrorTooManyOperations(_)
));
engine.set_max_operations(0);
engine.eval::<()>("for x in range(0, 10000) {}")?;
Ok(())
}
#[test]
fn test_max_operations_functions() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
engine.set_max_operations(500);
engine.on_progress(|count| {
if count % 100 == 0 {
println!("{}", count);
}
true
});
engine.eval::<()>(
r#"
fn inc(x) { x + 1 }
let x = 0;
while x < 20 { x = inc(x); }
"#,
)?;
assert!(matches!(
*engine
.eval::<()>(
r#"
fn inc(x) { x + 1 }
let x = 0;
while x < 1000 { x = inc(x); }
"#
)
.expect_err("should error"),
EvalAltResult::ErrorTooManyOperations(_)
));
Ok(())
}
#[test]
fn test_max_operations_eval() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
engine.set_max_operations(500);
engine.on_progress(|count| {
if count % 100 == 0 {
println!("{}", count);
}
true
});
assert!(matches!(
*engine
.eval::<()>(
r#"
let script = "for x in range(0, 500) {}";
eval(script);
"#
)
.expect_err("should error"),
EvalAltResult::ErrorTooManyOperations(_)
));
Ok(())
}

View File

@ -15,6 +15,7 @@ fn test_stack_overflow() -> Result<(), Box<EvalAltResult>> {
325 325
); );
#[cfg(not(feature = "unchecked"))]
match engine.eval::<()>( match engine.eval::<()>(
r" r"
fn foo(n) { if n == 0 { 0 } else { n + foo(n-1) } } fn foo(n) { if n == 0 { 0 } else { n + foo(n-1) } }