diff --git a/.idea/php.xml b/.idea/php.xml index e769432..2f2d64b 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -15,6 +15,8 @@ + + diff --git a/.idea/webResources.xml b/.idea/webResources.xml index 6f8e6ec..cf25a90 100644 --- a/.idea/webResources.xml +++ b/.idea/webResources.xml @@ -6,6 +6,7 @@ + diff --git a/InstructionInterface.png b/InstructionInterface.png new file mode 100644 index 0000000..778ec23 Binary files /dev/null and b/InstructionInterface.png differ diff --git a/README.md b/README.md index 9df26f1..7cbbc02 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,28 @@ or you are executing programs written by users, but you don't want to give them the ability to execute arbitrary code on your system +## Table of Contents + + +* [RTEX Engine](#rtex-engine) + * [Table of Contents](#table-of-contents) + * [How does it work?](#how-does-it-work) + * [Instructions](#instructions) + * [Methods & Namespaces](#methods--namespaces) + * [Builtin Instruction Sets](#builtin-instruction-sets) +* [License](#license) +* [Contributing](#contributing) + + +----------------------------------------------------------------------------- + ## How does it work? RTEX Engine operates on the simple principle of executing instructions in order, and then moving on to the next instruction. - +[README.md](README.md) ### Instructions Instructions are simply associative arrays that contain @@ -35,8 +50,6 @@ functions do not provide a way to interact with the host system or the filesystem, but you can extend the engine with your own methods and namespaces -#### JSON Representation - Since instructions are associative arrays, they can be represented in JSON, which is a common format for data transfer. The following is an example of a set of @@ -86,6 +99,7 @@ instructions that is shown above in JSON format: ] ``` + ### Methods & Namespaces Methods are different to instructions in the terms @@ -106,6 +120,18 @@ when they are invoked. ***Note:*** This functionality is a WIP, more information will be added as it is implemented +----------------------------------------------------------------------------- + +## Builtin Instruction Sets + +RTEX Engine comes with a limited set of instructions by default, +but you can extend the engine with your own methods and namespaces + + - base + - [get](docs/instructions/base/get.md) + - [set](docs/instructions/base/get.md) + - math + - [add](docs/instructions/add.md) # License diff --git a/config_example.yaml b/config_example.yaml new file mode 100644 index 0000000..5a7010b --- /dev/null +++ b/config_example.yaml @@ -0,0 +1,81 @@ +# Runtime configuration for RTEX, this section defines the configuration +# values used for the engine. Allowing you to set a maximum number of +# resources a program may use, or disable certain instructions. +# you can also import other NCC packages designed to extend RTEX +# such as the 'com.nosia.rtex_filesystem' package which adds IO functionality to RTEX. +runtime: + # The maximum value size in bytes, this is the maximum size of a value + # that can be stored in memory. This is used to prevent programs from + # using too much memory. + # (default: 0) + max_variable_size: 0 + + # The maximum number of variables that can be stored in memory at once. + # (default: 0) + max_variables: 0 + + # The maximum number of instructions that can be executed before the + # program is terminated. + # (default: 0) + max_stack_size: 0 + + # A list of instructions that are disabled, this is used to prevent + # programs from using certain instructions. + # (default: []) + instruction_blacklist: + - "eq" + - "neq" + - "gt" + - "gte" + + # A list of packages to import, this is used to import packages that + # extend the functionality of RTEX. + # (default: []) + import_namespaces: + - "com.nosial.rtex_filesystem" + +# Enabling supervisor mode will allow the runtime to spawn the program +# as a child process and monitor the resource usage of the child process. +# This is useful to terminate badly behaving programs. (eg; infinite loops) +# The supervisor will terminate the child process if it exceeds the +# resource limits set in the runtime section. +supervisor: + + # If true, the supervisor will be enabled + enabled: true + + # The maximum number of seconds a program can run for + max_execution_time: 100 # in seconds + + # The maximum number of seconds a program can use the CPU for + max_cpu_time: 100 # in seconds + + # The maximum number of bytes a program can use for memory + max_memory: 1000000 # in bytes + + +# Enabling a Redis hook will allow for easier debugging of programs. +# This will allow you to view the state of the program at any time +# This works by providing a reference ID to the program as a command-line +# argument. +# +# The engine will then connect to a Redis server and store the state of +# the program in a Redis hash. This hash will be updated every time the +# program executes an instruction. +# +# Libraries can also use this hook to store data in the Redis hash. +redis_hook: + # If true, the Redis hook will be enabled + enabled: True + + # The host of the Redis server + host: "127.0.0.1" + + # The port of the Redis server + port: 6379 + + # The password of the Redis server (optional) + password: null + + # If the information should be destroyed when the program exits + destroy_on_exit: False \ No newline at end of file diff --git a/instructions/get_var.json b/instructions/get_var.json deleted file mode 100644 index 928e166..0000000 --- a/instructions/get_var.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "type": "get", - "_": { - "variable": "foo" - } -} \ No newline at end of file diff --git a/instructions/set_var.json b/instructions/set_var.json deleted file mode 100644 index 83b586e..0000000 --- a/instructions/set_var.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "set", - "_": { - "variable": "foo", - "value": "bar" - } -} \ No newline at end of file diff --git a/project.json b/project.json index 1ba78bb..7241b47 100644 --- a/project.json +++ b/project.json @@ -38,7 +38,10 @@ "build": { "source_path": "src", "default_configuration": "release", - "main_execution": "main", + "main": { + "policy": "main", + "create_symlink": true + }, "define_constants": { "ASSEMBLY_NAME": "%ASSEMBLY.NAME%", "ASSEMBLY_PACKAGE": "%ASSEMBLY.PACKAGE%", @@ -57,6 +60,12 @@ "version": "latest", "source_type": "remote", "source": "nosial/libs.log=latest@n64" + }, + { + "name": "com.cheprasov.php_redis_client", + "version": "latest", + "source_type": "remote", + "source": "cheprasov/php-redis-client=latest@composer" } ], "configurations": [ diff --git a/src/RTEX/Abstracts/InstructionType.php b/src/RTEX/Abstracts/InstructionType.php index 00c5548..27a5589 100644 --- a/src/RTEX/Abstracts/InstructionType.php +++ b/src/RTEX/Abstracts/InstructionType.php @@ -27,9 +27,9 @@ const Equals = 'eq'; const NotEquals = 'neq'; const GreaterThan = 'gt'; - const GreaterThanOrEquals = 'gte'; + const GreaterThanOrEqual = 'gte'; const LessThan = 'lt'; - const LessThanOrEquals = 'lte'; + const LessThanOrEqual = 'lte'; const All = [ @@ -52,9 +52,9 @@ self::Equals, self::GreaterThan, - self::GreaterThanOrEquals, + self::GreaterThanOrEqual, self::LessThan, - self::LessThanOrEquals, + self::LessThanOrEqual, self::NotEquals, ]; } \ No newline at end of file diff --git a/src/RTEX/Classes/RedisHookInstance.php b/src/RTEX/Classes/RedisHookInstance.php new file mode 100644 index 0000000..a6cda24 --- /dev/null +++ b/src/RTEX/Classes/RedisHookInstance.php @@ -0,0 +1,27 @@ +Redis = new \Redis(); + $this->Redis->connect($host, $port); + if($password !== null) + $this->Redis->auth($password); + } + else + { + $this->Redis = new RedisClient([ + 'host' => $host, + 'port' => $port, + 'password' => $password + ]); + } + } + } \ No newline at end of file diff --git a/src/RTEX/Classes/Utilities.php b/src/RTEX/Classes/Utilities.php index bcbbd86..6df52dd 100644 --- a/src/RTEX/Classes/Utilities.php +++ b/src/RTEX/Classes/Utilities.php @@ -12,10 +12,11 @@ * Determines the type of variable, throws an exception if the type is not supported * * @param $input + * @param bool $return_unknown * @return string * @throws TypeException */ - public static function getType($input): string + public static function getType($input, bool $return_unknown=false): string { if ($input instanceof InstructionInterface) return VariableType::Instruction; @@ -33,6 +34,8 @@ return VariableType::Array; if (is_null($input)) return VariableType::Null; + if ($return_unknown) + return VariableType::Unknown; throw new TypeException(gettype($input)); } @@ -43,9 +46,8 @@ * * @param $input * @return array|mixed - * @noinspection PhpMissingReturnTypeInspection */ - public static function toArray($input) + public static function toArray($input): mixed { if($input instanceof InstructionInterface) return $input->toArray(); diff --git a/src/RTEX/Engine.php b/src/RTEX/Engine.php index 58ab570..ab4f676 100644 --- a/src/RTEX/Engine.php +++ b/src/RTEX/Engine.php @@ -1,13 +1,14 @@ Program->getMain()->getInstructions() as $instruction) { @@ -47,20 +48,24 @@ * * @param $input * @return mixed - * @throws UnsupportedVariableType - * @noinspection PhpMissingReturnTypeInspection + * @throws EvaluationException */ - public function eval($input) + public function eval($input): mixed { - switch(Utilities::determineType($input)) + try { - case VariableTypes::Instruction: - /** @var InstructionInterface $input */ + if($input instanceof InstructionInterface) + { + Log::debug('net.nosial.rtex', $input); return $input->eval($this); - - default: - return $input; + } } + catch(Exception $e) + { + throw new EvaluationException($e->getMessage(), $e->getCode(), $e); + } + + return $input; } /** @@ -86,4 +91,18 @@ { return $this->Environment; } + + /** + * Calls the method + * + * @param string $namespace + * @param string $method + * @param array $arguments + * @return mixed + */ + public function callMethod(string $namespace, string $method, array $arguments) + { + // TODO: Implement callMethod() method. + return null; + } } \ No newline at end of file diff --git a/src/RTEX/Exceptions/Core/MalformedInstructionException.php b/src/RTEX/Exceptions/Core/MalformedInstructionException.php deleted file mode 100644 index 361b92f..0000000 --- a/src/RTEX/Exceptions/Core/MalformedInstructionException.php +++ /dev/null @@ -1,16 +0,0 @@ - ClearMethod::class]) + * + * @return MethodInterface[] + */ + public static function getMethods(): array; + + /** + * Returns the name of the namespace (e.g. 'console') + * + * @return string + */ + public static function getName(): string; + } \ No newline at end of file diff --git a/src/RTEX/Interfaces/MethodInterface.php b/src/RTEX/Interfaces/MethodInterface.php new file mode 100644 index 0000000..daccda0 --- /dev/null +++ b/src/RTEX/Interfaces/MethodInterface.php @@ -0,0 +1,18 @@ +RedisHook = new RedisHook(); + } + + /** + * Returns the redis hook configuration + * + * @return RedisHook + */ + public function getRedisHook(): RedisHook + { + return $this->RedisHook; + } + + /** + * Returns an array representation of the configuration + * + * @return array + */ + public function toArray(): array + { + return [ + 'redis_hook' => $this->RedisHook->toArray() + ]; + } + + /** + * Constructs a configuration object from an array + * + * @param array $data + * @return static + */ + public static function fromArray(array $data): self + { + $instance = new self(); + $instance->RedisHook = RedisHook::fromArray(($data['redis_hook'] ?? [])); + return $instance; + } + } \ No newline at end of file diff --git a/src/RTEX/Objects/Engine/Configuration/RedisHook.php b/src/RTEX/Objects/Engine/Configuration/RedisHook.php new file mode 100644 index 0000000..6f1c07e --- /dev/null +++ b/src/RTEX/Objects/Engine/Configuration/RedisHook.php @@ -0,0 +1,172 @@ +Enabled = false; + $this->Host = '127.0.0.1'; + $this->Port = 6379; + $this->Password = null; + } + + /** + * Whether the hook is enabled or not + * + * @return false|string + */ + public function isEnabled(): false|string + { + return $this->Enabled; + } + + /** + * Enables the hook + * + * @returns void + */ + public function enable(): void + { + $this->Enabled = true; + } + + /** + * Disables the hook + * + * @return void + */ + public function disable(): void + { + $this->Enabled = false; + } + + /** + * Returns the host of the redis server + * + * @return string + */ + public function getHost(): string + { + return $this->Host; + } + + /** + * Sets the host of the redis server + * + * @param string $Host + */ + public function setHost(string $Host): void + { + $this->Host = $Host; + } + + /** + * Returns the port of the redis server + * + * @return int + */ + public function getPort(): int + { + return $this->Port; + } + + /** + * Sets the port of the redis server + * + * @param int $Port + */ + public function setPort(int $Port): void + { + $this->Port = $Port; + } + + /** + * Returns the password of the redis server + * + * @return string|null + */ + public function getPassword(): ?string + { + return $this->Password; + } + + /** + * Sets the password of the redis server + * + * @param string|null $Password + */ + public function setPassword(?string $Password): void + { + $this->Password = $Password; + } + + /** + * Returns an array representation of the configuration + * + * @return array + */ + public function toArray(): array + { + return [ + 'enabled' => $this->Enabled, + 'host' => $this->Host, + 'port' => $this->Port, + 'password' => $this->Password + ]; + } + + /** + * Constructs a new instance from an array + * + * @param array $data + * @return RedisHook + */ + public static function fromArray(array $data): self + { + $instance = new self(); + $instance->Enabled = ($data['enabled'] ?? false); + $instance->Host = ($data['host'] ?? '127.0.0.1'); + $instance->Port = ($data['port'] ?? 6379); + $instance->Password = ($data['password'] ?? null); + return $instance; + } + } \ No newline at end of file diff --git a/src/RTEX/Objects/Engine/Configuration/Runtime.php b/src/RTEX/Objects/Engine/Configuration/Runtime.php new file mode 100644 index 0000000..d16b51d --- /dev/null +++ b/src/RTEX/Objects/Engine/Configuration/Runtime.php @@ -0,0 +1,211 @@ +MaxVariableSize = 0; + $this->MaxVariables = 0; + $this->MaxStackSize = 0; + $this->InstructionBlacklist = []; + } + + /** + * @return int + */ + public function getMaxVariableSize(): int + { + return $this->MaxVariableSize; + } + + /** + * @param int $MaxVariableSize + */ + public function setMaxVariableSize(int $MaxVariableSize): void + { + $this->MaxVariableSize = $MaxVariableSize; + } + + /** + * @return int + */ + public function getMaxVariables(): int + { + return $this->MaxVariables; + } + + /** + * @param int $MaxVariables + */ + public function setMaxVariables(int $MaxVariables): void + { + $this->MaxVariables = $MaxVariables; + } + + /** + * @return int + */ + public function getMaxStackSize(): int + { + return $this->MaxStackSize; + } + + /** + * @param int $MaxStackSize + */ + public function setMaxStackSize(int $MaxStackSize): void + { + $this->MaxStackSize = $MaxStackSize; + } + + /** + * Returns the instruction blacklist + * + * @return array|string[] + */ + public function getInstructionBlacklist(): array + { + return $this->InstructionBlacklist; + } + + /** + * Sets the instruction blacklist + * + * @param array|string[] $InstructionBlacklist + */ + public function setInstructionBlacklist(array $InstructionBlacklist): void + { + $this->InstructionBlacklist = $InstructionBlacklist; + } + + /** + * Adds an instruction to the blacklist + * + * @param string $instruction + * @return void + */ + public function addInstructionToBlacklist(string $instruction): void + { + if (in_array($instruction, $this->InstructionBlacklist)) + return; + + $this->InstructionBlacklist[] = $instruction; + } + + /** + * Removes an instruction from the blacklist + * + * @param string $instruction + * @return void + */ + public function removeInstructionFromBlacklist(string $instruction): void + { + if (!in_array($instruction, $this->InstructionBlacklist)) + return; + + $this->InstructionBlacklist = array_diff($this->InstructionBlacklist, [$instruction]); + } + + /** + * @return string[] + */ + public function getImportNamespaces(): array + { + return $this->ImportNamespaces; + } + + /** + * @param string[] $ImportNamespaces + */ + public function setImportNamespaces(array $ImportNamespaces): void + { + $this->ImportNamespaces = $ImportNamespaces; + } + + /** + * Adds a namespace to the import list + * + * @param string $namespace + * @return void + */ + public function addNamespaceToImport(string $namespace): void + { + if (in_array($namespace, $this->ImportNamespaces)) + return; + + $this->ImportNamespaces[] = $namespace; + } + + /** + * Removes a namespace from the import list + * + * @param string $namespace + * @return void + */ + public function removeNamespaceFromImport(string $namespace): void + { + if (!in_array($namespace, $this->ImportNamespaces)) + return; + + $this->ImportNamespaces = array_diff($this->ImportNamespaces, [$namespace]); + } + + } \ No newline at end of file diff --git a/src/RTEX/Objects/Environment.php b/src/RTEX/Objects/Engine/Environment.php similarity index 64% rename from src/RTEX/Objects/Environment.php rename to src/RTEX/Objects/Engine/Environment.php index 5fd9535..d683d93 100644 --- a/src/RTEX/Objects/Environment.php +++ b/src/RTEX/Objects/Engine/Environment.php @@ -1,8 +1,11 @@ variableExists($name)) + throw new NameException("Variable '$name' is not defined"); + return $this->RuntimeVariables[$name]; } @@ -37,22 +45,42 @@ * @param string $name * @param mixed $value */ - public function setRuntimeVariable(string $name, $value): void + public function setRuntimeVariable(string $name, mixed $value): void { Log::debug('net.nosial.rtex', $name); $this->RuntimeVariables[$name] = $value; } + /** + * @param string $name + * @return bool + */ + public function variableExists(string $name): bool + { + return array_key_exists($name, $this->RuntimeVariables); + } + /** * Clears the value of the specified variable * * @return void + * @noinspection PhpUnused */ public function clearRuntimeVariables(): void { $this->RuntimeVariables = []; } + /** + * Counts the number of variables in the environment + * + * @return int + */ + public function countRuntimeVariables(): int + { + return count($this->RuntimeVariables); + } + /** * Returns an array representation of the object * diff --git a/src/RTEX/Objects/Program/Script.php b/src/RTEX/Objects/Program/Script.php index 1b185fa..6d9eead 100644 --- a/src/RTEX/Objects/Program/Script.php +++ b/src/RTEX/Objects/Program/Script.php @@ -1,10 +1,11 @@ Instructions[$index] = $instruction; } + /** + * Reorders the instructions in the script + * + * @param int $index + * @param int $newIndex + * @return void + * @noinspection PhpUnused + */ + public function reorderInstruction(int $index, int $newIndex): void + { + $instruction = $this->getInstruction($index); + $this->deleteInstruction($index); + array_splice($this->Instructions, $newIndex, 0, [$instruction]); + } + /** * Clears all instructions from the script * * @return void */ - public function clear() + public function clear(): void { $this->Instructions = []; } @@ -110,15 +128,14 @@ * * @param array $data * @return Script - * @throws MalformedInstructionException - * @throws UnsupportedVariableType + * @throws InstructionException */ public static function fromArray(array $data): Script { $script = new Script(); foreach ($data as $instruction) - $script->addInstruction(Utilities::constructInstruction($instruction)); + $script->addInstruction(InstructionBuilder::fromRaw($instruction)); return $script; } @@ -130,4 +147,18 @@ { return $this->Instructions; } + + /** + * Returns a string representation of the script (for debugging) + * + * @return string + */ + public function __toString(): string + { + $results = []; + foreach ($this->Instructions as $instruction) + $results[] = $instruction->__toString(); + + return implode("\n", $results); + } } \ No newline at end of file diff --git a/src/RTEX/RTEX.php b/src/RTEX/RTEX.php index 5c63be0..199f79d 100644 --- a/src/RTEX/RTEX.php +++ b/src/RTEX/RTEX.php @@ -2,6 +2,69 @@ namespace RTEX; + use OptsLib\Parse; + class RTEX { + /** + * The main CLI entry point for the RTEX program + * + * @return void + * @noinspection PhpNoReturnAttributeCanBeAddedInspection + */ + public static function main(): void + { + $args = Parse::getArguments(); + + $file = $args['path'] ?? $args['p'] ?? null; + + if(($args['version'] ?? $args['v'] ?? false)) + { + self::displayVersion(true); + } + + if($file == null || ($args['help'] ?? $args['h'] ?? false)) + { + self::displayHelp(true); + } + + exit(0); + } + + /** + * Prints the version of the RTEX program and optionally exits the program + * + * @param bool $exit + * @return void + */ + private static function displayVersion(bool $exit=false): void + { + print('RTEX 0.1.0' . PHP_EOL); + + if($exit) + { + exit(0); + } + } + + /** + * Prints the help menu and optionally exits the program + * + * @param bool $exit + * @return void + */ + private static function displayHelp(bool $exit=false): void + { + print('RTEX - Runtime Execution' . PHP_EOL); + print('Usage: rtex [options] [file]' . PHP_EOL); + print('Options:' . PHP_EOL); + print(' -h, --help Display this help message' . PHP_EOL); + print(' -p, --path Specify the path to the program file' . PHP_EOL); + print(' -v, --version Display the version of RTEX' . PHP_EOL); + + if($exit) + { + exit(0); + } + } } \ No newline at end of file diff --git a/src/RTEX/bin/main b/src/RTEX/bin/main new file mode 100644 index 0000000..890d659 --- /dev/null +++ b/src/RTEX/bin/main @@ -0,0 +1,14 @@ +getMessage() . PHP_EOL); + exit(1); + } diff --git a/tests/pure_runner.php b/tests/pure_runner.php new file mode 100644 index 0000000..a87d105 --- /dev/null +++ b/tests/pure_runner.php @@ -0,0 +1,24 @@ +getMain()->addInstruction(InstructionBuilder::set('foo', 'bar')); + $program->getMain()->addInstruction(InstructionBuilder::set('bar', 500)); + + $program->getMain()->addInstruction(InstructionBuilder::set('results', + InstructionBuilder::sum( + 500, + InstructionBuilder::get('bar') + ) + )); + + $engine = new Engine($program); + $engine->run(); \ No newline at end of file diff --git a/tests/script_builder.php b/tests/script_builder.php new file mode 100644 index 0000000..71d0e2b --- /dev/null +++ b/tests/script_builder.php @@ -0,0 +1,35 @@ +getMain()->addInstruction(InstructionBuilder::abs(-2)); + $program->getMain()->addInstruction(InstructionBuilder::div(2, 2)); + $program->getMain()->addInstruction(InstructionBuilder::floor(2.5)); + $program->getMain()->addInstruction(InstructionBuilder::mod(2, 2)); + $program->getMain()->addInstruction(InstructionBuilder::mul(2, 2)); + $program->getMain()->addInstruction(InstructionBuilder::pow(2, 2)); + $program->getMain()->addInstruction(InstructionBuilder::round(2.5)); + $program->getMain()->addInstruction(InstructionBuilder::sqrt(2)); + $program->getMain()->addInstruction(InstructionBuilder::sub(2, 2)); + $program->getMain()->addInstruction(InstructionBuilder::sum(2, 2)); + + $program->getMain()->addInstruction(InstructionBuilder::array_set(["test"=>["foo"=>"bar"]], 'test.foo', 'baz')); + $program->getMain()->addInstruction(InstructionBuilder::array_get(["test"=>["foo"=>"bar"]], 'test.foo')); + $program->getMain()->addInstruction(InstructionBuilder::set('test', 'foo')); + $program->getMain()->addInstruction(InstructionBuilder::get('test')); + + $program->getMain()->addInstruction(InstructionBuilder::eq(2, 2)); + $program->getMain()->addInstruction(InstructionBuilder::gt(2, 2)); + $program->getMain()->addInstruction(InstructionBuilder::gte(2, 2)); + $program->getMain()->addInstruction(InstructionBuilder::lt(2, 2)); + $program->getMain()->addInstruction(InstructionBuilder::lte(2, 2)); + $program->getMain()->addInstruction(InstructionBuilder::neq(2, 2)); + + print(json_encode($program->toArray(), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL); + print((string)$program->getMain() . PHP_EOL); \ No newline at end of file diff --git a/tests/syntax_parse.php b/tests/syntax_parse.php new file mode 100644 index 0000000..bd03089 --- /dev/null +++ b/tests/syntax_parse.php @@ -0,0 +1,72 @@ +