diff --git a/.gitignore b/.gitignore index add19fb..b5885de 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,10 @@ bindings/ruby/bin/ # JetBrains IDEs files .idea/ + +# Java build artifacts +**/*.class +**/*.jar +bindings/java/.classpath +bindings/java/.project +bindings/java/.settings/ diff --git a/Cargo.lock b/Cargo.lock index e53c16e..fddf06b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,9 +196,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.54" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "shlex", @@ -262,9 +262,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.5.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" dependencies = [ "clap_builder", "clap_derive", @@ -272,9 +272,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.5.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" dependencies = [ "anstream", "anstyle", @@ -284,9 +284,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2 1.0.106", @@ -460,9 +460,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" @@ -571,9 +571,9 @@ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1831,18 +1831,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.34" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" +checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.34" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" +checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" dependencies = [ "proc-macro2 1.0.106", "quote 1.0.44", diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt index aa9e123..57eee9e 100644 --- a/bindings/c/CMakeLists.txt +++ b/bindings/c/CMakeLists.txt @@ -13,6 +13,7 @@ FetchContent_Declare( FetchContent_MakeAvailable(Corrosion) project("regorus-test") +enable_testing() corrosion_import_crate( # Path to /bindings/ffi/Cargo.toml @@ -35,3 +36,10 @@ add_executable(regorus_test main.c) # Add path to /bindings/ffi target_include_directories(regorus_test PRIVATE "../ffi") target_link_libraries(regorus_test regorus_ffi) + +add_executable(regorus_rvm_test rvm_tests.c) +target_include_directories(regorus_rvm_test PRIVATE "../ffi") +target_link_libraries(regorus_rvm_test regorus_ffi) + +add_test(NAME regorus_c_engine COMMAND regorus_test) +add_test(NAME regorus_c_rvm COMMAND regorus_rvm_test) diff --git a/bindings/c/rvm_tests.c b/bindings/c/rvm_tests.c new file mode 100644 index 0000000..ab0a84c --- /dev/null +++ b/bindings/c/rvm_tests.c @@ -0,0 +1,289 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include +#include +#include "regorus.h" + +static int assert_ok(RegorusResult r, const char* message) { + if (r.status != Ok) { + fprintf(stderr, "%s: %s\n", message, r.error_message ? r.error_message : "(no error)"); + return 0; + } + return 1; +} + +int main() { + RegorusResult result = {0}; + bool result_valid = false; + RegorusProgram* program = NULL; + RegorusBuffer* buffer = NULL; + RegorusProgram* program2 = NULL; + RegorusRvm* vm = NULL; + RegorusProgram* host_program = NULL; + RegorusRvm* host_vm = NULL; + bool is_partial = false; + int exit_code = 1; + + const char* data_json = + "{" + " \"roles\": {" + " \"alice\": [\"admin\", \"reader\"]" + " }" + "}"; + const char* input_json = + "{" + " \"user\": \"alice\"," + " \"actions\": [\"read\"]" + "}"; + const char* module_text = + "package demo\n" + "default allow = false\n" + "allow if {\n" + " input.user == \"alice\"\n" + " some role in data.roles[input.user]\n" + " role == \"admin\"\n" + " count(input.actions) > 0\n" + "}\n"; + + const char* host_data_json = "{}"; + const char* host_input_json = "{\"account\":{\"id\":\"acct-1\",\"active\":true}}"; + const char* host_module_text = + "package demo\n" + "import rego.v1\n" + "default allow := false\n" + "allow if {\n" + " input.account.active == true\n" + " details := __builtin_host_await(input.account.id, \"account\")\n" + " details.tier == \"gold\"\n" + "}\n"; + + RegorusPolicyModule module; + module.id = "demo.rego"; + module.content = module_text; + + const char* entry_points[] = {"data.demo.allow"}; + printf("Rego policy:\n%s\n", module_text); + printf("Compiling program from modules...\n"); + result = regorus_program_compile_from_modules( + data_json, + &module, + 1, + entry_points, + 1 + ); + result_valid = true; + if (!assert_ok(result, "compile program")) { + goto Cleanup; + } + program = (RegorusProgram*)result.pointer_value; + regorus_result_drop(result); + result_valid = false; + + printf("Generating assembly listing...\n"); + result = regorus_program_generate_listing(program); + result_valid = true; + if (!assert_ok(result, "generate listing")) { + goto Cleanup; + } + printf("Assembly listing:\n%s\n", result.output ? result.output : "(null)"); + regorus_result_drop(result); + result_valid = false; + + printf("Serializing program...\n"); + result = regorus_program_serialize_binary(program); + result_valid = true; + if (!assert_ok(result, "serialize program")) { + goto Cleanup; + } + buffer = (RegorusBuffer*)result.pointer_value; + regorus_result_drop(result); + result_valid = false; + + printf("Deserializing program (%zu bytes)...\n", buffer->len); + result = regorus_program_deserialize_binary( + buffer->data, + buffer->len, + &is_partial + ); + result_valid = true; + if (!assert_ok(result, "deserialize program")) { + goto Cleanup; + } + + if (is_partial) { + fprintf(stderr, "deserialized program marked partial\n"); + goto Cleanup; + } + + program2 = (RegorusProgram*)result.pointer_value; + regorus_result_drop(result); + result_valid = false; + + printf("Creating VM...\n"); + vm = regorus_rvm_new(); + if (!vm) { + fprintf(stderr, "failed to allocate VM\n"); + goto Cleanup; + } + + printf("Loading program into VM...\n"); + result = regorus_rvm_load_program(vm, program2); + result_valid = true; + if (!assert_ok(result, "load program")) { + goto Cleanup; + } + regorus_result_drop(result); + result_valid = false; + + printf("Setting data...\n"); + result = regorus_rvm_set_data(vm, data_json); + result_valid = true; + if (!assert_ok(result, "set data")) { + goto Cleanup; + } + regorus_result_drop(result); + result_valid = false; + + printf("Setting input...\n"); + result = regorus_rvm_set_input(vm, input_json); + result_valid = true; + if (!assert_ok(result, "set input")) { + goto Cleanup; + } + regorus_result_drop(result); + result_valid = false; + + printf("Executing entry point...\n"); + result = regorus_rvm_execute(vm); + result_valid = true; + if (!assert_ok(result, "execute")) { + goto Cleanup; + } + + printf("Execution result (data.demo.allow): %s\n", + result.output ? result.output : "(null)"); + printf("Decision: user=alice action=read -> allow=%s\n", + result.output ? result.output : "(null)"); + if (!result.output || strcmp(result.output, "true") != 0) { + fprintf(stderr, "unexpected result: %s\n", result.output); + goto Cleanup; + } + + printf("\n--- HostAwait example (suspendable execution) ---\n"); + RegorusPolicyModule host_module; + host_module.id = "host_await.rego"; + host_module.content = host_module_text; + + const char* host_entry_points[] = {"data.demo.allow"}; + result = regorus_program_compile_from_modules( + host_data_json, + &host_module, + 1, + host_entry_points, + 1 + ); + result_valid = true; + if (!assert_ok(result, "compile host await program")) { + goto Cleanup; + } + host_program = (RegorusProgram*)result.pointer_value; + regorus_result_drop(result); + result_valid = false; + + host_vm = regorus_rvm_new(); + if (!host_vm) { + fprintf(stderr, "failed to allocate host await VM\n"); + goto Cleanup; + } + + result = regorus_rvm_set_execution_mode(host_vm, 1); + result_valid = true; + if (!assert_ok(result, "set execution mode")) { + goto Cleanup; + } + regorus_result_drop(result); + result_valid = false; + + result = regorus_rvm_load_program(host_vm, host_program); + result_valid = true; + if (!assert_ok(result, "load host await program")) { + goto Cleanup; + } + regorus_result_drop(result); + result_valid = false; + + result = regorus_rvm_set_data(host_vm, host_data_json); + result_valid = true; + if (!assert_ok(result, "set host data")) { + goto Cleanup; + } + regorus_result_drop(result); + result_valid = false; + + result = regorus_rvm_set_input(host_vm, host_input_json); + result_valid = true; + if (!assert_ok(result, "set host input")) { + goto Cleanup; + } + regorus_result_drop(result); + result_valid = false; + + result = regorus_rvm_execute(host_vm); + result_valid = true; + if (!assert_ok(result, "execute host await")) { + goto Cleanup; + } + printf("HostAwait initial result: %s\n", result.output ? result.output : "(null)"); + regorus_result_drop(result); + result_valid = false; + + result = regorus_rvm_get_execution_state(host_vm); + result_valid = true; + if (!assert_ok(result, "get execution state")) { + goto Cleanup; + } + printf("Execution state: %s\n", result.output ? result.output : "(null)"); + regorus_result_drop(result); + result_valid = false; + + result = regorus_rvm_resume(host_vm, "{\"tier\":\"gold\"}", true); + result_valid = true; + if (!assert_ok(result, "resume host await")) { + goto Cleanup; + } + printf("HostAwait resumed result: %s\n", result.output ? result.output : "(null)"); + + if (!result.output || strcmp(result.output, "true") != 0) { + fprintf(stderr, "unexpected host await result\n"); + goto Cleanup; + } + regorus_result_drop(result); + result_valid = false; + + exit_code = 0; + +Cleanup: + if (result_valid) { + regorus_result_drop(result); + } + if (host_vm) { + regorus_rvm_drop(host_vm); + } + if (host_program) { + regorus_program_drop(host_program); + } + if (vm) { + regorus_rvm_drop(vm); + } + if (program2) { + regorus_program_drop(program2); + } + if (buffer) { + regorus_buffer_drop(buffer); + } + if (program) { + regorus_program_drop(program); + } + return exit_code; +} diff --git a/bindings/cpp/CMakeLists.txt b/bindings/cpp/CMakeLists.txt index df81fca..3d05d42 100644 --- a/bindings/cpp/CMakeLists.txt +++ b/bindings/cpp/CMakeLists.txt @@ -14,6 +14,7 @@ FetchContent_MakeAvailable(Corrosion) project("regorus-test") set(CMAKE_CXX_STANDARD 17) +enable_testing() # installable ffi target @@ -83,3 +84,9 @@ install(FILES add_executable(regorus_test main.cpp) target_link_libraries(regorus_test regorus_ffi::regorus_ffi) + +add_executable(regorus_rvm_test rvm_tests.cpp) +target_link_libraries(regorus_rvm_test regorus_ffi::regorus_ffi) + +add_test(NAME regorus_cpp_engine COMMAND regorus_test) +add_test(NAME regorus_cpp_rvm COMMAND regorus_rvm_test) diff --git a/bindings/cpp/regorus.hpp b/bindings/cpp/regorus.hpp index 6a81dcc..6cdfb1c 100644 --- a/bindings/cpp/regorus.hpp +++ b/bindings/cpp/regorus.hpp @@ -1,6 +1,8 @@ #ifndef REGORUS_WRAPPER_HPP #define REGORUS_WRAPPER_HPP +#include +#include #include #include @@ -8,8 +10,11 @@ namespace regorus { - class Result { - public: + class Buffer; + class Program; + + class Result { + public: operator bool() const { return result.status == RegorusStatus::Ok; } bool operator !() const { return result.status != RegorusStatus::Ok; } @@ -30,18 +35,39 @@ namespace regorus { } } + void* pointer() const { + return result.pointer_value; + } + + Program program() const; + Buffer buffer() const; + + Result(RegorusResult r) : result(r) {} + Result(Result&& other) noexcept : result(other.result) { + other.result.output = nullptr; + other.result.error_message = nullptr; + other.result.pointer_value = nullptr; + } + Result& operator=(Result&& other) noexcept { + if (this != &other) { + regorus_result_drop(result); + result = other.result; + other.result.output = nullptr; + other.result.error_message = nullptr; + other.result.pointer_value = nullptr; + } + return *this; + } + ~Result() { regorus_result_drop(result); } private: - friend class Engine; RegorusResult result; - Result(RegorusResult r) : result(r) {} private: Result(const Result&) = delete; - Result(Result&&) = delete; Result& operator=(const Result&) = delete; }; @@ -109,6 +135,10 @@ namespace regorus { ~Engine() { regorus_engine_drop(engine); } + + RegorusEngine* raw() const { + return engine; + } private: @@ -119,6 +149,247 @@ namespace regorus { Engine(Engine&&) = delete; Engine& operator=(const Engine&) = delete; }; + + class CompiledPolicy { + public: + explicit CompiledPolicy(RegorusCompiledPolicy* p) : policy(p) {} + + Result eval_with_input(const char* input_json) { + return Result(regorus_compiled_policy_eval_with_input(policy, input_json)); + } + + Result get_policy_info() { + return Result(regorus_compiled_policy_get_policy_info(policy)); + } + + RegorusCompiledPolicy* raw() const { + return policy; + } + + ~CompiledPolicy() { + if (policy) { + regorus_compiled_policy_drop(policy); + } + } + + private: + RegorusCompiledPolicy* policy; + CompiledPolicy(const CompiledPolicy&) = delete; + CompiledPolicy(CompiledPolicy&&) = delete; + CompiledPolicy& operator=(const CompiledPolicy&) = delete; + }; + + class Buffer { + public: + Buffer() : buffer(nullptr) {} + explicit Buffer(RegorusBuffer* b) : buffer(b) {} + + const std::uint8_t* data() const { + return buffer ? buffer->data : nullptr; + } + + size_t size() const { + return buffer ? buffer->len : 0; + } + + RegorusBuffer* raw() const { + return buffer; + } + + ~Buffer() { + if (buffer) { + regorus_buffer_drop(buffer); + } + } + + private: + RegorusBuffer* buffer; + Buffer(const Buffer&) = delete; + Buffer(Buffer&&) = delete; + Buffer& operator=(const Buffer&) = delete; + }; + + class Program { + public: + Program() : program(regorus_program_new()) {} + explicit Program(RegorusProgram* p) : program(p) {} + + static Result compile_from_policy( + RegorusCompiledPolicy* compiled_policy, + const char* const* entry_points, + size_t entry_points_len + ) { + return Result(regorus_program_compile_from_policy( + compiled_policy, + entry_points, + entry_points_len + )); + } + + static Result compile_from_modules( + const char* data_json, + const RegorusPolicyModule* modules, + size_t modules_len, + const char* const* entry_points, + size_t entry_points_len + ) { + return Result(regorus_program_compile_from_modules( + data_json, + modules, + modules_len, + entry_points, + entry_points_len + )); + } + + static Result compile_from_engine( + RegorusEngine* engine, + const char* const* entry_points, + size_t entry_points_len + ) { + return Result(regorus_engine_compile_program_with_entrypoints( + engine, + entry_points, + entry_points_len + )); + } + + Result serialize_binary() const { + return Result(regorus_program_serialize_binary(program)); + } + + static Result deserialize_binary( + const std::uint8_t* data, + size_t len, + bool* is_partial + ) { + return Result(regorus_program_deserialize_binary(data, len, is_partial)); + } + + Result generate_listing() const { + return Result(regorus_program_generate_listing(program)); + } + + Result generate_tabular_listing() const { + return Result(regorus_program_generate_tabular_listing(program)); + } + + RegorusProgram* raw() const { + return program; + } + + ~Program() { + if (program) { + regorus_program_drop(program); + } + } + + private: + RegorusProgram* program; + Program(const Program&) = delete; + Program(Program&&) = delete; + Program& operator=(const Program&) = delete; + }; + + inline Program Result::program() const { + return Program(reinterpret_cast(result.pointer_value)); + } + + inline Buffer Result::buffer() const { + return Buffer(reinterpret_cast(result.pointer_value)); + } + + class Rvm { + public: + Rvm() : vm(regorus_rvm_new()) {} + explicit Rvm(RegorusRvm* v) : vm(v) {} + + static Result create_with_policy(RegorusCompiledPolicy* compiled_policy) { + return Result(regorus_rvm_new_with_policy(compiled_policy)); + } + + Result load_program(const Program& program) { + return Result(regorus_rvm_load_program(vm, program.raw())); + } + + Result set_data(const char* data_json) { + return Result(regorus_rvm_set_data(vm, data_json)); + } + + Result set_input(const char* input_json) { + return Result(regorus_rvm_set_input(vm, input_json)); + } + + Result set_max_instructions(size_t max_instructions) { + return Result(regorus_rvm_set_max_instructions(vm, max_instructions)); + } + + Result set_strict_builtin_errors(bool strict) { + return Result(regorus_rvm_set_strict_builtin_errors(vm, strict)); + } + + Result set_execution_mode(std::uint8_t mode) { + return Result(regorus_rvm_set_execution_mode(vm, mode)); + } + + Result set_step_mode(bool enabled) { + return Result(regorus_rvm_set_step_mode(vm, enabled)); + } + + Result set_execution_timer_config(bool has_config, RegorusExecutionTimerConfig config) { + return Result(regorus_rvm_set_execution_timer_config(vm, has_config, config)); + } + + Result execute() { + return Result(regorus_rvm_execute(vm)); + } + + Result execute_entry_point_by_name(const char* entry_point) { + return Result(regorus_rvm_execute_entry_point_by_name(vm, entry_point)); + } + + Result execute_entry_point_by_index(size_t index) { + return Result(regorus_rvm_execute_entry_point_by_index(vm, index)); + } + + Result resume(const char* resume_value_json, bool has_value) { + return Result(regorus_rvm_resume(vm, resume_value_json, has_value)); + } + + Result get_execution_state() { + return Result(regorus_rvm_get_execution_state(vm)); + } + + RegorusRvm* raw() const { + return vm; + } + + ~Rvm() { + if (vm) { + regorus_rvm_drop(vm); + } + } + + private: + RegorusRvm* vm; + Rvm(const Rvm&) = delete; + Rvm(Rvm&&) = delete; + Rvm& operator=(const Rvm&) = delete; + }; + + inline Result compile_policy_with_entrypoint( + const char* data_json, + const RegorusPolicyModule* modules, + size_t modules_len, + const char* entry_point + ) { + return Result(regorus_compile_policy_with_entrypoint( + data_json, + modules, + modules_len, + entry_point + )); + } } #endif // REGORUS_WRAPPER_HPP diff --git a/bindings/cpp/rvm_tests.cpp b/bindings/cpp/rvm_tests.cpp new file mode 100644 index 0000000..8ee7b18 --- /dev/null +++ b/bindings/cpp/rvm_tests.cpp @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include +#include +#include "regorus.hpp" + +int main() { + const char* data_json = + "{" + " \"roles\": {" + " \"alice\": [\"admin\", \"reader\"]" + " }" + "}"; + const char* input_json = + "{" + " \"user\": \"alice\"," + " \"actions\": [\"read\"]" + "}"; + const char* module_text = + "package demo\n" + "default allow = false\n" + "allow if {\n" + " input.user == \"alice\"\n" + " some role in data.roles[input.user]\n" + " role == \"admin\"\n" + " count(input.actions) > 0\n" + "}\n"; + + const char* host_data_json = "{}"; + const char* host_input_json = "{\"account\":{\"id\":\"acct-1\",\"active\":true}}"; + const char* host_module_text = + "package demo\n" + "import rego.v1\n" + "default allow := false\n" + "allow if {\n" + " input.account.active == true\n" + " details := __builtin_host_await(input.account.id, \"account\")\n" + " details.tier == \"gold\"\n" + "}\n"; + + RegorusPolicyModule module; + module.id = "demo.rego"; + module.content = module_text; + + const char* entry_points[] = {"data.demo.allow"}; + std::cout << "Rego policy:\n" << module_text << std::endl; + std::cout << "Compiling program from modules..." << std::endl; + auto program_result = regorus::Program::compile_from_modules( + data_json, + &module, + 1, + entry_points, + 1 + ); + if (!program_result) { + std::cerr << "compile program (modules): " << program_result.error() << std::endl; + return 1; + } + + regorus::Program program = program_result.program(); + + std::cout << "Generating assembly listing..." << std::endl; + auto listing_result = program.generate_listing(); + if (!listing_result) { + std::cerr << "generate listing: " << listing_result.error() << std::endl; + return 1; + } + std::cout << "Assembly listing:\n" << listing_result.output() << std::endl; + + std::cout << "Serializing program..." << std::endl; + auto serialize_result = program.serialize_binary(); + if (!serialize_result) { + std::cerr << "serialize program: " << serialize_result.error() << std::endl; + return 1; + } + + regorus::Buffer buffer(reinterpret_cast(serialize_result.pointer())); + bool is_partial = false; + std::cout << "Deserializing program (" << buffer.size() << " bytes)..." << std::endl; + auto deserialize_result = regorus::Program::deserialize_binary( + buffer.data(), + buffer.size(), + &is_partial + ); + if (!deserialize_result) { + std::cerr << "deserialize program: " << deserialize_result.error() << std::endl; + return 1; + } + + if (is_partial) { + std::cerr << "deserialized program marked partial" << std::endl; + return 1; + } + + regorus::Program program2 = deserialize_result.program(); + + { + std::cout << "Creating VM..." << std::endl; + regorus::Rvm vm; + auto load_result = vm.load_program(program2); + if (!load_result) { + std::cerr << "load program: " << load_result.error() << std::endl; + return 1; + } + + std::cout << "Setting data..." << std::endl; + auto data_result = vm.set_data(data_json); + if (!data_result) { + std::cerr << "set data: " << data_result.error() << std::endl; + return 1; + } + + std::cout << "Setting input..." << std::endl; + auto input_result = vm.set_input(input_json); + if (!input_result) { + std::cerr << "set input: " << input_result.error() << std::endl; + return 1; + } + + std::cout << "Executing entry point..." << std::endl; + auto exec_result = vm.execute(); + if (!exec_result) { + std::cerr << "execute: " << exec_result.error() << std::endl; + return 1; + } + + std::cout << "Execution result (data.demo.allow): " << exec_result.output() << std::endl; + std::cout << "Decision: user=alice action=read -> allow=" << exec_result.output() << std::endl; + if (std::string(exec_result.output()) != "true") { + std::cerr << "unexpected result: " << exec_result.output() << std::endl; + return 1; + } + } + + regorus::Engine engine; + std::cout << "Compiling program from engine..." << std::endl; + auto add_policy_result = engine.add_policy("demo.rego", module_text); + if (!add_policy_result) { + std::cerr << "engine add policy: " << add_policy_result.error() << std::endl; + return 1; + } + + auto engine_program_result = regorus::Program::compile_from_engine( + engine.raw(), + entry_points, + 1 + ); + if (!engine_program_result) { + std::cerr << "compile program (engine): " << engine_program_result.error() << std::endl; + return 1; + } + + regorus::Program engine_program = engine_program_result.program(); + + regorus::Rvm engine_vm; + auto engine_load_result = engine_vm.load_program(engine_program); + if (!engine_load_result) { + std::cerr << "engine load program: " << engine_load_result.error() << std::endl; + return 1; + } + + std::cout << "Setting engine data..." << std::endl; + auto engine_data_result = engine_vm.set_data(data_json); + if (!engine_data_result) { + std::cerr << "engine set data: " << engine_data_result.error() << std::endl; + return 1; + } + + std::cout << "Setting engine input..." << std::endl; + auto engine_input_result = engine_vm.set_input(input_json); + if (!engine_input_result) { + std::cerr << "engine set input: " << engine_input_result.error() << std::endl; + return 1; + } + + std::cout << "Executing engine entry point..." << std::endl; + auto engine_exec_result = engine_vm.execute(); + if (!engine_exec_result) { + std::cerr << "engine execute: " << engine_exec_result.error() << std::endl; + return 1; + } + + std::cout << "Engine execution result (data.demo.allow): " << engine_exec_result.output() << std::endl; + std::cout << "Decision: user=alice action=read -> allow=" << engine_exec_result.output() << std::endl; + if (std::string(engine_exec_result.output()) != "true") { + std::cerr << "unexpected engine result: " << engine_exec_result.output() << std::endl; + return 1; + } + + std::cout << "\n--- HostAwait example (suspendable execution) ---" << std::endl; + RegorusPolicyModule host_module; + host_module.id = "host_await.rego"; + host_module.content = host_module_text; + const char* host_entry_points[] = {"data.demo.allow"}; + + auto host_program_result = regorus::Program::compile_from_modules( + host_data_json, + &host_module, + 1, + host_entry_points, + 1 + ); + if (!host_program_result) { + std::cerr << "compile host await program: " << host_program_result.error() << std::endl; + return 1; + } + + regorus::Program host_program = host_program_result.program(); + regorus::Rvm host_vm; + auto host_mode_result = host_vm.set_execution_mode(1); + if (!host_mode_result) { + std::cerr << "set execution mode: " << host_mode_result.error() << std::endl; + return 1; + } + + auto host_load_result = host_vm.load_program(host_program); + if (!host_load_result) { + std::cerr << "load host await program: " << host_load_result.error() << std::endl; + return 1; + } + + auto host_data_result = host_vm.set_data(host_data_json); + if (!host_data_result) { + std::cerr << "set host data: " << host_data_result.error() << std::endl; + return 1; + } + + auto host_input_result = host_vm.set_input(host_input_json); + if (!host_input_result) { + std::cerr << "set host input: " << host_input_result.error() << std::endl; + return 1; + } + + auto host_exec_result = host_vm.execute(); + if (!host_exec_result) { + std::cerr << "execute host await: " << host_exec_result.error() << std::endl; + return 1; + } + std::cout << "HostAwait initial result: " << host_exec_result.output() << std::endl; + + auto host_state_result = host_vm.get_execution_state(); + if (!host_state_result) { + std::cerr << "get execution state: " << host_state_result.error() << std::endl; + return 1; + } + std::cout << "Execution state: " << host_state_result.output() << std::endl; + + auto host_resume_result = host_vm.resume("{\"tier\":\"gold\"}", true); + if (!host_resume_result) { + std::cerr << "resume host await: " << host_resume_result.error() << std::endl; + return 1; + } + std::cout << "HostAwait resumed result: " << host_resume_result.output() << std::endl; + if (std::string(host_resume_result.output()) != "true") { + std::cerr << "unexpected host await result: " << host_resume_result.output() << std::endl; + return 1; + } + + return 0; +} diff --git a/bindings/csharp/README.md b/bindings/csharp/README.md index ac00e1f..ad6cf07 100644 --- a/bindings/csharp/README.md +++ b/bindings/csharp/README.md @@ -64,3 +64,43 @@ Regorus.MemoryLimits.SetThreadFlushThresholdOverride(null); ``` See bindings/csharp/Regorus.Tests/RegorusTests.cs for scenario coverage and bindings/csharp/TargetExampleApp/Program.cs for end-to-end usage. + +## RVM Usage Example + +The RVM API lets you compile a program from modules/entrypoints and execute it in a VM: + +```csharp +using Regorus; + +const string Policy = """ +package demo +default allow = false +allow if { + input.user == "alice" + some role in data.roles[input.user] + role == "admin" +} +"""; + +const string Data = """ +{ "roles": { "alice": ["admin"] } } +"""; + +const string Input = """ +{ "user": "alice" } +"""; + +var modules = new[] { new PolicyModule("demo.rego", Policy) }; +var entryPoints = new[] { "data.demo.allow" }; + +using var program = Program.CompileFromModules(Data, modules, entryPoints); +var listing = program.GenerateListing(); + +using var vm = new Rvm(); +vm.LoadProgram(program); +vm.SetDataJson(Data); +vm.SetInputJson(Input); + +var result = vm.Execute(); +Console.WriteLine($"allow: {result}"); +``` diff --git a/bindings/csharp/Regorus.Tests/RvmProgramTests.cs b/bindings/csharp/Regorus.Tests/RvmProgramTests.cs new file mode 100644 index 0000000..fd059a0 --- /dev/null +++ b/bindings/csharp/Regorus.Tests/RvmProgramTests.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Regorus.Tests; + +[TestClass] +public sealed class RvmProgramTests +{ + private const string Policy = """ +package demo +default allow = false +allow if { + input.user == "alice" + some role in data.roles[input.user] + role == "admin" + count(input.actions) > 0 +} +"""; + + private const string Data = """ +{ + "roles": { + "alice": ["admin", "reader"] + } +} +"""; + + private const string Input = """ +{ + "user": "alice", + "actions": ["read"] +} +"""; + + private const string HostAwaitPolicy = """ +package demo +import rego.v1 + +default allow := false + +allow if { + input.account.active == true + details := __builtin_host_await(input.account.id, "account") + details.tier == "gold" +} +"""; + + private const string HostAwaitInput = """ +{ + "account": { + "id": "acct-1", + "active": true + } +} +"""; + + [TestMethod] + public void Program_compile_and_execute_succeeds() + { + var modules = new[] { new PolicyModule("demo.rego", Policy) }; + var entryPoints = new[] { "data.demo.allow" }; + + var program = Program.CompileFromModules(Data, modules, entryPoints); + var listing = program.GenerateListing(); + Assert.IsFalse(string.IsNullOrWhiteSpace(listing), "listing should be generated"); + + var binary = program.SerializeBinary(); + var rehydrated = Program.DeserializeBinary(binary, out var isPartial); + Assert.IsFalse(isPartial, "program should be fully deserialized"); + + using var vm = new Rvm(); + vm.LoadProgram(rehydrated); + vm.SetDataJson(Data); + vm.SetInputJson(Input); + + var result = vm.Execute(); + Assert.AreEqual("true", result, "expected allow=true"); + } + + [TestMethod] + public void Program_compile_from_engine_succeeds() + { + using var engine = new Engine(); + engine.AddPolicy("demo.rego", Policy); + + var program = Program.CompileFromEngine(engine, new[] { "data.demo.allow" }); + using var vm = new Rvm(); + vm.LoadProgram(program); + vm.SetDataJson(Data); + vm.SetInputJson(Input); + + var result = vm.Execute(); + Assert.AreEqual("true", result, "expected allow=true"); + } + + [TestMethod] + public void Program_host_await_suspend_and_resume_succeeds() + { + var modules = new[] { new PolicyModule("host_await.rego", HostAwaitPolicy) }; + var entryPoints = new[] { "data.demo.allow" }; + + using var program = Program.CompileFromModules("{}", modules, entryPoints); + using var vm = new Rvm(); + vm.SetExecutionMode(1); + vm.LoadProgram(program); + vm.SetInputJson(HostAwaitInput); + + var initial = vm.Execute(); + var state = vm.GetExecutionState(); + Assert.IsNotNull(state, "execution state should be available"); + StringAssert.Contains(state!, "HostAwait", "expected HostAwait suspension"); + + var resumed = vm.Resume("{\"tier\":\"gold\"}"); + Assert.AreEqual("true", resumed, "expected allow=true after resume"); + } +} \ No newline at end of file diff --git a/bindings/csharp/Regorus/CompiledPolicy.cs b/bindings/csharp/Regorus/CompiledPolicy.cs index a13163d..3cdae76 100644 --- a/bindings/csharp/Regorus/CompiledPolicy.cs +++ b/bindings/csharp/Regorus/CompiledPolicy.cs @@ -180,7 +180,7 @@ namespace Regorus return handle; } - private T UseHandle(Func func) + internal T UseHandle(Func func) { var handle = GetHandleForUse(); bool addedRef = false; @@ -204,6 +204,11 @@ namespace Regorus } } + internal T UseHandleForInterop(Func func) + { + return UseHandle(func); + } + private void UseHandle(Action action) { UseHandle(handlePtr => diff --git a/bindings/csharp/Regorus/Engine.cs b/bindings/csharp/Regorus/Engine.cs index acee729..fbfec73 100644 --- a/bindings/csharp/Regorus/Engine.cs +++ b/bindings/csharp/Regorus/Engine.cs @@ -441,7 +441,7 @@ namespace Regorus } } - private RegorusEngineHandle GetHandleForUse() + internal RegorusEngineHandle GetHandleForUse() { var handle = _handle; if (handle is null || handle.IsClosed || handle.IsInvalid) @@ -451,7 +451,7 @@ namespace Regorus return handle; } - private void UseHandle(Action action) + internal void UseHandle(Action action) { UseHandle(handlePtr => { @@ -460,7 +460,7 @@ namespace Regorus }); } - private T UseHandle(Func func) + internal T UseHandle(Func func) { var handle = GetHandleForUse(); bool addedRef = false; @@ -484,5 +484,10 @@ namespace Regorus } } + internal T UseHandleForInterop(Func func) + { + return UseHandle(func); + } + } } diff --git a/bindings/csharp/Regorus/NativeMethods.cs b/bindings/csharp/Regorus/NativeMethods.cs index 0786b9a..44a6249 100644 --- a/bindings/csharp/Regorus/NativeMethods.cs +++ b/bindings/csharp/Regorus/NativeMethods.cs @@ -26,6 +26,13 @@ namespace Regorus.Internal [DllImport(LibraryName, EntryPoint = "regorus_result_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] internal static extern void regorus_result_drop(RegorusResult result); + /// + /// Drop a RegorusBuffer. + /// data is not valid after drop. + /// + [DllImport(LibraryName, EntryPoint = "regorus_buffer_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern void regorus_buffer_drop(RegorusBuffer* buffer); + #endregion #region Memory Limit Methods @@ -85,6 +92,12 @@ namespace Regorus.Internal [DllImport(LibraryName, EntryPoint = "regorus_engine_clone", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] internal static extern RegorusEngine* regorus_engine_clone(RegorusEngine* engine); + /// + /// Compile an RVM program from the engine state with entry points. + /// + [DllImport(LibraryName, EntryPoint = "regorus_engine_compile_program_with_entrypoints", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_engine_compile_program_with_entrypoints(RegorusEngine* engine, byte** entryPoints, UIntPtr entryPointsLen); + /// /// Drop a RegorusEngine. /// @@ -92,6 +105,138 @@ namespace Regorus.Internal internal static extern void regorus_engine_drop(RegorusEngine* engine); /// + + /// + /// Compile an RVM program from data/modules and entry points. + /// + [DllImport(LibraryName, EntryPoint = "regorus_program_compile_from_modules", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_program_compile_from_modules(byte* data_json, RegorusPolicyModule* modules, UIntPtr modules_len, byte** entry_points, UIntPtr entry_points_len); + + /// + /// Construct a new empty program. + /// + [DllImport(LibraryName, EntryPoint = "regorus_program_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusProgram* regorus_program_new(); + + /// + /// Drop a program handle. + /// + [DllImport(LibraryName, EntryPoint = "regorus_program_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern void regorus_program_drop(RegorusProgram* program); + + /// + /// Serialize a program to binary format. + /// + [DllImport(LibraryName, EntryPoint = "regorus_program_serialize_binary", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_program_serialize_binary(RegorusProgram* program); + + /// + /// Deserialize a program from binary format. + /// + [DllImport(LibraryName, EntryPoint = "regorus_program_deserialize_binary", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_program_deserialize_binary(byte* data, UIntPtr len, byte* is_partial); + + /// + /// Generate a readable assembly listing for the program. + /// + [DllImport(LibraryName, EntryPoint = "regorus_program_generate_listing", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_program_generate_listing(RegorusProgram* program); + + /// + /// Create a new RVM instance. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusRvm* regorus_rvm_new(); + + /// + /// Create a new RVM instance from a compiled policy. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_new_with_policy", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_new_with_policy(RegorusCompiledPolicy* compiled_policy); + + /// + /// Drop an RVM instance. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern void regorus_rvm_drop(RegorusRvm* vm); + + /// + /// Load a program into the RVM. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_load_program", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_load_program(RegorusRvm* vm, RegorusProgram* program); + + /// + /// Set the data document for the RVM. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_set_data", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_set_data(RegorusRvm* vm, byte* data_json); + + /// + /// Set the input document for the RVM. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_set_input", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_set_input(RegorusRvm* vm, byte* input_json); + + /// + /// Execute the program. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_execute", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_execute(RegorusRvm* vm); + + /// + /// Execute an entry point by name. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_execute_entry_point_by_name", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_execute_entry_point_by_name(RegorusRvm* vm, byte* entry_point); + + /// + /// Execute an entry point by index. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_execute_entry_point_by_index", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_execute_entry_point_by_index(RegorusRvm* vm, UIntPtr index); + + /// + /// Resume execution. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_resume", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_resume(RegorusRvm* vm, byte* resume_value_json, [MarshalAs(UnmanagedType.I1)] bool has_value); + + /// + /// Get the current execution state. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_get_execution_state", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_get_execution_state(RegorusRvm* vm); + + /// + /// Set the maximum instruction limit. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_set_max_instructions", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_set_max_instructions(RegorusRvm* vm, UIntPtr max_instructions); + + /// + /// Set strict builtin error handling. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_set_strict_builtin_errors", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_set_strict_builtin_errors(RegorusRvm* vm, [MarshalAs(UnmanagedType.I1)] bool strict); + + /// + /// Set execution mode (0 run-to-completion, 1 suspendable). + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_set_execution_mode", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_set_execution_mode(RegorusRvm* vm, byte mode); + + /// + /// Set step mode for suspendable execution. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_set_step_mode", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_set_step_mode(RegorusRvm* vm, [MarshalAs(UnmanagedType.I1)] bool enabled); + + /// + /// Set execution timer configuration. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_set_execution_timer_config", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_set_execution_timer_config(RegorusRvm* vm, [MarshalAs(UnmanagedType.I1)] bool has_config, RegorusExecutionTimerConfig config); /// Add a policy. /// The policy is parsed into AST. /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_policy @@ -617,6 +762,17 @@ namespace Regorus.Internal public uint check_interval; } + /// + /// Byte buffer returned from FFI. + /// + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct RegorusBuffer + { + public byte* data; + public UIntPtr len; + public UIntPtr capacity; + } + /// /// Wrapper for regorus::Engine. /// @@ -633,6 +789,22 @@ namespace Regorus.Internal { } + /// + /// Wrapper for regorus::rvm::Program. + /// + [StructLayout(LayoutKind.Sequential)] + internal unsafe partial struct RegorusProgram + { + } + + /// + /// Wrapper for regorus::rvm::RegoVM. + /// + [StructLayout(LayoutKind.Sequential)] + internal unsafe partial struct RegorusRvm + { + } + /// /// FFI wrapper for PolicyModule struct. /// diff --git a/bindings/csharp/Regorus/Program.cs b/bindings/csharp/Regorus/Program.cs new file mode 100644 index 0000000..b9d7f0b --- /dev/null +++ b/bindings/csharp/Regorus/Program.cs @@ -0,0 +1,333 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Regorus.Internal; + +#nullable enable +namespace Regorus +{ + /// + /// Represents a compiled RVM program. + /// + public unsafe sealed class Program : IDisposable + { + private RegorusProgramHandle? _handle; + private int _isDisposed; + + private Program(RegorusProgramHandle handle) + { + _handle = handle ?? throw new ArgumentNullException(nameof(handle)); + } + + /// + /// Create an empty program. + /// + public static Program CreateEmpty() + { + return new Program(RegorusProgramHandle.Create()); + } + + /// + /// Compile an RVM program from modules and entry points. + /// + public static Program CompileFromModules(string dataJson, IEnumerable modules, IEnumerable entryPoints) + { + var modulesArray = modules.ToArray(); + var entryPointsArray = entryPoints.ToArray(); + if (entryPointsArray.Length == 0) + { + throw new ArgumentException("At least one entry point is required.", nameof(entryPoints)); + } + + var nativeModules = new RegorusPolicyModule[modulesArray.Length]; + var pinnedStrings = new List(modulesArray.Length * 2 + entryPointsArray.Length); + var entryPointers = new IntPtr[entryPointsArray.Length]; + + try + { + for (int i = 0; i < modulesArray.Length; i++) + { + var idPinned = Utf8Marshaller.Pin(modulesArray[i].Id); + var contentPinned = Utf8Marshaller.Pin(modulesArray[i].Content); + pinnedStrings.Add(idPinned); + pinnedStrings.Add(contentPinned); + + nativeModules[i] = new RegorusPolicyModule + { + id = idPinned.Pointer, + content = contentPinned.Pointer + }; + } + + for (int i = 0; i < entryPointsArray.Length; i++) + { + var entryPinned = Utf8Marshaller.Pin(entryPointsArray[i]); + pinnedStrings.Add(entryPinned); + entryPointers[i] = (IntPtr)entryPinned.Pointer; + } + + return Utf8Marshaller.WithUtf8(dataJson, dataPtr => + { + fixed (RegorusPolicyModule* modulesPtr = nativeModules) + fixed (IntPtr* entryPtr = entryPointers) + { + var result = API.regorus_program_compile_from_modules( + (byte*)dataPtr, + modulesPtr, + (UIntPtr)modulesArray.Length, + (byte**)entryPtr, + (UIntPtr)entryPointsArray.Length); + + return GetProgramResult(result); + } + }); + } + finally + { + foreach (var pinned in pinnedStrings) + { + pinned.Dispose(); + } + } + } + + /// + /// Compile an RVM program from an engine instance and entry points. + /// + public static Program CompileFromEngine(Engine engine, IEnumerable entryPoints) + { + if (engine is null) + { + throw new ArgumentNullException(nameof(engine)); + } + + var entryPointsArray = entryPoints.ToArray(); + if (entryPointsArray.Length == 0) + { + throw new ArgumentException("At least one entry point is required.", nameof(entryPoints)); + } + + var pinnedStrings = new List(entryPointsArray.Length); + var entryPointers = new IntPtr[entryPointsArray.Length]; + try + { + for (int i = 0; i < entryPointsArray.Length; i++) + { + var entryPinned = Utf8Marshaller.Pin(entryPointsArray[i]); + pinnedStrings.Add(entryPinned); + entryPointers[i] = (IntPtr)entryPinned.Pointer; + } + + return engine.UseHandleForInterop(enginePtr => + { + fixed (IntPtr* entryPtr = entryPointers) + { + var result = API.regorus_engine_compile_program_with_entrypoints( + (RegorusEngine*)enginePtr, + (byte**)entryPtr, + (UIntPtr)entryPointsArray.Length); + + return GetProgramResult(result); + } + }); + } + finally + { + foreach (var pinned in pinnedStrings) + { + pinned.Dispose(); + } + } + } + + /// + /// Deserialize an RVM program from binary format. + /// + public static Program DeserializeBinary(byte[] data, out bool isPartial) + { + if (data is null) + { + throw new ArgumentNullException(nameof(data)); + } + + byte partialFlag = 0; + fixed (byte* dataPtr = data) + { + var result = API.regorus_program_deserialize_binary(dataPtr, (UIntPtr)data.Length, &partialFlag); + var program = GetProgramResult(result); + isPartial = partialFlag != 0; + return program; + } + } + + /// + /// Serialize the program to binary format. + /// + public byte[] SerializeBinary() + { + ThrowIfDisposed(); + return UseHandle(programPtr => + { + var result = API.regorus_program_serialize_binary((RegorusProgram*)programPtr); + return ExtractBuffer(result); + }); + } + + /// + /// Generate a readable assembly listing. + /// + public string? GenerateListing() + { + ThrowIfDisposed(); + return UseHandle(programPtr => + { + return CheckAndDropResult(API.regorus_program_generate_listing((RegorusProgram*)programPtr)); + }); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) + { + _handle?.Dispose(); + _handle = null; + } + } + + private void ThrowIfDisposed() + { + if (_isDisposed != 0 || _handle is null || _handle.IsClosed) + { + throw new ObjectDisposedException(nameof(Program)); + } + } + + internal RegorusProgramHandle GetHandleForUse() + { + var handle = _handle; + if (handle is null || handle.IsClosed || handle.IsInvalid) + { + throw new ObjectDisposedException(nameof(Program)); + } + return handle; + } + + internal T UseHandle(Func func) + { + var handle = GetHandleForUse(); + bool addedRef = false; + try + { + handle.DangerousAddRef(ref addedRef); + var pointer = handle.DangerousGetHandle(); + if (pointer == IntPtr.Zero) + { + throw new ObjectDisposedException(nameof(Program)); + } + + return func(pointer); + } + finally + { + if (addedRef) + { + handle.DangerousRelease(); + } + } + } + + private static Program GetProgramResult(RegorusResult result) + { + try + { + if (result.status != RegorusStatus.Ok) + { + var message = Utf8Marshaller.FromUtf8(result.error_message); + throw result.status.CreateException(message); + } + + if (result.data_type != RegorusDataType.Pointer || result.pointer_value == null) + { + throw new Exception("Expected program pointer but got different data type"); + } + + var handle = RegorusProgramHandle.FromPointer((IntPtr)result.pointer_value); + return new Program(handle); + } + finally + { + API.regorus_result_drop(result); + } + } + + private static string? CheckAndDropResult(RegorusResult result) + { + try + { + if (result.status != RegorusStatus.Ok) + { + var message = Utf8Marshaller.FromUtf8(result.error_message); + throw result.status.CreateException(message); + } + + return result.data_type switch + { + RegorusDataType.String => Utf8Marshaller.FromUtf8(result.output), + RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(), + RegorusDataType.Integer => result.int_value.ToString(), + RegorusDataType.None => null, + _ => Utf8Marshaller.FromUtf8(result.output) + }; + } + finally + { + API.regorus_result_drop(result); + } + } + + private static byte[] ExtractBuffer(RegorusResult result) + { + RegorusBuffer* buffer = null; + try + { + if (result.status != RegorusStatus.Ok) + { + var message = Utf8Marshaller.FromUtf8(result.error_message); + throw result.status.CreateException(message); + } + + if (result.data_type != RegorusDataType.Pointer || result.pointer_value == null) + { + throw new Exception("Expected buffer pointer but got different data type"); + } + + buffer = (RegorusBuffer*)result.pointer_value; + var length = checked((int)buffer->len); + var data = new byte[length]; + if (length > 0) + { + Marshal.Copy((IntPtr)buffer->data, data, 0, length); + } + return data; + } + finally + { + if (buffer != null) + { + API.regorus_buffer_drop(buffer); + } + API.regorus_result_drop(result); + } + } + } +} diff --git a/bindings/csharp/Regorus/Rvm.cs b/bindings/csharp/Regorus/Rvm.cs new file mode 100644 index 0000000..b649070 --- /dev/null +++ b/bindings/csharp/Regorus/Rvm.cs @@ -0,0 +1,292 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Regorus.Internal; + +#nullable enable +namespace Regorus +{ + /// + /// Wrapper for the Regorus RVM runtime. + /// + public unsafe sealed class Rvm : IDisposable + { + private RegorusRvmHandle? _handle; + private int _isDisposed; + + public Rvm() + { + _handle = RegorusRvmHandle.Create(); + } + + private Rvm(RegorusRvmHandle handle) + { + _handle = handle ?? throw new ArgumentNullException(nameof(handle)); + } + + /// + /// Create an RVM instance backed by a compiled policy (for default rule evaluation). + /// + public static Rvm CreateWithPolicy(CompiledPolicy policy) + { + if (policy is null) + { + throw new ArgumentNullException(nameof(policy)); + } + + return policy.UseHandleForInterop(policyPtr => + { + var result = API.regorus_rvm_new_with_policy((RegorusCompiledPolicy*)policyPtr); + return GetRvmResult(result); + }); + } + + /// + /// Load a program into the VM. + /// + public void LoadProgram(Program program) + { + ThrowIfDisposed(); + if (program is null) + { + throw new ArgumentNullException(nameof(program)); + } + + program.UseHandle(programPtr => + { + UseHandle(vmPtr => + { + CheckAndDropResult(API.regorus_rvm_load_program((RegorusRvm*)vmPtr, (RegorusProgram*)programPtr)); + return 0; + }); + return 0; + }); + } + + /// + /// Set the data document for the VM. + /// + public void SetDataJson(string dataJson) + { + ThrowIfDisposed(); + Utf8Marshaller.WithUtf8(dataJson, dataPtr => + { + UseHandle(vmPtr => + { + CheckAndDropResult(API.regorus_rvm_set_data((RegorusRvm*)vmPtr, (byte*)dataPtr)); + return 0; + }); + }); + } + + /// + /// Set the input document for the VM. + /// + public void SetInputJson(string inputJson) + { + ThrowIfDisposed(); + Utf8Marshaller.WithUtf8(inputJson, inputPtr => + { + UseHandle(vmPtr => + { + CheckAndDropResult(API.regorus_rvm_set_input((RegorusRvm*)vmPtr, (byte*)inputPtr)); + return 0; + }); + }); + } + + /// + /// Set the execution mode (0 = run-to-completion, 1 = suspendable). + /// + public void SetExecutionMode(byte mode) + { + ThrowIfDisposed(); + UseHandle(vmPtr => + { + CheckAndDropResult(API.regorus_rvm_set_execution_mode((RegorusRvm*)vmPtr, mode)); + return 0; + }); + } + + /// + /// Execute the program and return the JSON result. + /// + public string? Execute() + { + ThrowIfDisposed(); + return UseHandle(vmPtr => + { + return CheckAndDropResult(API.regorus_rvm_execute((RegorusRvm*)vmPtr)); + }); + } + + /// + /// Execute a named entry point. + /// + public string? ExecuteEntryPoint(string entryPoint) + { + ThrowIfDisposed(); + return Utf8Marshaller.WithUtf8(entryPoint, entryPtr => + { + return UseHandle(vmPtr => + { + return CheckAndDropResult(API.regorus_rvm_execute_entry_point_by_name((RegorusRvm*)vmPtr, (byte*)entryPtr)); + }); + }); + } + + /// + /// Execute an entry point by index. + /// + public string? ExecuteEntryPoint(ulong index) + { + ThrowIfDisposed(); + return UseHandle(vmPtr => + { + return CheckAndDropResult(API.regorus_rvm_execute_entry_point_by_index((RegorusRvm*)vmPtr, (UIntPtr)index)); + }); + } + + /// + /// Resume execution with an optional value. + /// + public string? Resume(string? resumeValueJson) + { + ThrowIfDisposed(); + if (resumeValueJson is null) + { + return UseHandle(vmPtr => + { + return CheckAndDropResult(API.regorus_rvm_resume((RegorusRvm*)vmPtr, null, has_value: false)); + }); + } + + return Utf8Marshaller.WithUtf8(resumeValueJson, valuePtr => + { + return UseHandle(vmPtr => + { + return CheckAndDropResult(API.regorus_rvm_resume((RegorusRvm*)vmPtr, (byte*)valuePtr, has_value: true)); + }); + }); + } + + /// + /// Get the current execution state. + /// + public string? GetExecutionState() + { + ThrowIfDisposed(); + return UseHandle(vmPtr => + { + return CheckAndDropResult(API.regorus_rvm_get_execution_state((RegorusRvm*)vmPtr)); + }); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (System.Threading.Interlocked.CompareExchange(ref _isDisposed, 1, 0) == 0) + { + _handle?.Dispose(); + _handle = null; + } + } + + private void ThrowIfDisposed() + { + if (_isDisposed != 0 || _handle is null || _handle.IsClosed) + { + throw new ObjectDisposedException(nameof(Rvm)); + } + } + + internal RegorusRvmHandle GetHandleForUse() + { + var handle = _handle; + if (handle is null || handle.IsClosed || handle.IsInvalid) + { + throw new ObjectDisposedException(nameof(Rvm)); + } + return handle; + } + + internal T UseHandle(Func func) + { + var handle = GetHandleForUse(); + bool addedRef = false; + try + { + handle.DangerousAddRef(ref addedRef); + var pointer = handle.DangerousGetHandle(); + if (pointer == IntPtr.Zero) + { + throw new ObjectDisposedException(nameof(Rvm)); + } + + return func(pointer); + } + finally + { + if (addedRef) + { + handle.DangerousRelease(); + } + } + } + + private static Rvm GetRvmResult(RegorusResult result) + { + try + { + if (result.status != RegorusStatus.Ok) + { + var message = Utf8Marshaller.FromUtf8(result.error_message); + throw result.status.CreateException(message); + } + + if (result.data_type != RegorusDataType.Pointer || result.pointer_value == null) + { + throw new Exception("Expected RVM pointer but got different data type"); + } + + var handle = RegorusRvmHandle.FromPointer((IntPtr)result.pointer_value); + return new Rvm(handle); + } + finally + { + API.regorus_result_drop(result); + } + } + + private static string? CheckAndDropResult(RegorusResult result) + { + try + { + if (result.status != RegorusStatus.Ok) + { + var message = Utf8Marshaller.FromUtf8(result.error_message); + throw result.status.CreateException(message); + } + + return result.data_type switch + { + RegorusDataType.String => Utf8Marshaller.FromUtf8(result.output), + RegorusDataType.Boolean => result.bool_value.ToString().ToLowerInvariant(), + RegorusDataType.Integer => result.int_value.ToString(), + RegorusDataType.None => null, + _ => Utf8Marshaller.FromUtf8(result.output) + }; + } + finally + { + API.regorus_result_drop(result); + } + } + } +} diff --git a/bindings/csharp/Regorus/SafeHandles.cs b/bindings/csharp/Regorus/SafeHandles.cs index 4fbcf17..cd38f3d 100644 --- a/bindings/csharp/Regorus/SafeHandles.cs +++ b/bindings/csharp/Regorus/SafeHandles.cs @@ -87,4 +87,100 @@ namespace Regorus return true; } } + + internal sealed class RegorusProgramHandle : SafeHandleZeroOrMinusOneIsInvalid + { + private RegorusProgramHandle() : base(ownsHandle: true) + { + } + + internal static RegorusProgramHandle Create() + { + unsafe + { + var raw = Internal.API.regorus_program_new(); + if (raw is null) + { + throw new InvalidOperationException("Failed to create Regorus program."); + } + + var handle = new RegorusProgramHandle(); + handle.SetHandle((IntPtr)raw); + return handle; + } + } + + internal static RegorusProgramHandle FromPointer(IntPtr pointer) + { + if (pointer == IntPtr.Zero) + { + throw new ArgumentException("Pointer cannot be zero.", nameof(pointer)); + } + + var handle = new RegorusProgramHandle(); + handle.SetHandle(pointer); + return handle; + } + + protected override bool ReleaseHandle() + { + if (!IsInvalid && !IsClosed) + { + unsafe + { + Internal.API.regorus_program_drop((Internal.RegorusProgram*)handle); + } + SetHandle(IntPtr.Zero); + } + return true; + } + } + + internal sealed class RegorusRvmHandle : SafeHandleZeroOrMinusOneIsInvalid + { + private RegorusRvmHandle() : base(ownsHandle: true) + { + } + + internal static RegorusRvmHandle Create() + { + unsafe + { + var raw = Internal.API.regorus_rvm_new(); + if (raw is null) + { + throw new InvalidOperationException("Failed to create Regorus RVM."); + } + + var handle = new RegorusRvmHandle(); + handle.SetHandle((IntPtr)raw); + return handle; + } + } + + internal static RegorusRvmHandle FromPointer(IntPtr pointer) + { + if (pointer == IntPtr.Zero) + { + throw new ArgumentException("Pointer cannot be zero.", nameof(pointer)); + } + + var handle = new RegorusRvmHandle(); + handle.SetHandle(pointer); + return handle; + } + + protected override bool ReleaseHandle() + { + if (!IsInvalid && !IsClosed) + { + unsafe + { + Internal.API.regorus_rvm_drop((Internal.RegorusRvm*)handle); + } + SetHandle(IntPtr.Zero); + } + return true; + } + } } diff --git a/bindings/csharp/TargetExampleApp/Program.cs b/bindings/csharp/TargetExampleApp/Program.cs index 588fbdf..945ab02 100644 --- a/bindings/csharp/TargetExampleApp/Program.cs +++ b/bindings/csharp/TargetExampleApp/Program.cs @@ -65,6 +65,55 @@ triplet_count := count([1 | private const string EXECUTION_TIMER_QUERY = "data.limits.timer.triplet_count"; private const int EXECUTION_TIMER_VALUE_COUNT = 40; + private const string RVM_POLICY = """ +package demo +import rego.v1 + +default allow := false + +allow if { + input.user == "alice" + some role in data.roles[input.user] + role == "admin" +} +"""; + + private const string RVM_DATA = """ +{ + "roles": { + "alice": ["admin", "reader"] + } +} +"""; + + private const string RVM_INPUT = """ +{ + "user": "alice" +} +"""; + + private const string HOST_AWAIT_POLICY = """ +package demo +import rego.v1 + +default allow := false + +allow if { + input.account.active == true + details := __builtin_host_await(input.account.id, "account") + details.tier == "gold" +} +"""; + + private const string HOST_AWAIT_INPUT = """ + { + "account": { + "id": "acct-1", + "active": true + } + } + """; + // Test data constants private const string COMPLIANT_STORAGE_ACCOUNT = @"{ ""type"": ""Microsoft.Storage/storageAccounts"", @@ -174,6 +223,15 @@ triplet_count := count([1 | Console.WriteLine("\n5. Execution timer configuration:"); DemonstrateExecutionTimer(); + + Console.WriteLine("\n6. RVM program execution:"); + DemonstrateRvmUsage(); + + Console.WriteLine("\n7. RVM program compilation from engine:"); + DemonstrateRvmCompileFromEngine(); + + Console.WriteLine("\n8. RVM host await (suspend/resume):"); + DemonstrateRvmHostAwait(); } static void DemonstrateConcurrentEvaluation(Regorus.CompiledPolicy compiledPolicy) @@ -359,4 +417,77 @@ triplet_count := count([1 | Regorus.Engine.ClearFallbackExecutionTimerConfig(); } } + + static void DemonstrateRvmUsage() + { + var modules = new List + { + new Regorus.PolicyModule("demo.rego", RVM_POLICY) + }; + var entryPoints = new[] { "data.demo.allow" }; + + using var program = Regorus.Program.CompileFromModules(RVM_DATA, modules, entryPoints); + var binary = program.SerializeBinary(); + using var rehydrated = Regorus.Program.DeserializeBinary(binary, out var isPartial); + if (isPartial) + { + throw new InvalidOperationException("RVM program deserialization returned a partial program."); + } + + Console.WriteLine($"Serialized program size: {binary.Length} bytes"); + + var listing = rehydrated.GenerateListing(); + + Console.WriteLine("RVM listing:"); + Console.WriteLine(listing); + + using var vm = new Regorus.Rvm(); + vm.LoadProgram(rehydrated); + vm.SetDataJson(RVM_DATA); + vm.SetInputJson(RVM_INPUT); + + var result = vm.Execute(); + Console.WriteLine($"RVM result: {result}"); + } + + static void DemonstrateRvmCompileFromEngine() + { + using var engine = new Regorus.Engine(); + engine.AddPolicy("demo.rego", RVM_POLICY); + engine.AddDataJson(RVM_DATA); + + var entryPoints = new[] { "data.demo.allow" }; + using var program = Regorus.Program.CompileFromEngine(engine, entryPoints); + + using var vm = new Regorus.Rvm(); + vm.LoadProgram(program); + vm.SetDataJson(RVM_DATA); + vm.SetInputJson(RVM_INPUT); + + var result = vm.ExecuteEntryPoint("data.demo.allow"); + Console.WriteLine($"RVM result from engine-compiled program: {result}"); + } + + static void DemonstrateRvmHostAwait() + { + var modules = new List + { + new Regorus.PolicyModule("host_await.rego", HOST_AWAIT_POLICY) + }; + var entryPoints = new[] { "data.demo.allow" }; + + using var program = Regorus.Program.CompileFromModules("{}", modules, entryPoints); + using var vm = new Regorus.Rvm(); + vm.SetExecutionMode(1); + vm.LoadProgram(program); + vm.SetInputJson(HOST_AWAIT_INPUT); + + var initial = vm.Execute(); + var state = vm.GetExecutionState(); + Console.WriteLine($"HostAwait initial result: {initial}"); + Console.WriteLine($"Execution state: {state}"); + + var resumed = vm.Resume("{\"tier\":\"gold\"}"); + Console.WriteLine($"HostAwait resumed result: {resumed}"); + } } diff --git a/bindings/ffi/Cargo.lock b/bindings/ffi/Cargo.lock index c56e150..c8d1446 100644 --- a/bindings/ffi/Cargo.lock +++ b/bindings/ffi/Cargo.lock @@ -102,6 +102,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "serde", + "unty", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -172,9 +182,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.54" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "shlex", @@ -211,18 +221,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.54" +version = "4.5.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.54" +version = "4.5.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" dependencies = [ "anstream", "anstyle", @@ -329,9 +339,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fluent-uri" @@ -407,9 +417,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -539,6 +549,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] [[package]] @@ -941,11 +953,13 @@ name = "regorus" version = "0.9.0" dependencies = [ "anyhow", + "bincode", "chrono", "chrono-tz", "dashmap", "data-encoding", "globset", + "indexmap", "ipnet", "jsonschema", "lazy_static", @@ -1244,6 +1258,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.8" @@ -1468,18 +1488,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.34" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" +checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.34" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" +checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" dependencies = [ "proc-macro2", "quote", diff --git a/bindings/ffi/Cargo.toml b/bindings/ffi/Cargo.toml index 08b4c89..d83f9ca 100644 --- a/bindings/ffi/Cargo.toml +++ b/bindings/ffi/Cargo.toml @@ -30,6 +30,7 @@ default = [ "std", "coverage", "allocator-memory-limits", + "rvm", "regorus/arc", "regorus/full-opa", "contention_checks", @@ -40,6 +41,7 @@ std = ["regorus/std"] coverage = ["regorus/coverage"] allocator-memory-limits = ["regorus/allocator-memory-limits"] contention_checks = ["parking_lot"] +rvm = ["regorus/rvm"] custom_allocator = [] [build-dependencies] diff --git a/bindings/ffi/src/common.rs b/bindings/ffi/src/common.rs index 11bc698..10ce9f7 100644 --- a/bindings/ffi/src/common.rs +++ b/bindings/ffi/src/common.rs @@ -1,12 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use alloc::boxed::Box; use alloc::ffi::CString; use alloc::format; use alloc::string::{String, ToString}; +use alloc::vec::Vec; use anyhow::{anyhow, bail, Result}; use core::ffi::{c_char, c_longlong, c_void, CStr}; -use core::ptr; +use core::{mem, ptr}; /// Status of a call on `RegorusEngine`. #[repr(C)] @@ -90,6 +92,19 @@ pub struct RegorusResult { pub(crate) error_message: *mut c_char, } +/// Byte buffer returned from FFI for binary payloads. +/// +/// Must be freed using `regorus_buffer_drop`. +#[repr(C)] +pub struct RegorusBuffer { + /// Pointer to byte buffer data. + pub data: *mut u8, + /// Number of bytes stored in `data`. + pub len: usize, + /// Capacity of the allocation backing `data`. + pub capacity: usize, +} + impl RegorusResult { /// Create a successful result with no data. pub(crate) fn ok_void() -> Self { @@ -185,6 +200,18 @@ impl RegorusResult { } } +impl RegorusBuffer { + pub(crate) fn from_vec(mut data: Vec) -> *mut RegorusBuffer { + let buffer = RegorusBuffer { + data: data.as_mut_ptr(), + len: data.len(), + capacity: data.capacity(), + }; + mem::forget(data); + Box::into_raw(Box::new(buffer)) + } +} + pub(crate) fn to_c_str(s: String) -> *mut c_char { match CString::new(s) { Ok(cs) => cs.into_raw(), @@ -222,6 +249,21 @@ pub(crate) fn to_regorus_string_result(r: Result) -> RegorusResult { } } +/// Drop a `RegorusBuffer`. +/// +/// `data` is not valid after drop. +#[no_mangle] +pub extern "C" fn regorus_buffer_drop(buffer: *mut RegorusBuffer) { + if let Ok(buffer) = to_ref(buffer) { + unsafe { + if !buffer.data.is_null() { + let _ = Vec::from_raw_parts(buffer.data, buffer.len, buffer.capacity); + } + let _ = Box::from_raw(ptr::from_mut(buffer)); + } + } +} + /// Drop a `RegorusResult`. /// /// `output` and `error_message` strings are not valid after drop. diff --git a/bindings/ffi/src/engine.rs b/bindings/ffi/src/engine.rs index 404d2c5..817d41e 100644 --- a/bindings/ffi/src/engine.rs +++ b/bindings/ffi/src/engine.rs @@ -11,9 +11,16 @@ use crate::panic_guard::with_unwind_guard; use alloc::boxed::Box; use alloc::format; use alloc::string::String; +#[cfg(feature = "rvm")] +use alloc::sync::Arc; +use alloc::vec::Vec; use anyhow::{anyhow, Result}; use core::ffi::{c_char, c_void}; use core::ptr; +#[cfg(feature = "rvm")] +use regorus::languages::rego::compiler::Compiler; +#[cfg(feature = "rvm")] +use regorus::rvm::program::Program; /// Wrapper for `regorus::Engine`. pub struct RegorusEngine { @@ -720,3 +727,65 @@ pub extern "C" fn regorus_engine_compile_with_entrypoint( } }) } + +/// Compile an RVM program from the engine state with entry points. +/// +/// * `entry_points` - Array of entry point rule paths +/// * `entry_points_len` - Number of entry points +#[cfg(feature = "rvm")] +#[no_mangle] +pub extern "C" fn regorus_engine_compile_program_with_entrypoints( + engine: *mut RegorusEngine, + entry_points: *const *const c_char, + entry_points_len: usize, +) -> RegorusResult { + with_unwind_guard(|| { + let result = || -> Result> { + if entry_points_len == 0 { + return Err(anyhow!("entry_points must contain at least one entry")); + } + + if entry_points.is_null() && entry_points_len > 0 { + return Err(anyhow!("null entry_points pointer")); + } + + let mut entry_points_vec = Vec::with_capacity(entry_points_len); + for i in 0..entry_points_len { + unsafe { + let entry_ptr = entry_points.add(i); + if entry_ptr.is_null() { + return Err(anyhow!("null entry point at index {i}")); + } + let entry = from_c_str(*entry_ptr)?; + entry_points_vec.push(entry); + } + } + + let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect(); + + let rule = entry_points_ref + .first() + .ok_or_else(|| anyhow!("entry_points must contain at least one entry"))?; + let rule_rc: regorus::Rc = (*rule).into(); + + let engine = to_ref(engine)?; + let mut guard = engine.try_write()?; + let compiled_policy = guard.compile_with_entrypoint(&rule_rc)?; + + let program = Compiler::compile_from_policy(&compiled_policy, &entry_points_ref)?; + Ok(program) + }(); + + match result { + Ok(program) => { + let wrapped = crate::rvm::RegorusProgram { program }; + let boxed = Box::new(wrapped); + RegorusResult::ok_pointer(Box::into_raw(boxed) as *mut c_void) + } + Err(e) => RegorusResult::err_with_message( + RegorusStatus::CompilationFailed, + format!("Failed to compile RVM program: {e}"), + ), + } + }) +} diff --git a/bindings/ffi/src/lib.rs b/bindings/ffi/src/lib.rs index 6f76203..84f3d6e 100644 --- a/bindings/ffi/src/lib.rs +++ b/bindings/ffi/src/lib.rs @@ -14,5 +14,7 @@ mod engine; mod limits; mod lock; mod panic_guard; +#[cfg(feature = "rvm")] +pub(crate) mod rvm; mod schema_registry; mod target_registry; diff --git a/bindings/ffi/src/rvm.rs b/bindings/ffi/src/rvm.rs new file mode 100644 index 0000000..c47182d --- /dev/null +++ b/bindings/ffi/src/rvm.rs @@ -0,0 +1,606 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::common::{ + from_c_str, to_ref, to_regorus_result, RegorusBuffer, RegorusResult, RegorusStatus, +}; +use crate::compile::RegorusPolicyModule; +use crate::compiled_policy::RegorusCompiledPolicy; +use crate::limits::RegorusExecutionTimerConfig; +use crate::lock::{new_handle, try_read, try_write, Handle, ReadGuard, WriteGuard}; +use crate::panic_guard::with_unwind_guard; +use alloc::boxed::Box; +use alloc::format; +use alloc::string::String; +use alloc::sync::Arc; +use alloc::vec::Vec; +use anyhow::{anyhow, Result}; +use core::ffi::{c_char, c_void}; +use core::ptr; +use regorus::languages::rego::compiler::Compiler; +use regorus::rvm::program::{ + generate_assembly_listing, generate_tabular_assembly_listing, AssemblyListingConfig, + DeserializationResult, Program, +}; +use regorus::rvm::vm::{ExecutionMode, ExecutionState, RegoVM}; +use regorus::PolicyModule; +use regorus::Value; + +/// Wrapper for `regorus::rvm::Program`. +#[derive(Clone)] +pub struct RegorusProgram { + pub(crate) program: Arc, +} + +/// Wrapper for `regorus::rvm::RegoVM`. +pub struct RegorusRvm { + vm: Handle, +} + +impl RegorusRvm { + fn new(vm: RegoVM) -> Self { + Self { vm: new_handle(vm) } + } + + fn contention_error() -> anyhow::Error { + anyhow!("regorus rvm handle is already in use; create a separate VM per thread") + } + + fn try_write(&self) -> Result> { + try_write(&self.vm).ok_or_else(Self::contention_error) + } + + fn try_read(&self) -> Result> { + try_read(&self.vm).ok_or_else(Self::contention_error) + } +} + +/// Drop a `RegorusProgram`. +#[no_mangle] +pub extern "C" fn regorus_program_drop(program: *mut RegorusProgram) { + if let Ok(program) = to_ref(program) { + unsafe { + let _ = Box::from_raw(ptr::from_mut(program)); + } + } +} + +/// Drop a `RegorusRvm`. +#[no_mangle] +pub extern "C" fn regorus_rvm_drop(vm: *mut RegorusRvm) { + if let Ok(vm) = to_ref(vm) { + unsafe { + let _ = Box::from_raw(ptr::from_mut(vm)); + } + } +} + +/// Compile a compiled policy into an RVM program. +/// +/// * `compiled_policy` - Compiled policy handle +/// * `entry_points` - Array of entry point rule paths +/// * `entry_points_len` - Number of entry points +#[no_mangle] +pub extern "C" fn regorus_program_compile_from_policy( + compiled_policy: *mut RegorusCompiledPolicy, + entry_points: *const *const c_char, + entry_points_len: usize, +) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result<*mut RegorusProgram> { + if entry_points.is_null() && entry_points_len > 0 { + return Err(anyhow!("null entry_points pointer")); + } + + let mut entry_points_vec = Vec::with_capacity(entry_points_len); + for i in 0..entry_points_len { + unsafe { + let entry_ptr = entry_points.add(i); + if entry_ptr.is_null() { + return Err(anyhow!("null entry point at index {i}")); + } + let entry = from_c_str(*entry_ptr)?; + entry_points_vec.push(entry); + } + } + + let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect(); + + let compiled_policy = &to_ref(compiled_policy)?.compiled_policy; + let program = Compiler::compile_from_policy(compiled_policy, &entry_points_ref)?; + Ok(Box::into_raw(Box::new(RegorusProgram { program }))) + }(); + + match output { + Ok(program) => RegorusResult::ok_pointer(program as *mut c_void), + Err(err) => RegorusResult::err_with_message( + RegorusStatus::CompilationFailed, + format!("RVM compilation failed: {err}"), + ), + } + }) +} + +/// Compile an RVM program from data/modules and entry points. +/// +/// * `data_json` - JSON string containing static data for policy evaluation +/// * `modules` - Array of policy modules to compile +/// * `modules_len` - Number of modules in the array +/// * `entry_points` - Array of entry point rule paths +/// * `entry_points_len` - Number of entry points +#[no_mangle] +pub extern "C" fn regorus_program_compile_from_modules( + data_json: *const c_char, + modules: *const RegorusPolicyModule, + modules_len: usize, + entry_points: *const *const c_char, + entry_points_len: usize, +) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result<*mut RegorusProgram> { + if entry_points_len == 0 { + return Err(anyhow!("entry_points must contain at least one entry")); + } + + let data_str = from_c_str(data_json)?; + let data = Value::from_json_str(&data_str)?; + let policy_modules = convert_c_modules_to_rust(modules, modules_len)?; + + let entry_points_vec = convert_c_entry_points(entry_points, entry_points_len)?; + let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect(); + + let entry_rule = entry_points_ref + .first() + .ok_or_else(|| anyhow!("entry_points must contain at least one entry"))?; + + let compiled_policy = regorus::compile_policy_with_entrypoint( + data, + &policy_modules, + (*entry_rule).into(), + )?; + + let program = Compiler::compile_from_policy(&compiled_policy, &entry_points_ref)?; + Ok(Box::into_raw(Box::new(RegorusProgram { program }))) + }(); + + match output { + Ok(program) => RegorusResult::ok_pointer(program as *mut c_void), + Err(err) => RegorusResult::err_with_message( + RegorusStatus::CompilationFailed, + format!("RVM compilation failed: {err}"), + ), + } + }) +} + +/// Create a new, empty RVM program. +#[no_mangle] +pub extern "C" fn regorus_program_new() -> *mut RegorusProgram { + let program = Program::new(); + Box::into_raw(Box::new(RegorusProgram { + program: Arc::new(program), + })) +} + +/// Serialize a program to the binary RVM format. +#[no_mangle] +pub extern "C" fn regorus_program_serialize_binary(program: *mut RegorusProgram) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result<*mut RegorusBuffer> { + let program = &to_ref(program)?.program; + let bytes = program.serialize_binary().map_err(|e| anyhow!(e))?; + Ok(RegorusBuffer::from_vec(bytes)) + }(); + + match output { + Ok(buffer) => RegorusResult::ok_pointer(buffer as *mut c_void), + Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{err}")), + } + }) +} + +/// Deserialize a program from the binary RVM format. +/// +/// Returns a `RegorusProgram` handle and sets `is_partial` to true when the +/// program requires recompilation. +#[no_mangle] +pub extern "C" fn regorus_program_deserialize_binary( + data: *const u8, + len: usize, + is_partial: *mut bool, +) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result<(*mut RegorusProgram, bool)> { + if data.is_null() && len > 0 { + return Err(anyhow!("null data pointer")); + } + let data = unsafe { core::slice::from_raw_parts(data, len) }; + let (program, partial) = + match Program::deserialize_binary(data).map_err(|e| anyhow!(e))? { + DeserializationResult::Complete(program) => (program, false), + DeserializationResult::Partial(program) => (program, true), + }; + Ok(( + Box::into_raw(Box::new(RegorusProgram { + program: Arc::new(program), + })), + partial, + )) + }(); + + match output { + Ok((program, partial)) => { + if !is_partial.is_null() { + unsafe { + *is_partial = partial; + } + } + RegorusResult::ok_pointer(program as *mut c_void) + } + Err(err) => { + RegorusResult::err_with_message(RegorusStatus::InvalidDataFormat, err.to_string()) + } + } + }) +} + +/// Generate a default assembly listing for the program. +#[no_mangle] +pub extern "C" fn regorus_program_generate_listing(program: *mut RegorusProgram) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result { + let program = &to_ref(program)?.program; + Ok(generate_assembly_listing( + program, + &AssemblyListingConfig::default(), + )) + }(); + + match output { + Ok(listing) => RegorusResult::ok_string(listing), + Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{err}")), + } + }) +} + +/// Generate a tabular assembly listing for the program. +#[no_mangle] +pub extern "C" fn regorus_program_generate_tabular_listing( + program: *mut RegorusProgram, +) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result { + let program = &to_ref(program)?.program; + Ok(generate_tabular_assembly_listing( + program, + &AssemblyListingConfig::default(), + )) + }(); + + match output { + Ok(listing) => RegorusResult::ok_string(listing), + Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{err}")), + } + }) +} + +/// Construct a new RVM instance. +#[no_mangle] +pub extern "C" fn regorus_rvm_new() -> *mut RegorusRvm { + Box::into_raw(Box::new(RegorusRvm::new(RegoVM::new()))) +} + +/// Construct a new RVM instance with a compiled policy for default rule evaluation. +#[no_mangle] +pub extern "C" fn regorus_rvm_new_with_policy( + compiled_policy: *mut RegorusCompiledPolicy, +) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result<*mut RegorusRvm> { + let policy = to_ref(compiled_policy)?.compiled_policy.clone(); + Ok(Box::into_raw(Box::new(RegorusRvm::new( + RegoVM::new_with_policy(policy), + )))) + }(); + + match output { + Ok(vm) => RegorusResult::ok_pointer(vm as *mut c_void), + Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()), + } + }) +} + +/// Load a program into the RVM. +#[no_mangle] +pub extern "C" fn regorus_rvm_load_program( + vm: *mut RegorusRvm, + program: *mut RegorusProgram, +) -> RegorusResult { + with_unwind_guard(|| { + to_regorus_result(|| -> Result<()> { + let vm = to_ref(vm)?; + let mut guard = vm.try_write()?; + let program = to_ref(program)?.program.clone(); + guard.load_program(program); + Ok(()) + }()) + }) +} + +/// Set the VM data document from JSON. +#[no_mangle] +pub extern "C" fn regorus_rvm_set_data(vm: *mut RegorusRvm, data: *const c_char) -> RegorusResult { + with_unwind_guard(|| { + to_regorus_result(|| -> Result<()> { + let vm = to_ref(vm)?; + let mut guard = vm.try_write()?; + let data_value = Value::from_json_str(&from_c_str(data)?)?; + guard.set_data(data_value)?; + Ok(()) + }()) + }) +} + +/// Set the VM input document from JSON. +#[no_mangle] +pub extern "C" fn regorus_rvm_set_input( + vm: *mut RegorusRvm, + input: *const c_char, +) -> RegorusResult { + with_unwind_guard(|| { + to_regorus_result(|| -> Result<()> { + let vm = to_ref(vm)?; + let mut guard = vm.try_write()?; + let input_value = Value::from_json_str(&from_c_str(input)?)?; + guard.set_input(input_value); + Ok(()) + }()) + }) +} + +/// Set the maximum number of instructions that can execute. +#[no_mangle] +pub extern "C" fn regorus_rvm_set_max_instructions( + vm: *mut RegorusRvm, + max_instructions: usize, +) -> RegorusResult { + with_unwind_guard(|| { + to_regorus_result(|| -> Result<()> { + let vm = to_ref(vm)?; + let mut guard = vm.try_write()?; + guard.set_max_instructions(max_instructions); + Ok(()) + }()) + }) +} + +/// Configure strict builtin error behavior. +#[no_mangle] +pub extern "C" fn regorus_rvm_set_strict_builtin_errors( + vm: *mut RegorusRvm, + strict: bool, +) -> RegorusResult { + with_unwind_guard(|| { + to_regorus_result(|| -> Result<()> { + let vm = to_ref(vm)?; + let mut guard = vm.try_write()?; + guard.set_strict_builtin_errors(strict); + Ok(()) + }()) + }) +} + +/// Configure the execution mode (0 = run-to-completion, 1 = suspendable). +#[no_mangle] +pub extern "C" fn regorus_rvm_set_execution_mode(vm: *mut RegorusRvm, mode: u8) -> RegorusResult { + with_unwind_guard(|| { + to_regorus_result(|| -> Result<()> { + let vm = to_ref(vm)?; + let mut guard = vm.try_write()?; + let mode = match mode { + 0 => ExecutionMode::RunToCompletion, + 1 => ExecutionMode::Suspendable, + _ => return Err(anyhow!("invalid execution mode: {mode}")), + }; + guard.set_execution_mode(mode); + Ok(()) + }()) + }) +} + +/// Enable or disable step mode when running suspendable execution. +#[no_mangle] +pub extern "C" fn regorus_rvm_set_step_mode(vm: *mut RegorusRvm, enabled: bool) -> RegorusResult { + with_unwind_guard(|| { + to_regorus_result(|| -> Result<()> { + let vm = to_ref(vm)?; + let mut guard = vm.try_write()?; + guard.set_step_mode(enabled); + Ok(()) + }()) + }) +} + +/// Configure the per-VM execution timer override. +#[no_mangle] +pub extern "C" fn regorus_rvm_set_execution_timer_config( + vm: *mut RegorusRvm, + has_config: bool, + config: RegorusExecutionTimerConfig, +) -> RegorusResult { + with_unwind_guard(|| { + to_regorus_result(|| -> Result<()> { + let vm = to_ref(vm)?; + let mut guard = vm.try_write()?; + if has_config { + guard.set_execution_timer_config(Some(config.to_execution_timer_config()?)); + } else { + guard.set_execution_timer_config(None); + } + Ok(()) + }()) + }) +} + +/// Execute the program's main entry point. +#[no_mangle] +pub extern "C" fn regorus_rvm_execute(vm: *mut RegorusRvm) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result { + let vm = to_ref(vm)?; + let mut guard = vm.try_write()?; + let result = guard.execute()?; + result.to_json_str() + }(); + + match output { + Ok(json) => RegorusResult::ok_string(json), + Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()), + } + }) +} + +/// Execute a named entry point. +#[no_mangle] +pub extern "C" fn regorus_rvm_execute_entry_point_by_name( + vm: *mut RegorusRvm, + entry_point: *const c_char, +) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result { + let vm = to_ref(vm)?; + let mut guard = vm.try_write()?; + let name = from_c_str(entry_point)?; + let result = guard.execute_entry_point_by_name(&name)?; + result.to_json_str() + }(); + + match output { + Ok(json) => RegorusResult::ok_string(json), + Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()), + } + }) +} + +/// Execute an entry point by index. +#[no_mangle] +pub extern "C" fn regorus_rvm_execute_entry_point_by_index( + vm: *mut RegorusRvm, + index: usize, +) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result { + let vm = to_ref(vm)?; + let mut guard = vm.try_write()?; + let result = guard.execute_entry_point_by_index(index)?; + result.to_json_str() + }(); + + match output { + Ok(json) => RegorusResult::ok_string(json), + Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()), + } + }) +} + +/// Resume execution for suspendable runs. +#[no_mangle] +pub extern "C" fn regorus_rvm_resume( + vm: *mut RegorusRvm, + resume_value_json: *const c_char, + has_value: bool, +) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result { + let vm = to_ref(vm)?; + let mut guard = vm.try_write()?; + let value = if has_value { + Some(Value::from_json_str(&from_c_str(resume_value_json)?)?) + } else { + None + }; + let result = guard.resume(value)?; + result.to_json_str() + }(); + + match output { + Ok(json) => RegorusResult::ok_string(json), + Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()), + } + }) +} + +/// Get the current execution state of the VM. +#[no_mangle] +pub extern "C" fn regorus_rvm_get_execution_state(vm: *mut RegorusRvm) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result { + let vm = to_ref(vm)?; + let guard = vm.try_read()?; + let state: ExecutionState = guard.execution_state().clone(); + Ok(format!("{:?}", state)) + }(); + + match output { + Ok(json) => RegorusResult::ok_string(json), + Err(err) => RegorusResult::err_with_message(RegorusStatus::Error, err.to_string()), + } + }) +} + +fn convert_c_entry_points( + entry_points: *const *const c_char, + entry_points_len: usize, +) -> Result> { + if entry_points.is_null() && entry_points_len > 0 { + return Err(anyhow!("null entry_points pointer")); + } + + let mut entry_points_vec = Vec::with_capacity(entry_points_len); + for i in 0..entry_points_len { + unsafe { + let entry_ptr = entry_points.add(i); + if entry_ptr.is_null() { + return Err(anyhow!("null entry point at index {i}")); + } + let entry = from_c_str(*entry_ptr)?; + entry_points_vec.push(entry); + } + } + + Ok(entry_points_vec) +} + +fn convert_c_modules_to_rust( + modules: *const RegorusPolicyModule, + modules_len: usize, +) -> Result> { + if modules.is_null() && modules_len > 0 { + return Err(anyhow!("null modules pointer")); + } + + let mut policy_modules = Vec::with_capacity(modules_len); + + for i in 0..modules_len { + unsafe { + let module = modules.add(i); + if module.is_null() { + return Err(anyhow!("null module at index {i}")); + } + + let module_ref = &*module; + + let id = from_c_str(module_ref.id) + .map_err(|e| anyhow!("invalid module id at index {i}: {e}"))?; + let content = from_c_str(module_ref.content) + .map_err(|e| anyhow!("invalid module content at index {i}: {e}"))?; + + policy_modules.push(PolicyModule { + id: id.into(), + content: content.into(), + }); + } + } + + Ok(policy_modules) +} diff --git a/bindings/go/main.go b/bindings/go/main.go index 807a95b..5cca2db 100644 --- a/bindings/go/main.go +++ b/bindings/go/main.go @@ -100,4 +100,138 @@ func main() { os.Exit(1) } fmt.Printf("%s\n", output) + + // RVM regular example (compile, serialize, execute) + const regularPolicy = ` +package demo +import rego.v1 + +default allow := false + +allow if { + input.user == "alice" + input.active == true +} +` + const regularInput = `{"user":"alice","active":true}` + + regularModules := []regorus.PolicyModule{{Id: "demo.rego", Content: regularPolicy}} + regularEntryPoints := []string{"data.demo.allow"} + regularProgram, err := regorus.CompileProgramFromModules("{}", regularModules, regularEntryPoints) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + defer regularProgram.Close() + + listing, err := regularProgram.GenerateListing() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + fmt.Printf("RVM listing:\n%s\n", listing) + + binary, err := regularProgram.SerializeBinary() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + regularProgram.Close() + + rehydrated, isPartial, err := regorus.DeserializeProgram(binary) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + if isPartial { + fmt.Fprintf(os.Stderr, "error: program marked partial\n") + os.Exit(1) + } + defer rehydrated.Close() + + regularVm, err := regorus.NewRvm() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + defer regularVm.Close() + + if err := regularVm.LoadProgram(rehydrated); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + if err := regularVm.SetInputJson(regularInput); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + regularResult, err := regularVm.Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + fmt.Printf("RVM regular result: %s\n", regularResult) + + // RVM HostAwait example + const rvmPolicy = ` +package demo +import rego.v1 + +default allow := false + +allow if { + input.account.active == true + details := __builtin_host_await(input.account.id, "account") + details.tier == "gold" +} +` + const rvmInput = `{"account":{"id":"acct-1","active":true}}` + + modules := []regorus.PolicyModule{{Id: "demo.rego", Content: rvmPolicy}} + entryPoints := []string{"data.demo.allow"} + program, err := regorus.CompileProgramFromModules("{}", modules, entryPoints) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + defer program.Close() + + vm, err := regorus.NewRvm() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + defer vm.Close() + + if err := vm.SetExecutionMode(1); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + if err := vm.LoadProgram(program); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + if err := vm.SetInputJson(rvmInput); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + if _, err := vm.Execute(); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + state, err := vm.GetExecutionState() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + fmt.Printf("HostAwait state: %s\n", state) + + result, err := vm.Resume(`{"tier":"gold"}`, true) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + fmt.Printf("HostAwait result: %s\n", result) } diff --git a/bindings/go/pkg/regorus/mod.go b/bindings/go/pkg/regorus/mod.go index cf94577..3e2a498 100644 --- a/bindings/go/pkg/regorus/mod.go +++ b/bindings/go/pkg/regorus/mod.go @@ -1,6 +1,6 @@ package regorus -// #cgo LDFLAGS: -L ../../../ffi/target/release -lregorus_ffi +// #cgo LDFLAGS: -L ../../../ffi/target/release -L ../../../ffi/target/debug -lregorus_ffi // #include "../../../ffi/regorus.h" import "C" import ( diff --git a/bindings/go/pkg/regorus/rvm.go b/bindings/go/pkg/regorus/rvm.go new file mode 100644 index 0000000..d986b7e --- /dev/null +++ b/bindings/go/pkg/regorus/rvm.go @@ -0,0 +1,283 @@ +package regorus + +// #cgo LDFLAGS: -L ../../../ffi/target/release -L ../../../ffi/target/debug -lregorus_ffi +// #include "../../../ffi/regorus.h" +import "C" +import ( + "fmt" + "unsafe" +) + +type PolicyModule struct { + Id string + Content string +} + +type Program struct { + p *C.RegorusProgram +} + +type Rvm struct { + vm *C.RegorusRvm +} + +type Buffer struct { + b *C.RegorusBuffer +} + +func (b *Buffer) Close() { + if b != nil && b.b != nil { + C.regorus_buffer_drop(b.b) + b.b = nil + } +} + +func (b *Buffer) Bytes() []byte { + if b == nil || b.b == nil || b.b.data == nil || b.b.len == 0 { + return nil + } + return C.GoBytes(unsafe.Pointer(b.b.data), C.int(b.b.len)) +} + +func (p *Program) Close() { + if p != nil && p.p != nil { + C.regorus_program_drop(p.p) + p.p = nil + } +} + +func (p *Program) SerializeBinary() ([]byte, error) { + result := C.regorus_program_serialize_binary(p.p) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return nil, fmt.Errorf("%s", C.GoString(result.error_message)) + } + buffer := &Buffer{b: (*C.RegorusBuffer)(result.pointer_value)} + defer buffer.Close() + return buffer.Bytes(), nil +} + +func (p *Program) GenerateListing() (string, error) { + result := C.regorus_program_generate_listing(p.p) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return "", fmt.Errorf("%s", C.GoString(result.error_message)) + } + return C.GoString(result.output), nil +} + +func (p *Program) GenerateTabularListing() (string, error) { + result := C.regorus_program_generate_tabular_listing(p.p) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return "", fmt.Errorf("%s", C.GoString(result.error_message)) + } + return C.GoString(result.output), nil +} + +func DeserializeProgram(data []byte) (*Program, bool, error) { + if len(data) == 0 { + return nil, false, fmt.Errorf("empty program data") + } + var isPartial C.bool + result := C.regorus_program_deserialize_binary((*C.uchar)(unsafe.Pointer(&data[0])), C.ulong(len(data)), (*C.bool)(unsafe.Pointer(&isPartial))) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return nil, false, fmt.Errorf("%s", C.GoString(result.error_message)) + } + return &Program{p: (*C.RegorusProgram)(result.pointer_value)}, bool(isPartial), nil +} + +func CompileProgramFromModules(data string, modules []PolicyModule, entryPoints []string) (*Program, error) { + dataC := C.CString(data) + defer C.free(unsafe.Pointer(dataC)) + + cModules := make([]C.RegorusPolicyModule, len(modules)) + moduleIdPtrs := make([]*C.char, len(modules)) + moduleContentPtrs := make([]*C.char, len(modules)) + for i, module := range modules { + idC := C.CString(module.Id) + contentC := C.CString(module.Content) + moduleIdPtrs[i] = idC + moduleContentPtrs[i] = contentC + cModules[i].id = idC + cModules[i].content = contentC + } + defer func() { + for i := range moduleIdPtrs { + if moduleIdPtrs[i] != nil { + C.free(unsafe.Pointer(moduleIdPtrs[i])) + } + if moduleContentPtrs[i] != nil { + C.free(unsafe.Pointer(moduleContentPtrs[i])) + } + } + }() + + entryPtrs := make([]*C.char, len(entryPoints)) + for i, entry := range entryPoints { + entryPtrs[i] = C.CString(entry) + } + defer func() { + for _, ptr := range entryPtrs { + C.free(unsafe.Pointer(ptr)) + } + }() + + var modulesPtr *C.RegorusPolicyModule + if len(cModules) > 0 { + modulesPtr = (*C.RegorusPolicyModule)(unsafe.Pointer(&cModules[0])) + } + var entryPtr **C.char + if len(entryPtrs) > 0 { + entryPtr = (**C.char)(unsafe.Pointer(&entryPtrs[0])) + } + + result := C.regorus_program_compile_from_modules( + dataC, + modulesPtr, + C.ulong(len(cModules)), + entryPtr, + C.ulong(len(entryPtrs)), + ) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return nil, fmt.Errorf("%s", C.GoString(result.error_message)) + } + return &Program{p: (*C.RegorusProgram)(result.pointer_value)}, nil +} + +func CompileProgramFromEngine(engine *Engine, entryPoints []string) (*Program, error) { + entryPtrs := make([]*C.char, len(entryPoints)) + for i, entry := range entryPoints { + entryPtrs[i] = C.CString(entry) + } + defer func() { + for _, ptr := range entryPtrs { + C.free(unsafe.Pointer(ptr)) + } + }() + + var entryPtr **C.char + if len(entryPtrs) > 0 { + entryPtr = (**C.char)(unsafe.Pointer(&entryPtrs[0])) + } + + result := C.regorus_engine_compile_program_with_entrypoints( + engine.e, + entryPtr, + C.ulong(len(entryPtrs)), + ) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return nil, fmt.Errorf("%s", C.GoString(result.error_message)) + } + return &Program{p: (*C.RegorusProgram)(result.pointer_value)}, nil +} + +func NewRvm() (*Rvm, error) { + vm := C.regorus_rvm_new() + if vm == nil { + return nil, fmt.Errorf("failed to create RVM") + } + return &Rvm{vm: vm}, nil +} + +func (r *Rvm) Close() { + if r != nil && r.vm != nil { + C.regorus_rvm_drop(r.vm) + r.vm = nil + } +} + +func (r *Rvm) LoadProgram(program *Program) error { + result := C.regorus_rvm_load_program(r.vm, program.p) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return fmt.Errorf("%s", C.GoString(result.error_message)) + } + return nil +} + +func (r *Rvm) SetDataJson(data string) error { + dataC := C.CString(data) + defer C.free(unsafe.Pointer(dataC)) + result := C.regorus_rvm_set_data(r.vm, dataC) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return fmt.Errorf("%s", C.GoString(result.error_message)) + } + return nil +} + +func (r *Rvm) SetInputJson(input string) error { + inputC := C.CString(input) + defer C.free(unsafe.Pointer(inputC)) + result := C.regorus_rvm_set_input(r.vm, inputC) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return fmt.Errorf("%s", C.GoString(result.error_message)) + } + return nil +} + +func (r *Rvm) SetExecutionMode(mode byte) error { + result := C.regorus_rvm_set_execution_mode(r.vm, C.uchar(mode)) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return fmt.Errorf("%s", C.GoString(result.error_message)) + } + return nil +} + +func (r *Rvm) Execute() (string, error) { + result := C.regorus_rvm_execute(r.vm) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return "", fmt.Errorf("%s", C.GoString(result.error_message)) + } + return C.GoString(result.output), nil +} + +func (r *Rvm) ExecuteEntryPoint(name string) (string, error) { + nameC := C.CString(name) + defer C.free(unsafe.Pointer(nameC)) + result := C.regorus_rvm_execute_entry_point_by_name(r.vm, nameC) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return "", fmt.Errorf("%s", C.GoString(result.error_message)) + } + return C.GoString(result.output), nil +} + +func (r *Rvm) ExecuteEntryPointIndex(index uint64) (string, error) { + result := C.regorus_rvm_execute_entry_point_by_index(r.vm, C.ulong(index)) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return "", fmt.Errorf("%s", C.GoString(result.error_message)) + } + return C.GoString(result.output), nil +} + +func (r *Rvm) Resume(resumeValue string, hasValue bool) (string, error) { + var valueC *C.char + if hasValue { + valueC = C.CString(resumeValue) + defer C.free(unsafe.Pointer(valueC)) + } + result := C.regorus_rvm_resume(r.vm, valueC, C.bool(hasValue)) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return "", fmt.Errorf("%s", C.GoString(result.error_message)) + } + return C.GoString(result.output), nil +} + +func (r *Rvm) GetExecutionState() (string, error) { + result := C.regorus_rvm_get_execution_state(r.vm) + defer C.regorus_result_drop(result) + if result.status != C.Ok { + return "", fmt.Errorf("%s", C.GoString(result.error_message)) + } + return C.GoString(result.output), nil +} diff --git a/bindings/go/pkg/regorus/rvm_test.go b/bindings/go/pkg/regorus/rvm_test.go new file mode 100644 index 0000000..5f40444 --- /dev/null +++ b/bindings/go/pkg/regorus/rvm_test.go @@ -0,0 +1,128 @@ +package regorus + +import "testing" + +const rvmPolicy = ` +package demo +import rego.v1 + +default allow := false + +allow if { + input.account.active == true + details := __builtin_host_await(input.account.id, "account") + details.tier == "gold" +} +` + +const rvmInput = `{"account":{"id":"acct-1","active":true}}` + +const rvmRegularPolicy = ` +package demo +import rego.v1 + +default allow := false + +allow if { + input.user == "alice" + input.active == true +} +` + +const rvmRegularInput = `{"user":"alice","active":true}` + +func TestRvmProgramCompileAndExecute(t *testing.T) { + modules := []PolicyModule{{Id: "demo.rego", Content: rvmRegularPolicy}} + entryPoints := []string{"data.demo.allow"} + program, err := CompileProgramFromModules("{}", modules, entryPoints) + if err != nil { + t.Fatalf("compile program: %v", err) + } + defer program.Close() + + listing, err := program.GenerateListing() + if err != nil || listing == "" { + t.Fatalf("listing failed: %v", err) + } + + binary, err := program.SerializeBinary() + if err != nil { + t.Fatalf("serialize program: %v", err) + } + + rehydrated, isPartial, err := DeserializeProgram(binary) + if err != nil { + t.Fatalf("deserialize program: %v", err) + } + if isPartial { + t.Fatalf("deserialized program marked partial") + } + defer rehydrated.Close() + + vm, err := NewRvm() + if err != nil { + t.Fatalf("new vm: %v", err) + } + defer vm.Close() + + if err := vm.LoadProgram(rehydrated); err != nil { + t.Fatalf("load program: %v", err) + } + if err := vm.SetInputJson(rvmRegularInput); err != nil { + t.Fatalf("set input: %v", err) + } + + result, err := vm.Execute() + if err != nil { + t.Fatalf("execute: %v", err) + } + if result != "true" { + t.Fatalf("expected allow=true, got %s", result) + } +} + +func TestRvmHostAwaitSuspendResume(t *testing.T) { + modules := []PolicyModule{{Id: "host_await.rego", Content: rvmPolicy}} + entryPoints := []string{"data.demo.allow"} + program, err := CompileProgramFromModules("{}", modules, entryPoints) + if err != nil { + t.Fatalf("compile program: %v", err) + } + defer program.Close() + + vm, err := NewRvm() + if err != nil { + t.Fatalf("new vm: %v", err) + } + defer vm.Close() + + if err := vm.SetExecutionMode(1); err != nil { + t.Fatalf("set execution mode: %v", err) + } + if err := vm.LoadProgram(program); err != nil { + t.Fatalf("load program: %v", err) + } + if err := vm.SetInputJson(rvmInput); err != nil { + t.Fatalf("set input: %v", err) + } + + if _, err := vm.Execute(); err != nil { + t.Fatalf("execute in suspendable mode failed: %v", err) + } + + state, err := vm.GetExecutionState() + if err != nil { + t.Fatalf("get execution state: %v", err) + } + if state == "" { + t.Fatalf("expected non-empty execution state") + } + + result, err := vm.Resume(`{"tier":"gold"}`, true) + if err != nil { + t.Fatalf("resume: %v", err) + } + if result != "true" { + t.Fatalf("expected allow=true, got %s", result) + } +} diff --git a/bindings/go/regorus_test b/bindings/go/regorus_test new file mode 100755 index 0000000..9efa7dd Binary files /dev/null and b/bindings/go/regorus_test differ diff --git a/bindings/java/Cargo.lock b/bindings/java/Cargo.lock index e512030..2dc8af7 100644 --- a/bindings/java/Cargo.lock +++ b/bindings/java/Cargo.lock @@ -52,6 +52,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "serde", + "unty", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -109,9 +119,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.54" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "shlex", @@ -213,9 +223,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fluent-uri" @@ -279,9 +289,9 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -411,6 +421,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", + "serde", + "serde_core", ] [[package]] @@ -817,10 +829,12 @@ name = "regorus" version = "0.9.0" dependencies = [ "anyhow", + "bincode", "chrono", "chrono-tz", "data-encoding", "globset", + "indexmap", "ipnet", "jsonschema", "lazy_static", @@ -1067,6 +1081,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.8" @@ -1364,18 +1384,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.34" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" +checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.34" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" +checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" dependencies = [ "proc-macro2", "quote", diff --git a/bindings/java/Cargo.toml b/bindings/java/Cargo.toml index 97e7b04..e443ee3 100644 --- a/bindings/java/Cargo.toml +++ b/bindings/java/Cargo.toml @@ -21,4 +21,4 @@ ast = ["regorus/ast"] anyhow = "1.0" serde_json = "1.0.112" jni = "0.21.1" -regorus = { path = "../..", default-features = false, features = ["arc"] } +regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] } diff --git a/bindings/java/Test.java b/bindings/java/Test.java index ae06f6f..af482e7 100644 --- a/bindings/java/Test.java +++ b/bindings/java/Test.java @@ -2,6 +2,9 @@ // Licensed under the MIT License. import com.microsoft.regorus.Engine; +import com.microsoft.regorus.PolicyModule; +import com.microsoft.regorus.Program; +import com.microsoft.regorus.Rvm; public class Test { @@ -44,5 +47,70 @@ public class Test { "package world\nx { true }" ); } + + String regularPolicy = String.join("\n", + "package demo", + "import rego.v1", + "", + "default allow := false", + "", + "allow if {", + " input.user == \"alice\"", + " input.active == true", + "}" + ); + String regularInput = "{\"user\":\"alice\",\"active\":true}"; + + { + PolicyModule module = new PolicyModule("demo.rego", regularPolicy); + Program program = Program.compileFromModules("{}", new PolicyModule[]{module}, new String[]{"data.demo.allow"}); + System.out.println("RVM listing:\n" + program.generateListing()); + + byte[] binary = program.serializeBinary(); + program.close(); + + boolean[] isPartial = new boolean[1]; + Program rehydrated = Program.deserializeBinary(binary, isPartial); + if (isPartial[0]) { + throw new IllegalStateException("Deserialized program marked partial"); + } + + try (Rvm vm = new Rvm()) { + vm.loadProgram(rehydrated); + vm.setInputJson(regularInput); + String result = vm.execute(); + System.out.println("RVM regular result: " + result); + } + rehydrated.close(); + } + + String awaitPolicy = String.join("\n", + "package demo", + "import rego.v1", + "", + "default allow := false", + "", + "allow if {", + " input.account.active == true", + " details := __builtin_host_await(input.account.id, \"account\")", + " details.tier == \"gold\"", + "}" + ); + String awaitInput = "{\"account\":{\"id\":\"acct-1\",\"active\":true}}"; + + { + PolicyModule module = new PolicyModule("await.rego", awaitPolicy); + Program program = Program.compileFromModules("{}", new PolicyModule[]{module}, new String[]{"data.demo.allow"}); + try (Rvm vm = new Rvm()) { + vm.setExecutionMode((byte) 1); + vm.loadProgram(program); + vm.setInputJson(awaitInput); + vm.execute(); + System.out.println("HostAwait state: " + vm.getExecutionState()); + String resumed = vm.resume("{\"tier\":\"gold\"}"); + System.out.println("HostAwait result: " + resumed); + } + program.close(); + } } } diff --git a/bindings/java/src/lib.rs b/bindings/java/src/lib.rs index f774eaf..20e0672 100644 --- a/bindings/java/src/lib.rs +++ b/bindings/java/src/lib.rs @@ -2,11 +2,17 @@ // Licensed under the MIT License. use anyhow::Result; -use jni::objects::{JClass, JObject, JString}; -use jni::sys::{jlong, jstring}; +use jni::objects::{JBooleanArray, JByteArray, JClass, JObject, JObjectArray, JString}; +use jni::sys::{jboolean, jbooleanArray, jbyteArray, jlong, jobjectArray, jstring}; use jni::JNIEnv; -use regorus::{Engine, Value}; +use regorus::languages::rego::compiler::Compiler; +use regorus::rvm::program::{ + generate_assembly_listing, AssemblyListingConfig, DeserializationResult, Program as RvmProgram, +}; +use regorus::rvm::vm::{ExecutionMode, RegoVM}; +use regorus::{compile_policy_with_entrypoint, Engine, PolicyModule, Rc, Value}; +use std::sync::Arc; #[no_mangle] pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeNewEngine( @@ -370,6 +376,336 @@ pub extern "system" fn Java_com_microsoft_regorus_Engine_nativeDestroyEngine( } } +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromModules( + env: JNIEnv, + _class: JClass, + data_json: JString, + module_ids: jobjectArray, + module_contents: jobjectArray, + entry_points: jobjectArray, +) -> jlong { + let res = throw_err(env, |env| { + let data_json: String = env.get_string(&data_json)?.into(); + let data = Value::from_json_str(&data_json)?; + + let ids = get_string_array(env, module_ids)?; + let contents = get_string_array(env, module_contents)?; + if ids.len() != contents.len() { + return Err(anyhow::anyhow!("module id/content length mismatch")); + } + + let mut modules = Vec::with_capacity(ids.len()); + for (id, content) in ids.into_iter().zip(contents.into_iter()) { + modules.push(PolicyModule { + id: Rc::from(id.as_str()), + content: Rc::from(content.as_str()), + }); + } + + let entry_points_vec = get_string_array(env, entry_points)?; + if entry_points_vec.is_empty() { + return Err(anyhow::anyhow!( + "entry_points must contain at least one entry" + )); + } + let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect(); + let entry_rule = entry_points_ref[0]; + + let compiled = compile_policy_with_entrypoint(data, &modules, Rc::from(entry_rule))?; + let program = Compiler::compile_from_policy(&compiled, &entry_points_ref)?; + Ok(Box::into_raw(Box::new(program)) as jlong) + }); + + res.unwrap_or_default() +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromEngine( + env: JNIEnv, + _class: JClass, + engine_ptr: jlong, + entry_points: jobjectArray, +) -> jlong { + let res = throw_err(env, |env| { + let engine = unsafe { &mut *(engine_ptr as *mut Engine) }; + let entry_points_vec = get_string_array(env, entry_points)?; + if entry_points_vec.is_empty() { + return Err(anyhow::anyhow!( + "entry_points must contain at least one entry" + )); + } + let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect(); + let entry_rule = Rc::from(entry_points_ref[0]); + let compiled = engine.compile_with_entrypoint(&entry_rule)?; + let program = Compiler::compile_from_policy(&compiled, &entry_points_ref)?; + Ok(Box::into_raw(Box::new(program)) as jlong) + }); + + res.unwrap_or_default() +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Program_nativeGenerateListing( + env: JNIEnv, + _class: JClass, + program_ptr: jlong, +) -> jstring { + let res = throw_err(env, |env| { + let program = unsafe { &*(program_ptr as *mut Arc) }; + let listing = + generate_assembly_listing(program.as_ref(), &AssemblyListingConfig::default()); + let output = env.new_string(&listing)?; + Ok(output.into_raw()) + }); + + match res { + Ok(val) => val, + Err(_) => JObject::null().into_raw(), + } +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Program_nativeSerializeBinary( + env: JNIEnv, + _class: JClass, + program_ptr: jlong, +) -> jbyteArray { + let res = throw_err(env, |env| { + let program = unsafe { &*(program_ptr as *mut Arc) }; + let bytes = program.serialize_binary().map_err(|e| anyhow::anyhow!(e))?; + let array = env.byte_array_from_slice(&bytes)?; + Ok(array.into_raw()) + }); + + match res { + Ok(val) => val, + Err(_) => JObject::null().into_raw(), + } +} + +#[no_mangle] +/// # Safety +/// +/// The `data` and `is_partial` pointers must be valid JNI array references +/// for the duration of the call. They must come from the JVM for the current +/// thread and not be used after this function returns. +pub unsafe extern "system" fn Java_com_microsoft_regorus_Program_nativeDeserializeBinary( + env: JNIEnv, + _class: JClass, + data: jbyteArray, + is_partial: jbooleanArray, +) -> jlong { + let res = throw_err(env, |env| { + if data.is_null() { + return Err(anyhow::anyhow!("data must not be null")); + } + + let data = unsafe { JByteArray::from_raw(data) }; + let bytes = env.convert_byte_array(&data)?; + let (program, partial) = + match RvmProgram::deserialize_binary(&bytes).map_err(|e| anyhow::anyhow!(e))? { + DeserializationResult::Complete(program) => (program, false), + DeserializationResult::Partial(program) => (program, true), + }; + + if !is_partial.is_null() { + let is_partial = unsafe { JBooleanArray::from_raw(is_partial) }; + let len = env.get_array_length(&is_partial)?; + if len > 0 { + let value: [jboolean; 1] = [if partial { 1 } else { 0 }]; + env.set_boolean_array_region(&is_partial, 0, &value)?; + } + } + + Ok(Box::into_raw(Box::new(Arc::new(program))) as jlong) + }); + + res.unwrap_or_default() +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Program_nativeDrop( + _env: JNIEnv, + _class: JClass, + program_ptr: jlong, +) { + unsafe { + let _program = Box::from_raw(program_ptr as *mut Arc); + } +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeNew( + _env: JNIEnv, + _class: JClass, +) -> jlong { + let vm = RegoVM::new(); + Box::into_raw(Box::new(vm)) as jlong +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeLoadProgram( + env: JNIEnv, + _class: JClass, + vm_ptr: jlong, + program_ptr: jlong, +) { + let _ = throw_err(env, |_env| { + let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) }; + let program = unsafe { &*(program_ptr as *mut Arc) }; + vm.load_program(program.clone()); + Ok(()) + }); +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetDataJson( + env: JNIEnv, + _class: JClass, + vm_ptr: jlong, + data_json: JString, +) { + let _ = throw_err(env, |env| { + let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) }; + let data_json: String = env.get_string(&data_json)?.into(); + let data = Value::from_json_str(&data_json)?; + vm.set_data(data)?; + Ok(()) + }); +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetInputJson( + env: JNIEnv, + _class: JClass, + vm_ptr: jlong, + input_json: JString, +) { + let _ = throw_err(env, |env| { + let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) }; + let input_json: String = env.get_string(&input_json)?.into(); + let input = Value::from_json_str(&input_json)?; + vm.set_input(input); + Ok(()) + }); +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeSetExecutionMode( + env: JNIEnv, + _class: JClass, + vm_ptr: jlong, + mode: u8, +) { + let _ = throw_err(env, |_env| { + let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) }; + let mode = match mode { + 0 => ExecutionMode::RunToCompletion, + 1 => ExecutionMode::Suspendable, + _ => return Err(anyhow::anyhow!("invalid execution mode")), + }; + vm.set_execution_mode(mode); + Ok(()) + }); +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeExecute( + env: JNIEnv, + _class: JClass, + vm_ptr: jlong, +) -> jstring { + let res = throw_err(env, |env| { + let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) }; + let result = vm.execute()?; + let output = env.new_string(result.to_json_str()?)?; + Ok(output.into_raw()) + }); + + match res { + Ok(val) => val, + Err(_) => JObject::null().into_raw(), + } +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeExecuteEntryPoint( + env: JNIEnv, + _class: JClass, + vm_ptr: jlong, + entry_point: JString, +) -> jstring { + let res = throw_err(env, |env| { + let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) }; + let entry_point: String = env.get_string(&entry_point)?.into(); + let result = vm.execute_entry_point_by_name(&entry_point)?; + let output = env.new_string(result.to_json_str()?)?; + Ok(output.into_raw()) + }); + + match res { + Ok(val) => val, + Err(_) => JObject::null().into_raw(), + } +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeResume( + env: JNIEnv, + _class: JClass, + vm_ptr: jlong, + resume_json: JString, + has_value: bool, +) -> jstring { + let res = throw_err(env, |env| { + let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) }; + let value = if has_value { + let resume_json: String = env.get_string(&resume_json)?.into(); + Some(Value::from_json_str(&resume_json)?) + } else { + None + }; + let result = vm.resume(value)?; + let output = env.new_string(result.to_json_str()?)?; + Ok(output.into_raw()) + }); + + match res { + Ok(val) => val, + Err(_) => JObject::null().into_raw(), + } +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeGetExecutionState( + env: JNIEnv, + _class: JClass, + vm_ptr: jlong, +) -> jstring { + let res = throw_err(env, |env| { + let vm = unsafe { &mut *(vm_ptr as *mut RegoVM) }; + let output = env.new_string(format!("{:?}", vm.execution_state()))?; + Ok(output.into_raw()) + }); + + match res { + Ok(val) => val, + Err(_) => JObject::null().into_raw(), + } +} + +#[no_mangle] +pub extern "system" fn Java_com_microsoft_regorus_Rvm_nativeDrop( + _env: JNIEnv, + _class: JClass, + vm_ptr: jlong, +) { + unsafe { + let _vm = Box::from_raw(vm_ptr as *mut RegoVM); + } +} + fn throw_err(mut env: JNIEnv, mut f: impl FnMut(&mut JNIEnv) -> Result) -> Result { match f(&mut env) { Ok(val) => Ok(val), @@ -379,3 +715,19 @@ fn throw_err(mut env: JNIEnv, mut f: impl FnMut(&mut JNIEnv) -> Result) -> } } } + +fn get_string_array(env: &mut JNIEnv, array: jobjectArray) -> Result> { + if array.is_null() { + return Ok(Vec::new()); + } + let array = unsafe { JObjectArray::from_raw(array) }; + let len = env.get_array_length(&array)?; + let mut values = Vec::with_capacity(len as usize); + for i in 0..len { + let obj = env.get_object_array_element(&array, i)?; + let jstr = JString::from(obj); + let value: String = env.get_string(&jstr)?.into(); + values.push(value); + } + Ok(values) +} diff --git a/bindings/java/src/main/java/com/microsoft/regorus/Engine.java b/bindings/java/src/main/java/com/microsoft/regorus/Engine.java index 94d0ba7..9ee6f47 100644 --- a/bindings/java/src/main/java/com/microsoft/regorus/Engine.java +++ b/bindings/java/src/main/java/com/microsoft/regorus/Engine.java @@ -221,6 +221,8 @@ public class Engine implements AutoCloseable, Cloneable { /** * Get coverage report as json string. + * + * @return Coverage report as a JSON string. * */ public String getCoverageReport() { @@ -229,6 +231,8 @@ public class Engine implements AutoCloseable, Cloneable { /** * Get coverage report as ANSI color coded string. + * + * @return Coverage report formatted for console output. * */ public String getCoverageReportPretty() { @@ -247,12 +251,18 @@ public class Engine implements AutoCloseable, Cloneable { /** * Take gathered prints. + * + * @return Collected print output as JSON. * */ public String takePrints() { return nativeTakePrints(enginePtr); } + long getPtr() { + return enginePtr; + } + @Override public void close() { diff --git a/bindings/java/src/main/java/com/microsoft/regorus/PolicyModule.java b/bindings/java/src/main/java/com/microsoft/regorus/PolicyModule.java new file mode 100644 index 0000000..471dd47 --- /dev/null +++ b/bindings/java/src/main/java/com/microsoft/regorus/PolicyModule.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + **/ + +package com.microsoft.regorus; + +/** + * Represents a Rego module used for RVM program compilation. + */ +public final class PolicyModule { + /** + * Module identifier or filename. + */ + public final String id; + + /** + * Rego policy content. + */ + public final String content; + + /** + * Create a new policy module. + * + * @param id Module identifier or filename. + * @param content Rego policy content. + */ + public PolicyModule(String id, String content) { + this.id = id; + this.content = content; + } +} diff --git a/bindings/java/src/main/java/com/microsoft/regorus/Program.java b/bindings/java/src/main/java/com/microsoft/regorus/Program.java new file mode 100644 index 0000000..628120d --- /dev/null +++ b/bindings/java/src/main/java/com/microsoft/regorus/Program.java @@ -0,0 +1,102 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + **/ + +package com.microsoft.regorus; + +/** + * Represents a compiled RVM program. + */ +public final class Program implements AutoCloseable { + private static native long nativeCompileFromModules( + String dataJson, + String[] moduleIds, + String[] moduleContents, + String[] entryPoints); + + private static native long nativeCompileFromEngine(long enginePtr, String[] entryPoints); + private static native String nativeGenerateListing(long programPtr); + private static native byte[] nativeSerializeBinary(long programPtr); + private static native long nativeDeserializeBinary(byte[] data, boolean[] isPartial); + private static native void nativeDrop(long programPtr); + + private final long programPtr; + + Program(long ptr) { + this.programPtr = ptr; + } + + /** + * Compile a program from modules and entry points. + * + * @param dataJson JSON document to merge as static data. + * @param modules Policy modules to compile. + * @param entryPoints Entry point rule paths. + * @return Compiled program instance. + */ + public static Program compileFromModules(String dataJson, PolicyModule[] modules, String[] entryPoints) { + String[] ids = new String[modules.length]; + String[] contents = new String[modules.length]; + for (int i = 0; i < modules.length; i++) { + ids[i] = modules[i].id; + contents[i] = modules[i].content; + } + long ptr = nativeCompileFromModules(dataJson, ids, contents, entryPoints); + return new Program(ptr); + } + + /** + * Compile a program from an engine and entry points. + * + * @param engine Engine with loaded policies. + * @param entryPoints Entry point rule paths. + * @return Compiled program instance. + */ + public static Program compileFromEngine(Engine engine, String[] entryPoints) { + long ptr = nativeCompileFromEngine(engine.getPtr(), entryPoints); + return new Program(ptr); + } + + /** + * Generate a readable assembly listing. + * + * @return Listing text. + */ + public String generateListing() { + return nativeGenerateListing(programPtr); + } + + /** + * Serialize the program to binary format. + * + * @return Serialized bytes. + */ + public byte[] serializeBinary() { + return nativeSerializeBinary(programPtr); + } + + /** + * Deserialize a program from binary format. + * + * @param data Serialized program bytes. + * @param isPartial Optional array to receive the partial flag (index 0). + * @return Deserialized program instance. + */ + public static Program deserializeBinary(byte[] data, boolean[] isPartial) { + if (data == null || data.length == 0) { + throw new IllegalArgumentException("data must not be empty"); + } + long ptr = nativeDeserializeBinary(data, isPartial); + return new Program(ptr); + } + + long getPtr() { + return programPtr; + } + + @Override + public void close() { + nativeDrop(programPtr); + } +} diff --git a/bindings/java/src/main/java/com/microsoft/regorus/Rvm.java b/bindings/java/src/main/java/com/microsoft/regorus/Rvm.java new file mode 100644 index 0000000..cb5ebb6 --- /dev/null +++ b/bindings/java/src/main/java/com/microsoft/regorus/Rvm.java @@ -0,0 +1,110 @@ +/** + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + **/ + +package com.microsoft.regorus; + +/** + * Wrapper for the Regorus RVM runtime. + */ +public final class Rvm implements AutoCloseable { + private static native long nativeNew(); + private static native void nativeDrop(long vmPtr); + private static native void nativeLoadProgram(long vmPtr, long programPtr); + private static native void nativeSetDataJson(long vmPtr, String dataJson); + private static native void nativeSetInputJson(long vmPtr, String inputJson); + private static native void nativeSetExecutionMode(long vmPtr, byte mode); + private static native String nativeExecute(long vmPtr); + private static native String nativeExecuteEntryPoint(long vmPtr, String entryPoint); + private static native String nativeResume(long vmPtr, String resumeJson, boolean hasValue); + private static native String nativeGetExecutionState(long vmPtr); + + private final long vmPtr; + + /** + * Create a new RVM instance. + */ + public Rvm() { + this.vmPtr = nativeNew(); + } + + /** + * Load a program into the VM. + * + * @param program Compiled program. + */ + public void loadProgram(Program program) { + nativeLoadProgram(vmPtr, program.getPtr()); + } + + /** + * Set data JSON for the VM. + * + * @param dataJson JSON data document. + */ + public void setDataJson(String dataJson) { + nativeSetDataJson(vmPtr, dataJson); + } + + /** + * Set input JSON for the VM. + * + * @param inputJson JSON input document. + */ + public void setInputJson(String inputJson) { + nativeSetInputJson(vmPtr, inputJson); + } + + /** + * Set execution mode (0 = run-to-completion, 1 = suspendable). + * + * @param mode Execution mode. + */ + public void setExecutionMode(byte mode) { + nativeSetExecutionMode(vmPtr, mode); + } + + /** + * Execute the program. + * + * @return JSON result string. + */ + public String execute() { + return nativeExecute(vmPtr); + } + + /** + * Execute a named entry point. + * + * @param entryPoint Entry point rule path. + * @return JSON result string. + */ + public String executeEntryPoint(String entryPoint) { + return nativeExecuteEntryPoint(vmPtr, entryPoint); + } + + /** + * Resume execution with an optional JSON value. + * + * @param resumeJson JSON value to resume with, or null for no value. + * @return JSON result string. + */ + public String resume(String resumeJson) { + return nativeResume(vmPtr, resumeJson, resumeJson != null); + } + + /** + * Get the current execution state. + * + * @return Execution state string. + */ + public String getExecutionState() { + return nativeGetExecutionState(vmPtr); + } + + @Override + public void close() { + nativeDrop(vmPtr); + } +} diff --git a/bindings/python/Cargo.lock b/bindings/python/Cargo.lock index 8211dda..cbfa3a8 100644 --- a/bindings/python/Cargo.lock +++ b/bindings/python/Cargo.lock @@ -52,6 +52,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "serde", + "unty", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -103,9 +113,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "cc" -version = "1.2.54" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "shlex", @@ -191,9 +201,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fluent-uri" @@ -263,9 +273,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -395,6 +405,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", + "serde", + "serde_core", ] [[package]] @@ -876,10 +888,12 @@ name = "regorus" version = "0.9.0" dependencies = [ "anyhow", + "bincode", "chrono", "chrono-tz", "data-encoding", "globset", + "indexmap", "ipnet", "jsonschema", "lazy_static", @@ -1110,6 +1124,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.8" @@ -1313,18 +1333,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.34" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" +checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.34" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" +checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" dependencies = [ "proc-macro2", "quote", diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index ce75f07..d0e06e0 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -22,6 +22,6 @@ coverage = ["regorus/coverage"] anyhow = "1.0" ordered-float = "5.0.0" pyo3 = { version = "0.24.1", features = ["abi3-py310", "anyhow", "extension-module"] } -regorus = { path = "../..", default-features = false, features = ["arc"] } +regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] } serde_json = "1.0.140" diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index b8f552d..237998c 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -8,7 +8,14 @@ use pyo3::IntoPyObjectExt; use std::collections::{BTreeMap, BTreeSet}; -use ::regorus::Value; +use ::regorus::languages::rego::compiler::Compiler; +use ::regorus::rvm::program::{ + generate_assembly_listing, generate_tabular_assembly_listing, AssemblyListingConfig, + DeserializationResult, Program as RvmProgram, +}; +use ::regorus::rvm::vm::{ExecutionMode, RegoVM}; +use ::regorus::{compile_policy_with_entrypoint, PolicyModule, Rc, Value}; +use std::sync::Arc; /// Regorus engine. #[pyclass(unsendable)] @@ -16,6 +23,18 @@ pub struct Engine { engine: ::regorus::Engine, } +/// RVM program wrapper. +#[pyclass(unsendable)] +pub struct Program { + program: Arc, +} + +/// RVM runtime wrapper. +#[pyclass(unsendable)] +pub struct Rvm { + vm: RegoVM, +} + impl Default for Engine { fn default() -> Self { Self::new() @@ -384,7 +403,151 @@ impl Engine { } } +#[pymethods] +impl Program { + /// Compile an RVM program from modules and entry points. + #[staticmethod] + pub fn compile_from_modules( + data_json: String, + modules: Vec<(String, String)>, + entry_points: Vec, + ) -> Result { + if entry_points.is_empty() { + return Err(anyhow!("entry_points must contain at least one entry")); + } + + let data = Value::from_json_str(&data_json)?; + let policy_modules: Vec = modules + .into_iter() + .map(|(id, content)| PolicyModule { + id: Rc::from(id.as_str()), + content: Rc::from(content.as_str()), + }) + .collect(); + + let entry_points_ref: Vec<&str> = entry_points.iter().map(|s| s.as_str()).collect(); + let entry_rule = Rc::from(entry_points_ref[0]); + let compiled = compile_policy_with_entrypoint(data, &policy_modules, entry_rule)?; + let program = Compiler::compile_from_policy(&compiled, &entry_points_ref)?; + Ok(Self { program }) + } + + /// Deserialize an RVM program from binary data. + #[staticmethod] + pub fn deserialize_binary(data: Vec) -> Result<(Self, bool)> { + let (program, is_partial) = + match RvmProgram::deserialize_binary(&data).map_err(|e: String| anyhow!(e))? { + DeserializationResult::Complete(program) => (program, false), + DeserializationResult::Partial(program) => (program, true), + }; + Ok(( + Self { + program: Arc::new(program), + }, + is_partial, + )) + } + + /// Serialize a program to binary format. + pub fn serialize_binary(&self) -> Result> { + self.program + .serialize_binary() + .map_err(|e: String| anyhow!(e)) + } + + /// Generate a readable assembly listing. + pub fn generate_listing(&self) -> Result { + Ok(generate_assembly_listing( + self.program.as_ref(), + &AssemblyListingConfig::default(), + )) + } + + /// Generate a tabular assembly listing. + pub fn generate_tabular_listing(&self) -> Result { + Ok(generate_tabular_assembly_listing( + self.program.as_ref(), + &AssemblyListingConfig::default(), + )) + } +} + +impl Default for Rvm { + fn default() -> Self { + Self::new() + } +} + +#[pymethods] +impl Rvm { + #[new] + pub fn new() -> Self { + Self { vm: RegoVM::new() } + } + + /// Load an RVM program into the VM. + pub fn load_program(&mut self, program: &Program) -> Result<()> { + self.vm.load_program(program.program.clone()); + Ok(()) + } + + /// Set data JSON for the VM. + pub fn set_data_json(&mut self, data_json: String) -> Result<()> { + let data = Value::from_json_str(&data_json)?; + self.vm.set_data(data)?; + Ok(()) + } + + /// Set input JSON for the VM. + pub fn set_input_json(&mut self, input_json: String) -> Result<()> { + let input = Value::from_json_str(&input_json)?; + self.vm.set_input(input); + Ok(()) + } + + /// Set execution mode (0 = run-to-completion, 1 = suspendable). + pub fn set_execution_mode(&mut self, mode: u8) -> Result<()> { + let mode = match mode { + 0 => ExecutionMode::RunToCompletion, + 1 => ExecutionMode::Suspendable, + _ => return Err(anyhow!("invalid execution mode")), + }; + self.vm.set_execution_mode(mode); + Ok(()) + } + + /// Execute the program and return the JSON result. + pub fn execute(&mut self) -> Result { + self.vm.execute()?.to_json_str() + } + + /// Execute an entry point by name and return the JSON result. + pub fn execute_entry_point(&mut self, entry_point: String) -> Result { + self.vm + .execute_entry_point_by_name(&entry_point)? + .to_json_str() + } + + /// Resume execution with an optional JSON value. + pub fn resume(&mut self, resume_json: Option) -> Result { + let value = if let Some(json) = resume_json { + Some(Value::from_json_str(&json)?) + } else { + None + }; + self.vm.resume(value)?.to_json_str() + } + + /// Get the execution state as a string. + pub fn get_execution_state(&self) -> Result { + Ok(format!("{:?}", self.vm.execution_state())) + } +} + #[pymodule] pub fn regorus(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::() + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) } diff --git a/bindings/python/test.py b/bindings/python/test.py index f80119e..a1e2025 100644 --- a/bindings/python/test.py +++ b/bindings/python/test.py @@ -2,6 +2,10 @@ # Licensed under the MIT License. import regorus +import sys + +if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8") # Create engine engine = regorus.Engine() @@ -94,3 +98,68 @@ engine1.set_gather_prints(True) engine1.eval_query('print("Hello")') ps = engine1.take_prints() print(ps) + +# RVM regular example +policy = """ +package demo +import rego.v1 + +default allow := false + +allow if { + input.user == "alice" + input.active == true +} +""" +def run_regular_example(): + module = ("demo.rego", policy) + program = regorus.Program.compile_from_modules( + "{}", + [module], + ["data.demo.allow"], + ) + + print(program.generate_listing()) + + binary = program.serialize_binary() + program, is_partial = regorus.Program.deserialize_binary(binary) + if is_partial: + raise RuntimeError("Deserialized program marked partial") + + vm = regorus.Rvm() + vm.load_program(program) + vm.set_input_json('{"user":"alice","active":true}') + print(vm.execute()) + +run_regular_example() + +# RVM HostAwait example +policy = """ +package demo +import rego.v1 + +default allow := false + +allow if { + input.account.active == true + details := __builtin_host_await(input.account.id, "account") + details.tier == "gold" +} +""" +def run_host_await_example(): + module = ("await.rego", policy) + program = regorus.Program.compile_from_modules( + "{}", + [module], + ["data.demo.allow"], + ) + + vm = regorus.Rvm() + vm.set_execution_mode(1) + vm.load_program(program) + vm.set_input_json('{"account":{"id":"acct-1","active":true}}') + vm.execute() + print(vm.get_execution_state()) + print(vm.resume('{"tier":"gold"}')) + +run_host_await_example() diff --git a/bindings/wasm/Cargo.lock b/bindings/wasm/Cargo.lock index 8d2d7f7..81435bf 100644 --- a/bindings/wasm/Cargo.lock +++ b/bindings/wasm/Cargo.lock @@ -63,6 +63,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "serde", + "unty", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -120,9 +130,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.54" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" dependencies = [ "find-msvc-tools", "shlex", @@ -208,9 +218,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fluent-uri" @@ -314,9 +324,9 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -446,6 +456,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", + "serde", + "serde_core", ] [[package]] @@ -874,10 +886,12 @@ name = "regorus" version = "0.9.0" dependencies = [ "anyhow", + "bincode", "chrono", "chrono-tz", "data-encoding", "globset", + "indexmap", "ipnet", "jsonschema", "lazy_static", @@ -903,6 +917,7 @@ dependencies = [ "getrandom 0.2.17", "getrandom 0.3.4", "regorus", + "serde", "serde_json", "uuid", "wasm-bindgen", @@ -1098,6 +1113,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "url" version = "2.5.8" @@ -1398,18 +1419,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.34" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d" +checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.34" +version = "0.8.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d" +checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" dependencies = [ "proc-macro2", "quote", diff --git a/bindings/wasm/Cargo.toml b/bindings/wasm/Cargo.toml index f317c4d..ab66dc8 100644 --- a/bindings/wasm/Cargo.toml +++ b/bindings/wasm/Cargo.toml @@ -37,7 +37,8 @@ ast = ["regorus/ast"] coverage = ["regorus/coverage"] [dependencies] -regorus = { path = "../..", default-features = false, features = ["arc"] } +regorus = { path = "../..", default-features = false, features = ["arc", "rvm"] } +serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.140" wasm-bindgen = "0.2.100" # Specify uuid as a mandatory dependency so as to enable `js` feature which is now required diff --git a/bindings/wasm/src/lib.rs b/bindings/wasm/src/lib.rs index e0a0816..25f2b8d 100644 --- a/bindings/wasm/src/lib.rs +++ b/bindings/wasm/src/lib.rs @@ -3,6 +3,15 @@ #![allow(non_snake_case)] +use regorus::languages::rego::compiler::Compiler; +use regorus::rvm::program::{ + generate_assembly_listing, generate_tabular_assembly_listing, AssemblyListingConfig, + DeserializationResult, Program as RvmProgram, +}; +use regorus::rvm::vm::{ExecutionMode, RegoVM}; +use regorus::{compile_policy_with_entrypoint, PolicyModule, Rc, Value}; +use serde::Deserialize; +use std::sync::Arc; use wasm_bindgen::prelude::*; #[wasm_bindgen] @@ -11,6 +20,44 @@ pub struct Engine { engine: regorus::Engine, } +#[derive(Deserialize)] +struct ModuleSpec { + id: String, + content: String, +} + +#[wasm_bindgen] +pub struct Program { + program: Arc, +} + +#[wasm_bindgen] +pub struct ProgramDeserializationResult { + program: Arc, + is_partial: bool, +} + +#[wasm_bindgen] +impl ProgramDeserializationResult { + /// Whether the program was partially deserialized. + #[wasm_bindgen(getter)] + pub fn isPartial(&self) -> bool { + self.is_partial + } + + /// Get the deserialized program. + pub fn program(&self) -> Program { + Program { + program: self.program.clone(), + } + } +} + +#[wasm_bindgen] +pub struct Rvm { + vm: RegoVM, +} + fn error_to_jsvalue(e: E) -> JsValue { JsValue::from_str(&format!("{e}")) } @@ -193,6 +240,153 @@ impl Engine { } } +#[wasm_bindgen] +impl Program { + /// Compile an RVM program from modules and entry points. + pub fn compileFromModules( + data_json: String, + modules_json: String, + entry_points_json: String, + ) -> Result { + let data = Value::from_json_str(&data_json).map_err(error_to_jsvalue)?; + let modules: Vec = + serde_json::from_str(&modules_json).map_err(error_to_jsvalue)?; + let entry_points: Vec = + serde_json::from_str(&entry_points_json).map_err(error_to_jsvalue)?; + if entry_points.is_empty() { + return Err(error_to_jsvalue( + "entry_points must contain at least one entry", + )); + } + + let policy_modules: Vec = modules + .into_iter() + .map(|module| PolicyModule { + id: Rc::from(module.id.as_str()), + content: Rc::from(module.content.as_str()), + }) + .collect(); + + let entry_points_ref: Vec<&str> = entry_points.iter().map(|s| s.as_str()).collect(); + let compiled = + compile_policy_with_entrypoint(data, &policy_modules, Rc::from(entry_points_ref[0])) + .map_err(error_to_jsvalue)?; + let program = Compiler::compile_from_policy(&compiled, &entry_points_ref) + .map_err(error_to_jsvalue)?; + Ok(Program { program }) + } + + /// Serialize a program to binary format. + pub fn serializeBinary(&self) -> Result, JsValue> { + self.program + .serialize_binary() + .map_err(|e| error_to_jsvalue(e.to_string())) + } + + /// Deserialize an RVM program from binary format. + pub fn deserializeBinary(data: Vec) -> Result { + let (program, is_partial) = + match RvmProgram::deserialize_binary(&data).map_err(error_to_jsvalue)? { + DeserializationResult::Complete(program) => (program, false), + DeserializationResult::Partial(program) => (program, true), + }; + Ok(ProgramDeserializationResult { + program: Arc::new(program), + is_partial, + }) + } + + /// Generate a readable assembly listing. + pub fn generateListing(&self) -> Result { + Ok(generate_assembly_listing( + self.program.as_ref(), + &AssemblyListingConfig::default(), + )) + } + + /// Generate a tabular assembly listing. + pub fn generateTabularListing(&self) -> Result { + Ok(generate_tabular_assembly_listing( + self.program.as_ref(), + &AssemblyListingConfig::default(), + )) + } +} + +#[wasm_bindgen] +impl Rvm { + #[wasm_bindgen(constructor)] + pub fn new() -> Self { + Self { vm: RegoVM::new() } + } + + /// Load a program into the VM. + pub fn loadProgram(&mut self, program: &Program) { + self.vm.load_program(program.program.clone()); + } + + /// Set VM data from JSON. + pub fn setDataJson(&mut self, data_json: String) -> Result<(), JsValue> { + let data = Value::from_json_str(&data_json).map_err(error_to_jsvalue)?; + self.vm.set_data(data).map_err(error_to_jsvalue) + } + + /// Set VM input from JSON. + pub fn setInputJson(&mut self, input_json: String) -> Result<(), JsValue> { + let input = Value::from_json_str(&input_json).map_err(error_to_jsvalue)?; + self.vm.set_input(input); + Ok(()) + } + + /// Set execution mode (0 = run-to-completion, 1 = suspendable). + pub fn setExecutionMode(&mut self, mode: u8) -> Result<(), JsValue> { + let mode = match mode { + 0 => ExecutionMode::RunToCompletion, + 1 => ExecutionMode::Suspendable, + _ => return Err(error_to_jsvalue("invalid execution mode")), + }; + self.vm.set_execution_mode(mode); + Ok(()) + } + + /// Execute the program and return the JSON result. + pub fn execute(&mut self) -> Result { + let value = self.vm.execute().map_err(error_to_jsvalue)?; + value.to_json_str().map_err(error_to_jsvalue) + } + + /// Execute an entry point by name and return the JSON result. + pub fn executeEntryPoint(&mut self, entry_point: String) -> Result { + let value = self + .vm + .execute_entry_point_by_name(&entry_point) + .map_err(error_to_jsvalue)?; + value.to_json_str().map_err(error_to_jsvalue) + } + + /// Resume execution with an optional JSON value. + pub fn resume(&mut self, resume_json: Option) -> Result { + let value = if let Some(json) = resume_json { + Some(Value::from_json_str(&json).map_err(error_to_jsvalue)?) + } else { + None + }; + let result = self.vm.resume(value).map_err(error_to_jsvalue)?; + result.to_json_str().map_err(error_to_jsvalue) + } + + /// Get the execution state as a string. + pub fn getExecutionState(&self) -> String { + format!("{:?}", self.vm.execution_state()) + } +} + +impl Default for Rvm { + fn default() -> Self { + Self::new() + } +} + #[cfg(test)] mod tests { use crate::error_to_jsvalue; diff --git a/bindings/wasm/test.js b/bindings/wasm/test.js index 61a0677..3995c6f 100644 --- a/bindings/wasm/test.js +++ b/bindings/wasm/test.js @@ -85,3 +85,78 @@ console.log(report); // Print pretty report. report = engine.getCoverageReportPretty(); console.log(report); + +// RVM regular example +{ +const policy = ` +package demo +import rego.v1 + +default allow := false + +allow if { + input.user == "alice" + input.active == true +} +`; + +const modules = JSON.stringify([ + { id: "demo.rego", content: policy } +]); +const entryPoints = JSON.stringify(["data.demo.allow"]); + +const program = regorus.Program.compileFromModules( + "{}", + modules, + entryPoints +); + +console.log(program.generateListing()); + +const binary = program.serializeBinary(); +const deserialized = regorus.Program.deserializeBinary(binary); +if (deserialized.isPartial) { + throw new Error("Deserialized program marked partial"); +} +const rehydrated = deserialized.program(); + +const vm = new regorus.Rvm(); +vm.loadProgram(rehydrated); +vm.setInputJson('{"user":"alice","active":true}'); +console.log(vm.execute()); +} + +// RVM HostAwait example +{ +const policy = ` +package demo +import rego.v1 + +default allow := false + +allow if { + input.account.active == true + details := __builtin_host_await(input.account.id, "account") + details.tier == "gold" +} +`; + +const modules = JSON.stringify([ + { id: "await.rego", content: policy } +]); +const entryPoints = JSON.stringify(["data.demo.allow"]); + +const program = regorus.Program.compileFromModules( + "{}", + modules, + entryPoints +); + +const vm = new regorus.Rvm(); +vm.setExecutionMode(1); +vm.loadProgram(program); +vm.setInputJson('{"account":{"id":"acct-1","active":true}}'); +vm.execute(); +console.log(vm.getExecutionState()); +console.log(vm.resume('{"tier":"gold"}')); +} diff --git a/src/languages/rego/compiler/function_calls.rs b/src/languages/rego/compiler/function_calls.rs index adc30cd..fb9b149 100644 --- a/src/languages/rego/compiler/function_calls.rs +++ b/src/languages/rego/compiler/function_calls.rs @@ -28,6 +28,9 @@ enum CallTarget { builtin_index: u16, expected_args: Option, }, + HostAwait { + expected_args: Option, + }, } impl<'a> Compiler<'a> { @@ -56,6 +59,7 @@ impl<'a> Compiler<'a> { let expected_args = match &call_target { CallTarget::User { expected_args, .. } => *expected_args, CallTarget::Builtin { expected_args, .. } => *expected_args, + CallTarget::HostAwait { expected_args } => *expected_args, }; if let Some(expected) = expected_args { @@ -122,6 +126,26 @@ impl<'a> Compiler<'a> { }); self.emit_instruction(Instruction::BuiltinCall { params_index }, &span); } + CallTarget::HostAwait { .. } => { + if arg_regs.len() != 2 { + return Err(CompilerError::General { + message: format!( + "__builtin_host_await expects 2 arguments, got {}", + arg_regs.len() + ), + } + .at(&span)); + } + + self.emit_instruction( + Instruction::HostAwait { + dest, + arg: arg_regs[0], + id: arg_regs[1], + }, + &span, + ); + } } if let Some((plan, plan_span)) = &out_param_plan { @@ -162,6 +186,12 @@ impl<'a> Compiler<'a> { full_fcn_path: &str, span: &Span, ) -> Result { + if original_fcn_path == "__builtin_host_await" { + return Ok(CallTarget::HostAwait { + expected_args: Some(2), + }); + } + if self.is_user_defined_function(full_fcn_path) { let rule_index = self.get_or_assign_rule_index(full_fcn_path)?; let expected_args = self diff --git a/tests/rvm/rego/cases/host_await.yaml b/tests/rvm/rego/cases/host_await.yaml new file mode 100644 index 0000000..c26947e --- /dev/null +++ b/tests/rvm/rego/cases/host_await.yaml @@ -0,0 +1,103 @@ +cases: + - note: host_await_run_to_completion + data: {} + input: + enabled: true + skip_interpreter: true + execution_mode: run-to-completion + modules: + - | + package demo + import rego.v1 + + allow := [ + __builtin_host_await("ping", "id-1"), + __builtin_host_await("pong", "id-2") + ] if { + input.enabled + } + query: data.demo.allow + host_await_responses: + - id: "id-1" + value: "response-1" + - id: "id-2" + value: "response-2" + want_result: ["response-1", "response-2"] + + - note: host_await_suspendable_queue + data: {} + input: + enabled: true + payloads: ["a", "b"] + skip_interpreter: true + execution_mode: suspendable + modules: + - | + package demo + import rego.v1 + + allow := [result | + input.enabled + payload := input.payloads[_] + result := __builtin_host_await(payload, "queue") + ] + query: data.demo.allow + host_await_responses_suspendable: + - id: "queue" + value: "first" + - id: "queue" + value: "second" + want_result: ["first", "second"] + + - note: host_await_nested_rule + data: {} + input: + enabled: true + token: "alpha" + skip_interpreter: true + execution_mode: suspendable + modules: + - | + package demo + import rego.v1 + + allow if { + input.enabled + result := nested_result + result == "ok" + } + + nested_result := __builtin_host_await(input.token, "nested") + query: data.demo.allow + host_await_responses_suspendable: + - id: "nested" + value: "ok" + want_result: true + + - note: host_await_nested_comprehension + data: {} + input: + enabled: true + items: ["x", "y"] + skip_interpreter: true + execution_mode: suspendable + modules: + - | + package demo + import rego.v1 + + allow := [outer | + input.enabled + inner := [r | + item := input.items[_] + r := __builtin_host_await(item, "nested-comp") + ] + outer := inner + ] + query: data.demo.allow + host_await_responses_suspendable: + - id: "nested-comp" + value: "first" + - id: "nested-comp" + value: "second" + want_result: [["first", "second"]] diff --git a/tests/rvm/rego/mod.rs b/tests/rvm/rego/mod.rs index 35207f7..c25b401 100644 --- a/tests/rvm/rego/mod.rs +++ b/tests/rvm/rego/mod.rs @@ -6,10 +6,11 @@ use anyhow::Result; use regorus::languages::rego::compiler::Compiler; use regorus::rvm::program::{generate_tabular_assembly_listing, AssemblyListingConfig, Program}; use regorus::rvm::tests::test_utils::test_round_trip_serialization; -use regorus::rvm::vm::RegoVM; +use regorus::rvm::vm::{ExecutionMode, ExecutionState, RegoVM, SuspendReason}; use regorus::test_utils::{check_output, process_value, value_or_vec_to_vec, ValueOrVec}; use regorus::{CompiledPolicy, Engine, Rc, Value}; use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, VecDeque}; use std::fs; use test_generator::test_resources; @@ -35,6 +36,11 @@ struct TestCase { pub strict: bool, pub allow_interpreter_success: Option, pub allow_interpreter_incorrect_behavior: Option, + pub skip_interpreter: Option, + pub execution_mode: Option, + pub host_await_responses: Option>, + pub host_await_responses_run_to_completion: Option>, + pub host_await_responses_suspendable: Option>, } fn default_strict() -> bool { @@ -46,11 +52,86 @@ struct YamlTest { pub cases: Vec, } +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +struct HostAwaitResponseSpec { + pub id: Value, + pub value: Value, +} + +#[derive(Debug, Clone)] +struct RvmExecutionOptions { + execution_mode: ExecutionMode, + host_await_responses_run_to_completion: Option)>>, + host_await_responses_suspendable: Option>>, +} + +impl Default for RvmExecutionOptions { + fn default() -> Self { + Self { + execution_mode: ExecutionMode::RunToCompletion, + host_await_responses_run_to_completion: None, + host_await_responses_suspendable: None, + } + } +} + fn render_program_listing(program: &Program) -> String { let config = AssemblyListingConfig::default(); generate_tabular_assembly_listing(program, &config) } +fn build_host_await_response_map( + responses: &[HostAwaitResponseSpec], +) -> anyhow::Result>> { + let mut map: BTreeMap> = BTreeMap::new(); + for response in responses { + let id = process_value(&response.id)?; + let value = process_value(&response.value)?; + map.entry(id).or_default().push_back(value); + } + Ok(map) +} + +fn build_host_await_response_vec( + responses: &[HostAwaitResponseSpec], +) -> anyhow::Result)>> { + let map = build_host_await_response_map(responses)?; + Ok(map + .into_iter() + .map(|(id, values)| (id, values.into_iter().collect())) + .collect()) +} + +fn build_execution_options(case: &TestCase) -> anyhow::Result { + let execution_mode = match case.execution_mode.as_deref() { + None | Some("run-to-completion") => ExecutionMode::RunToCompletion, + Some("suspendable") => ExecutionMode::Suspendable, + Some(other) => { + return Err(anyhow::anyhow!("unsupported execution_mode: {other}")); + } + }; + + let rtc_responses = case + .host_await_responses_run_to_completion + .as_ref() + .or(case.host_await_responses.as_ref()) + .map(|responses| build_host_await_response_vec(responses)) + .transpose()?; + + let suspendable_responses = case + .host_await_responses_suspendable + .as_ref() + .or(case.host_await_responses.as_ref()) + .map(|responses| build_host_await_response_map(responses)) + .transpose()?; + + Ok(RvmExecutionOptions { + execution_mode, + host_await_responses_run_to_completion: rtc_responses, + host_await_responses_suspendable: suspendable_responses, + }) +} + fn dump_rvm_listing(case_note: &str, listing: &Option) { if let Some(listing) = listing { eprintln!("\n===== RVM assembly for '{}' =====", case_note); @@ -87,6 +168,7 @@ fn compile_and_run_rvm( data: &Value, input: &Value, listing_out: &mut Option, + execution_options: &RvmExecutionOptions, ) -> anyhow::Result { let results = compile_and_run_rvm_with_all_entry_points( compiled_policy, @@ -94,6 +176,7 @@ fn compile_and_run_rvm( data, input, listing_out, + execution_options, )?; results .into_iter() @@ -108,6 +191,7 @@ fn compile_and_run_rvm_with_entry_points( data: &Value, input: &Value, listing_out: &mut Option, + execution_options: &RvmExecutionOptions, ) -> anyhow::Result { let results = compile_and_run_rvm_with_all_entry_points( compiled_policy, @@ -115,6 +199,7 @@ fn compile_and_run_rvm_with_entry_points( data, input, listing_out, + execution_options, )?; if let Some(index) = entry_points @@ -140,6 +225,7 @@ fn compile_and_run_rvm_with_all_entry_points( data: &Value, input: &Value, listing_out: &mut Option, + execution_options: &RvmExecutionOptions, ) -> anyhow::Result> { let program = Compiler::compile_from_policy(compiled_policy, entry_points)?; @@ -153,9 +239,61 @@ fn compile_and_run_rvm_with_all_entry_points( vm.set_data(data.clone())?; vm.set_input(input.clone()); + if execution_options.execution_mode == ExecutionMode::Suspendable { + vm.set_execution_mode(ExecutionMode::Suspendable); + } + + if execution_options.execution_mode == ExecutionMode::RunToCompletion { + if let Some(responses) = &execution_options.host_await_responses_run_to_completion { + vm.set_host_await_responses(responses.clone()); + } + } + let mut results = Vec::new(); for (idx, _) in entry_points.iter().enumerate() { - let result = if entry_points.len() == 1 { + let result = if execution_options.execution_mode == ExecutionMode::Suspendable { + let mut suspendable_responses = execution_options + .host_await_responses_suspendable + .clone() + .unwrap_or_default(); + let _ = if entry_points.len() == 1 { + vm.execute()? + } else { + vm.execute_entry_point_by_index(idx)? + }; + + loop { + match vm.execution_state() { + ExecutionState::Completed { result } => break result.clone(), + ExecutionState::Error { error } => { + return Err(anyhow::anyhow!("{}", error)); + } + ExecutionState::Suspended { reason, .. } => match reason { + SuspendReason::HostAwait { identifier, .. } => { + let response = suspendable_responses + .get_mut(identifier) + .and_then(|queue| queue.pop_front()) + .ok_or_else(|| { + anyhow::anyhow!( + "Missing HostAwait response for identifier {:?}", + identifier + ) + })?; + vm.resume(Some(response))?; + } + other => { + return Err(anyhow::anyhow!( + "Unexpected suspension reason: {:?}", + other + )); + } + }, + ExecutionState::Running | ExecutionState::Ready => { + return Err(anyhow::anyhow!("VM stuck in running state")); + } + } + } + } else if entry_points.len() == 1 { vm.execute()? } else { vm.execute_entry_point_by_index(idx)? @@ -201,8 +339,8 @@ fn yaml_test_impl(file: &str) -> Result<()> { engine.add_policy(format!("rego_{idx}"), module.clone())?; } - if let Some(data) = case.data { - engine.add_data(data)?; + if let Some(ref data) = case.data { + engine.add_data(data.clone())?; } let input_value = case @@ -221,7 +359,13 @@ fn yaml_test_impl(file: &str) -> Result<()> { let entrypoint_ref = Rc::from(case.query.as_str()); let compilation_result = engine.compile_with_entrypoint(&entrypoint_ref); let data = engine.get_data(); - let interpreter_result = engine.eval_rule(case.query.clone()); + let interpreter_result = if case.skip_interpreter == Some(true) { + None + } else { + Some(engine.eval_rule(case.query.clone())) + }; + + let execution_options = build_execution_options(&case)?; if let Err(compilation_error) = &compilation_result { if let (None, Some(expected_error)) = (&case.want_result, &case.want_error) { @@ -275,6 +419,7 @@ fn yaml_test_impl(file: &str) -> Result<()> { &data, &input_value, &mut last_listing, + &execution_options, ) { Ok(actual_results) => { if actual_results.len() != expected_results.len() { @@ -359,6 +504,7 @@ fn yaml_test_impl(file: &str) -> Result<()> { &data, &input_value, &mut last_listing, + &execution_options, ) } else { compile_and_run_rvm( @@ -367,41 +513,52 @@ fn yaml_test_impl(file: &str) -> Result<()> { &data, &input_value, &mut last_listing, + &execution_options, ) }; match result { Ok(actual_result) => { - match &interpreter_result { - Ok(interpreter_value) => { - if actual_result != *interpreter_value { + if let Some(interpreter_result) = &interpreter_result { + match interpreter_result { + Ok(interpreter_value) => { + if actual_result != *interpreter_value { + if case.allow_interpreter_incorrect_behavior == Some(true) { + println!( + "✓ RVM result differs from interpreter for case '{}' (allowed)", + case.note + ); + } else { + panic_with_listing!( + &last_listing, + &case.note, + "RVM result does not match interpreter result for case '{}':\nRVM: {:?}\nInterpreter: {:?}", + case.note, + actual_result, + interpreter_value + ); + } + } + } + Err(err) => { if case.allow_interpreter_incorrect_behavior == Some(true) { println!( - "✓ RVM result differs from interpreter for case '{}' (allowed)", - case.note + "✓ Interpreter failed for case '{}' but RVM succeeded (allowed): {}", + case.note, + err ); } else { panic_with_listing!( &last_listing, &case.note, - "RVM result does not match interpreter result for case '{}':\nRVM: {:?}\nInterpreter: {:?}", + "Interpreter failed for case '{}' but RVM succeeded:\nRVM result: {:?}\nInterpreter error: {}", case.note, actual_result, - interpreter_value + err ); } } } - Err(err) => { - panic_with_listing!( - &last_listing, - &case.note, - "Interpreter failed for case '{}' but RVM succeeded:\nRVM result: {:?}\nInterpreter error: {}", - case.note, - actual_result, - err - ); - } } let expected_results = value_or_vec_to_vec(expected_result.clone()); @@ -409,7 +566,7 @@ fn yaml_test_impl(file: &str) -> Result<()> { check_output(&actual_results, &expected_results)?; } Err(e) => match &interpreter_result { - Ok(interpreter_value) => { + Some(Ok(interpreter_value)) => { if case.allow_interpreter_success == Some(true) { println!( "✓ RVM detected conflict for case '{}' (interpreter success allowed): {}", @@ -427,7 +584,7 @@ fn yaml_test_impl(file: &str) -> Result<()> { ); } } - Err(err) => { + Some(Err(err)) => { panic_with_listing!( &last_listing, &case.note, @@ -437,6 +594,15 @@ fn yaml_test_impl(file: &str) -> Result<()> { e ); } + None => { + panic_with_listing!( + &last_listing, + &case.note, + "RVM failed for case '{}' but a result was expected:\nRVM error: {}", + case.note, + e + ); + } }, } } @@ -450,6 +616,7 @@ fn yaml_test_impl(file: &str) -> Result<()> { &data, &input_value, &mut last_listing, + &execution_options, ) } else { compile_and_run_rvm( @@ -458,12 +625,13 @@ fn yaml_test_impl(file: &str) -> Result<()> { &data, &input_value, &mut last_listing, + &execution_options, ) }; match result { Ok(result) => match &interpreter_result { - Ok(interpreter_value) => { + Some(Ok(interpreter_value)) => { panic_with_listing!( &last_listing, &case.note, @@ -474,7 +642,7 @@ fn yaml_test_impl(file: &str) -> Result<()> { interpreter_value ); } - Err(_) => { + Some(Err(_)) => { panic_with_listing!( &last_listing, &case.note, @@ -484,9 +652,19 @@ fn yaml_test_impl(file: &str) -> Result<()> { serde_json::to_string_pretty(&result)? ); } + None => { + panic_with_listing!( + &last_listing, + &case.note, + "Test case '{}' expected error '{}' but RVM succeeded:\nRVM result: {}", + case.note, + expected_error, + serde_json::to_string_pretty(&result)? + ); + } }, Err(actual_error) => match &interpreter_result { - Ok(interpreter_value) => { + Some(Ok(interpreter_value)) => { if case.allow_interpreter_success == Some(true) { let actual_error_str = actual_error.to_string(); if !actual_error_str.contains(expected_error) { @@ -514,7 +692,7 @@ fn yaml_test_impl(file: &str) -> Result<()> { ); } } - Err(_) => { + Some(Err(_)) | None => { let actual_error_str = actual_error.to_string(); if !actual_error_str.contains(expected_error) { panic_with_listing!( diff --git a/xtask/src/tasks/bindings/c.rs b/xtask/src/tasks/bindings/c.rs index 21a4569..87ec0c9 100644 --- a/xtask/src/tasks/bindings/c.rs +++ b/xtask/src/tasks/bindings/c.rs @@ -44,7 +44,8 @@ impl TestCCommand { if !self.skip_ffi { prepare_ffi_artifacts(self.release, self.frozen)?; } - run_binding("bindings/c", "regorus_test", self.release) + run_binding("bindings/c", "regorus_test", self.release)?; + run_binding("bindings/c", "regorus_rvm_test", self.release) } } diff --git a/xtask/src/tasks/bindings/cpp.rs b/xtask/src/tasks/bindings/cpp.rs index 4708ab7..367bb6b 100644 --- a/xtask/src/tasks/bindings/cpp.rs +++ b/xtask/src/tasks/bindings/cpp.rs @@ -26,6 +26,7 @@ impl TestCppCommand { if !self.skip_ffi { prepare_ffi_artifacts(self.release, self.frozen)?; } - run_binding("bindings/cpp", "regorus_test", self.release) + run_binding("bindings/cpp", "regorus_test", self.release)?; + run_binding("bindings/cpp", "regorus_rvm_test", self.release) } } diff --git a/xtask/src/tasks/bindings/csharp.rs b/xtask/src/tasks/bindings/csharp.rs index 11b038d..d9b7955 100644 --- a/xtask/src/tasks/bindings/csharp.rs +++ b/xtask/src/tasks/bindings/csharp.rs @@ -407,6 +407,7 @@ fn run_regorus_tests( if clean { clean_msbuild_project(®orus_tests)?; } + println!("Running Regorus.Tests (includes RVM program tests)..."); restore_with_source( ®orus_tests, &property_args, diff --git a/xtask/src/tasks/bindings/go.rs b/xtask/src/tasks/bindings/go.rs index 9efccac..ff8db5b 100644 --- a/xtask/src/tasks/bindings/go.rs +++ b/xtask/src/tasks/bindings/go.rs @@ -40,6 +40,14 @@ impl TestGoCommand { run_go_command(&go_dir, ["mod", "tidy"], "go mod tidy (bindings/go)")?; run_go_command(&go_dir, ["build"], "go build (bindings/go)")?; + let lib_dir = ffi_dir.join("target").join(profile); + let mut go_test = Command::new("go"); + go_test.current_dir(&go_dir); + go_test.arg("test"); + go_test.arg("./..."); + add_library_search_path(&mut go_test, &lib_dir); + run_command(go_test, "go test ./... (bindings/go)")?; + let binary = go_test_binary(&go_dir); if !binary.exists() { return Err(anyhow!( @@ -48,7 +56,6 @@ impl TestGoCommand { )); } - let lib_dir = ffi_dir.join("target").join(profile); let mut run = Command::new(&binary); run.current_dir(&go_dir); add_library_search_path(&mut run, &lib_dir);